The OpenAPI to npm publishing workflow is one of the most common tasks API teams do, and one of the least standardized.
Most teams stitch together codegen, a build step, and a manual npm publish, then forget how it works between releases. This post is the workflow that holds up: deterministic, dry-runnable, versioned, with breaking-change detection and trusted publishing in the loop.
The five-step workflow
- Source of truth:
openapi.yamlin your API repo. One spec, owned by the team that owns the API. - Generate: spec → TypeScript SDK output in a separate SDK repo (or a subdirectory if you're confident).
- Diff: compare the new SDK's public surface against the last published version. Bump major if breaking.
- Build + dry-run publish: emit dist/, run
npm pack --dry-run, review the file list, runnpm publish --dry-run. - Tag + publish: tag the spec version that produced this SDK, then publish through npm trusted publishing or a narrowly scoped token.
Each step is cheap individually. The discipline is doing all five every release.
What "deterministic" actually means
A second run of the same workflow on the same spec should produce a byte-identical npm tarball. If it doesn't, you have hidden state somewhere — usually a generator version drift, a date stamp in a header, or a timestamp-based filename.
Things that break determinism:
- Pinning the generator with
^instead of an exact version. Pin to1.2.3, not^1.2.3. - Embedding
new Date().toISOString()in generated code. - Sorting maps in different orders across runs (Map iteration order can be insertion-order-dependent).
- Pulling templates from a network source at generate time.
Lock all of these and you can npm pack --dry-run twice in CI to assert the shasum matches before publishing.
A concrete CI pipeline
# .github/workflows/release.yml
name: Release SDK
on:
push:
branches: [main]
paths: ["openapi.yaml"]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Detect breaking changes
uses: oasdiff/oasdiff-action/breaking@main
with:
base: ${{ github.event.before }}:openapi.yaml
revision: openapi.yaml
fail-on: ERR
- name: Generate SDK
run: npx @openapitools/openapi-generator-cli@2.34.0 generate \
-i openapi.yaml -g typescript-fetch -o ./sdk
- name: Build
working-directory: ./sdk
run: |
npm ci
npm run build
- name: Dry-run publish
working-directory: ./sdk
run: npm publish --dry-run
- name: Publish
working-directory: ./sdk
run: npm publish --access public
- name: Tag spec version
run: |
git tag "spec-$(jq -r .version sdk/package.json)"
git push --tags
The shape:
paths: ["openapi.yaml"]— only run when the spec changes.- Breaking change check before generation. Failing here costs nothing; failing after npm publish costs a deprecation.
- Pin the generator to an exact version (
@2.34.0shown — pin to whatever you tested against). Reproducibility matters more than getting the newest features for free. - With npm trusted publishing configured, npm trusts the GitHub Actions OIDC identity for this package and publishes provenance without a long-lived npm token. Keep token-based publishing as a fallback only.
- Tag the spec version on the API repo so anyone can recover the exact OpenAPI that produced any published SDK.
Versioning the spec, the SDK, and the changelog together
A common confusion: which version do you bump? The SDK has its own semver (independent of the API). The spec has its own version (often matching the API).
Practical rule:
- The SDK version follows SDK-surface semver: breaking changes in method names, parameters, response types, error types. Bump major.
- The spec version follows API semver: breaking changes on the wire (removed endpoints, removed fields). Bump major.
- The API URL version (
/v1,/v2) follows the API-surface major.
These don't move together. A non-breaking spec change can produce a breaking SDK change if you renamed an operationId. A breaking spec change can produce a non-breaking SDK change if the SDK never exposed the affected surface.
Pin SDK ↔ spec by tagging the spec at publish time (spec-1.4.0) and including the spec sha in the SDK's package.json (a custom field, or a comment in the readme).
Common failure modes
Forgetting to bump. CI publishes with the previous version and 409s. Add a step that fails early if package.json version equals the last published version on npm.
Accidentally publishing test fixtures. Use files allowlist in package.json. Run npm pack --dry-run and review the file list manually for the first three releases until you trust the allowlist.
Provenance fails. Provenance requires OIDC tokens from a public CI. If you're on a private GitHub Actions runner, drop --provenance or move the publish step to a public runner.
Customer can't npm install. Almost always a missing exports map or a wrong main/module/types triple. See Publishing a TypeScript SDK to npm for the correct shape.
When to outsource the whole pipeline
If you're shipping more than 2 SDKs across more than 1 language, the value of running this in-house starts to evaporate. Hosted pipelines (Sourced, Stainless, Speakeasy) cover spec → SDK → diff → publish as one workflow and surface the diff visually in a dashboard.
With Sourced specifically: upload spec, preview the SDK in the dashboard, see the compatibility report against the last published version, and prepare npm publishing readiness from the same workflow. The CI and publishing work stays visible before go-live.
What Sourced does
Sourced's free tier lets you generate and review the TypeScript SDK before paying: unlimited previews and up to 2 hosted noindex docs review sites. Upgrade when the package is going live and you need repo sync, npm readiness, team review, production docs, or longer retention. Launch is $99/month per API project. Scale is $349/month and adds more API projects, more hosted docs sites, more team capacity, and production custom domains.
Want this end-to-end without writing CI?
Try Sourced free — Sourced runs the spec to TypeScript SDK to npm package readiness to compatibility-report flow with the SDK preview and breaking-change report visible in the dashboard before any npm release. Unlimited previews and up to 2 hosted docs review sites. No credit card required.