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.
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.
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.
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.
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.
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.
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:
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-tsDon'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/.
When you're ready to drop the Stainless code entirely:
- Delete everything in
src/exceptsrc/v1/ - Move
src/v1/*up tosrc/ - Replace the Stainless build system (
tsc-multi,scripts/) with a standard tool liketsup - Update
package.jsonexports to point the root at the Hey API output - Remove Stainless-specific workflows (
release-doctor.yml) and CI branch exclusions - Bump to your next major version and publish
Until that point, both SDK surfaces ship in parallel from the same package with zero interference.
- 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)
git clone <this-repo>
cd sdk-workflow-generator
chmod +x generate.sh
./generate.shThe script walks you through each variable, explains what it does, confirms your inputs, then writes the workflow files to an output directory.
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 |
./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/
├── 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.
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.
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.
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.shThe 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.
# 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.
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.
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.
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.
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.
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.
MIT