Skip to content

anuran-roy/stained

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 

Repository files navigation

SDK Workflow Generator

Generates GitHub Actions workflows and SDK configuration for automated SDK regeneration whenever your API changes. Push to your backend repo, and a PR with the updated SDK appears in your SDK repo minutes later.

Built on Hey API for OpenAPI-to-TypeScript codegen and GitHub's repository dispatch API for cross-repo triggering.

How it works

Source repo (your backend)              SDK repo
──────────────────────────              ────────
push to main
  │
  ▼
trigger-sdk-regen.yml
  │  waits for deploy to propagate
  │  verifies spec endpoint returns 200
  │  POST /repos/{sdk-repo}/dispatches
  │
  └─────────────────────────────────►  generate-sdk.yml
                                         │  fetches openapi.json from live URL
                                         │  runs Hey API codegen
                                         │  diffs against current SDK
                                         └─ opens PR if anything changed

Two workflows, two repos, plus SDK configuration. The source repo fires a dispatch event; the SDK repo receives it and does the work. The generator also produces an openapi-ts.config.ts and a package.json patcher script for the SDK repo. The SDK repo workflow also supports workflow_dispatch for manual runs from the GitHub UI.

Migrating from Stainless

This tool was built to replace Stainless SDK generation after their service sunset. If your SDK repo currently contains Stainless-generated code, the recommended migration path avoids breaking existing consumers.

The problem

Stainless generates the entire src/ directory of your SDK repo: the client class, error types, resource methods, retry logic, upload handling, and the HTTP layer. You can't swap the generator without changing the SDK's public API surface. Stainless produces class-based method chains (client.v1.context.add({...})), and while Hey API supports a similar nested structure via its operations.nesting config, the parameter shapes, constructor signature, error types, and response wrappers all differ.

The approach: subpath export

Instead of replacing src/ and shipping a breaking change, generate Hey API output into a subdirectory alongside the existing Stainless code:

src/
├── index.ts              ← existing Stainless root (unchanged)
├── client.ts             ← existing Stainless client (unchanged)
├── resources/            ← existing Stainless resources (unchanged)
│   └── v1/
│       └── context.ts
└── v1/                   ← new Hey API output (generated)
    ├── index.ts
    ├── client.gen.ts
    ├── sdk.gen.ts
    └── types.gen.ts

Set --sdk-output-dir to src/v1 when running the generator. The Hey API workflow writes only to that directory and never touches existing files.

Consumer experience

The existing root import continues to work unchanged:

// Existing consumers — no changes needed
import AlchemystAI from '@your-org/sdk';

const client = new AlchemystAI({ apiKey: '...' });
await client.v1.context.add({ context_type: 'resource', ... });

The new Hey API SDK is available via a /v1 subpath export:

// New consumers — use the /v1 subpath
import { AlchemystAI } from '@your-org/sdk/v1';

const client = new AlchemystAI({
  baseUrl: 'https://api.example.com',
  auth: () => process.env.API_KEY,
});

await client.v1.context.add({ body: { context_type: 'resource', ... } });

Both imports resolve from the same npm package. Consumers migrate at their own pace.

What to add to package.json

Most Stainless-generated package.json files include a wildcard export map (./*) that already resolves subpath imports. Verify by checking that your exports field contains something like:

"exports": {
  "./*": {
    "import": "./dist/*.mjs",
    "require": "./dist/*.js"
  }
}

If the wildcard is present, @your-org/sdk/v1 resolves to dist/v1/index.js automatically once the build copies src/v1/ into dist/v1/. If your export map is more restrictive, add an explicit entry:

"./v1": {
  "import": "./dist/v1/index.mjs",
  "require": "./dist/v1/index.js",
  "types": "./dist/v1/index.d.ts"
}

Also add the Hey API runtime dependency and dev dependency:

npm install @hey-api/client-fetch
npm install -D @hey-api/openapi-ts

Don't remove any existing Stainless dependencies. The existing build system (tsc-multi, scripts/build) should pick up src/v1/ automatically since it compiles all of src/.

Promoting v1 to root

When you're ready to drop the Stainless code entirely:

  1. Delete everything in src/ except src/v1/
  2. Move src/v1/* up to src/
  3. Replace the Stainless build system (tsc-multi, scripts/) with a standard tool like tsup
  4. Update package.json exports to point the root at the Hey API output
  5. Remove Stainless-specific workflows (release-doctor.yml) and CI branch exclusions
  6. Bump to your next major version and publish

Until that point, both SDK surfaces ship in parallel from the same package with zero interference.

Prerequisites

  • Python 3.6+
  • Two GitHub repos (one for the API source, one for the SDK)
  • A live HTTPS endpoint serving your OpenAPI spec as JSON
  • A GitHub Personal Access Token (explained below)

Quick start

git clone <this-repo>
cd sdk-workflow-generator
chmod +x generate.sh
./generate.sh

The script walks you through each variable, explains what it does, confirms your inputs, then writes the workflow files to an output directory.

Configuration

The generator accepts eight inputs. All are prompted interactively; all can be passed as flags for scripted use.

Flag What it is Example
--source-repo The backend repo that serves your API. Pushes here trigger SDK regeneration. my-org/platform-backend
--sdk-repo The repo that holds the generated SDK. Dispatch events land here. my-org/my-sdk
--sdk-output-dir Directory inside the SDK repo where Hey API writes generated files. Relative to repo root. src/v1
--sdk-class-name Top-level class name for the generated SDK. Consumers instantiate this. AlchemystAI
--openapi-url Live HTTPS URL that returns your OpenAPI spec as JSON. The SDK workflow fetches this on every run. https://api.example.com/openapi.json
--deploy-wait Seconds to wait after push before hitting the spec URL. Gives your CI/CD pipeline time to deploy so the endpoint reflects the latest code. Set to 0 if your spec is committed as a static file. 30
--branch Branch in the source repo that triggers the dispatch. main
--pat-secret-name Name of the GitHub Actions secret storing the PAT in the source repo. SDK_DISPATCH_PAT

Non-interactive usage

./generate.sh \
  --source-repo "my-org/platform-backend" \
  --sdk-repo "my-org/my-sdk" \
  --sdk-output-dir "src/v1" \
  --sdk-class-name "MySDK" \
  --openapi-url "https://api.example.com/openapi.json" \
  --deploy-wait 30 \
  --branch "main" \
  --pat-secret-name "SDK_DISPATCH_PAT" \
  --output-dir "./output"

Or call the Python script directly:

python3 generate_workflows.py \
  --source-repo "my-org/platform-backend" \
  --sdk-repo "my-org/my-sdk" \
  --sdk-output-dir "src/v1" \
  --sdk-class-name "MySDK" \
  --openapi-url "https://api.example.com/openapi.json" \
  --deploy-wait 30 \
  --branch "main" \
  --pat-secret-name "SDK_DISPATCH_PAT" \
  --output-dir "./output"

Output

output/
├── sdk-repo/
│   ├── .github/
│   │   └── workflows/
│   │       └── generate-sdk.yml           ← dispatch listener workflow
│   ├── openapi-ts.config.ts               ← Hey API codegen config
│   └── apply-package-updates.sh           ← run once to patch package.json
└── source-repo/
    └── .github/
        └── workflows/
            └── trigger-sdk-regen.yml      ← dispatch trigger workflow

Copy each workflow file into the corresponding repo's .github/workflows/ directory. Copy openapi-ts.config.ts to the SDK repo root. Run apply-package-updates.sh once in the SDK repo to install dependencies and configure exports, then delete it.

Setup after generating

1. Create a Personal Access Token

The source repo needs permission to trigger a workflow in the SDK repo. GitHub enforces this through a PAT. The built-in GITHUB_TOKEN only has access to the repo it lives in, so cross-repo dispatch requires a separate token.

Go to github.com/settings/tokens?type=beta (fine-grained PATs) and create a token with:

  • Repository access: Only select repositories → pick the SDK repo
  • Permissions:
    • Contents: Read (needed to reference the repo)
    • Actions: Write (needed for repository_dispatch)

Copy the token. You'll use it in the next step.

2. Store the PAT as a secret in the source repo

In the source repo, go to Settings → Secrets and variables → Actions → New repository secret.

  • Name: the secret name you chose during generation (default: SDK_DISPATCH_PAT)
  • Value: the PAT from step 1

The source repo workflow reads ${{ secrets.SDK_DISPATCH_PAT }} (or whatever name you chose) and sends it in the Authorization header when calling the GitHub dispatch API.

3. Set up Hey API in the SDK repo

The generator produces two files for this: apply-package-updates.sh and openapi-ts.config.ts. Run the script once, copy the config, and you're done.

cd /path/to/sdk-repo

# Run the package patcher (installs deps, adds generate script, adds subpath export)
cp /path/to/output/sdk-repo/apply-package-updates.sh .
chmod +x apply-package-updates.sh
./apply-package-updates.sh

# Copy the Hey API config
cp /path/to/output/sdk-repo/openapi-ts.config.ts .

# Clean up the one-time script
rm apply-package-updates.sh

The script detects your package manager (yarn, pnpm, or npm), installs @hey-api/client-fetch and @hey-api/openapi-ts, adds a "generate" script to package.json, and adds a subpath export so consumers can import from @your-org/sdk/v1. It's idempotent — running it twice won't duplicate anything.

4. Copy the workflow files

# SDK repo
cp output/sdk-repo/.github/workflows/generate-sdk.yml \
   /path/to/sdk-repo/.github/workflows/

# Source repo
cp output/source-repo/.github/workflows/trigger-sdk-regen.yml \
   /path/to/source-repo/.github/workflows/

Commit and push both.

5. Test with a manual dispatch

Before relying on push triggers, verify the plumbing works:

curl -X POST \
  -H "Authorization: Bearer YOUR_PAT_HERE" \
  -H "Accept: application/vnd.github+json" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  "https://api.github.com/repos/YOUR-ORG/YOUR-SDK-REPO/dispatches" \
  -d '{"event_type":"openapi-spec-updated","client_payload":{"source_repo":"manual-test"}}'

You should get HTTP 204 No Content. Check the Actions tab on the SDK repo to confirm the workflow ran.

Customization

Path filters

The source repo workflow includes default path filters that determine which pushes trigger SDK regeneration:

paths:
  - "src/api/**"
  - "src/routes/**"
  - "src/schemas/**"
  - "openapi.json"
  - "openapi.yaml"
  - "**/openapi/**"

Edit these after generating to match your project structure. If your OpenAPI spec is auto-generated from route decorators (FastAPI, NestJS), include the source files that feed into it. Remove the paths block entirely to trigger on every push to the branch.

Dispatch event type

Both workflows use openapi-spec-updated as the dispatch event type. If you run multiple dispatch workflows in the same SDK repo, change this string in both files to something unique.

PR behavior

The SDK repo workflow uses peter-evans/create-pull-request to open PRs. By default it pushes to a branch called auto/sdk-regeneration and force-updates it on each run, so you only ever have one open SDK regeneration PR at a time. Merge it or let the next run overwrite it.

To auto-merge instead of opening PRs, replace the Create Pull Request step with a direct commit-and-push.

File structure

sdk-workflow-generator/
├── generate.sh              # Interactive bash wrapper
├── generate_workflows.py    # Python template engine (the actual generator)
└── README.md

generate.sh collects inputs, displays a confirmation summary, and calls generate_workflows.py with the collected values. You can skip the bash wrapper and call the Python script directly with --flags.

License

MIT

About

A quick Hey API-based SDK generator that will help you quickly migrate from Stainless to HeyAPI without immediate breaking changes

Resources

License

Code of conduct

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors