Automating SDK Releases with the Speakeasy GitHub Action

This guide wires up the Speakeasy GitHub Action so that every change to your OpenAPI spec automatically regenerates your SDKs, opens a review pull request, and cuts a tagged release. It is part of the Speakeasy SDK generation section and the broader SDK Generation & Changelog Automation framework. The two moving parts are .speakeasy/workflow.yaml, which declares your spec source and SDK targets, and the speakeasy-api/sdk-generation-action, which runs that workflow in CI.

Speakeasy automated release pipeline A spec change triggers the Speakeasy GitHub Action, which regenerates the SDK, opens a pull request, and cuts a tagged release on merge. spec change openapi.yaml Speakeasy Action run-workflow regen PR mode: pr tagged release on merge

Problem & Context

When SDKs are generated by hand, the spec and the published clients drift. An engineer ships a new endpoint, forgets to regenerate, and the SDK lags by a release or two — or worse, someone regenerates locally and commits an SDK built from an uncommitted spec. The fix is to make regeneration a CI event tied to the spec, not a manual chore tied to memory.

The Speakeasy GitHub Action does exactly this. It reads .speakeasy/workflow.yaml, regenerates every SDK target, and — depending on mode — either opens a pull request you can review or commits directly. When the generated SDK actually changes, it bumps the version, tags the release, and publishes. This guide assumes you already have an OpenAPI spec; for a comparison with the configuration-file approach, see configuring Fern for multiple languages.

Step-by-Step Solution

1. Install the Speakeasy CLI

Install locally so you can author and test the workflow before pushing CI.

curl -fsSL https://go.speakeasy.com/cli-install.sh | sh
speakeasy --version

Expected output:

1.517.3

2. Create .speakeasy/workflow.yaml

Generate the scaffold with the interactive setup, which writes both the workflow and a .speakeasy/gen.yaml per target.

speakeasy quickstart

Then confirm .speakeasy/workflow.yaml declares your source spec and at least one SDK target:

# .speakeasy/workflow.yaml
workflowVersion: 1.0.0
speakeasyVersion: 1.517.3
sources:
  acme-api:
    inputs:
      - location: ./openapi.yaml
targets:
  typescript-sdk:
    target: typescript
    source: acme-api
    publish:
      npm:
        token: $NPM_TOKEN
  python-sdk:
    target: python
    source: acme-api
    publish:
      pypi:
        token: $PYPI_TOKEN

3. Validate the workflow locally

Run the workflow once on your machine to confirm the spec resolves and the SDKs generate.

speakeasy run

Expected output:

│ Workflow - success
│   └─Target: typescript-sdk - success
│   └─Target: python-sdk - success

4. Store the Speakeasy API key as a secret

The action needs SPEAKEASY_API_KEY to pull your workspace config. Copy it from the Speakeasy dashboard, then add it (plus your registry tokens) under the repository’s Settings → Secrets and variables → Actions.

gh secret set SPEAKEASY_API_KEY --body "$SPEAKEASY_API_KEY"
gh secret set NPM_TOKEN --body "$NPM_TOKEN"
gh secret set PYPI_TOKEN --body "$PYPI_TOKEN"

5. Add the Speakeasy GitHub Action workflow

Create .github/workflows/sdk-generation.yaml. Pin the action to a major version and set action: run-workflow with mode: pr so changes arrive as reviewable pull requests.

# .github/workflows/sdk-generation.yaml
name: Generate SDKs
on:
  push:
    branches: [main]
    paths:
      - "openapi.yaml"
      - ".speakeasy/**"
  workflow_dispatch: {}

jobs:
  generate:
    permissions:
      checks: write
      contents: write
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: speakeasy-api/sdk-generation-action@v15
        with:
          action: run-workflow
          mode: pr
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

6. Trigger regeneration on spec change

Commit a spec change and push. The action runs, and if the SDK output differs, it opens a PR.

git commit -am "feat(api): add /invoices endpoint"
git push origin main

Expected result in the Actions log:

Speakeasy SDK Generation
✓ Regenerated typescript-sdk (1 endpoint added)
✓ Regenerated python-sdk (1 endpoint added)
✓ Opened PR #42: chore: regenerate SDKs

Merging PR #42 with the release step enabled bumps the version, tags the commit, and publishes the SDKs.

Complete Working Example

A single repository setup: .speakeasy/workflow.yaml that defines the source and two targets with publishing, and the GitHub Action that runs it on spec change and publishes on merge.

# .speakeasy/workflow.yaml
workflowVersion: 1.0.0
speakeasyVersion: 1.517.3
sources:
  acme-api:
    inputs:
      - location: ./openapi.yaml
targets:
  typescript-sdk:
    target: typescript
    source: acme-api
    publish:
      npm:
        token: $NPM_TOKEN
  python-sdk:
    target: python
    source: acme-api
    publish:
      pypi:
        token: $PYPI_TOKEN
# .github/workflows/sdk-generation.yaml
name: Generate and Publish SDKs
on:
  push:
    branches: [main]
    paths:
      - "openapi.yaml"
      - ".speakeasy/**"
  workflow_dispatch: {}

jobs:
  generate:
    permissions:
      checks: write
      contents: write
      pull-requests: write
      statuses: write
    runs-on: ubuntu-latest
    steps:
      - name: Regenerate SDKs and open PR
        uses: speakeasy-api/sdk-generation-action@v15
        with:
          action: run-workflow
          mode: pr
          force: false
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

  publish:
    needs: generate
    permissions:
      contents: write
      statuses: write
    runs-on: ubuntu-latest
    steps:
      - name: Publish on merged regeneration
        uses: speakeasy-api/sdk-generation-action@v15
        with:
          action: release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}

Pushing a spec change opens a regeneration PR; merging it runs the release action, which tags and publishes the new SDK versions.

Gotchas & Edge Cases

The action needs write permissions to open PRs and tag releases. The default GITHUB_TOKEN has read-only permissions on many repositories. Grant contents: write and pull-requests: write in the job’s permissions block, or the action fails with Resource not accessible by integration when it tries to push the regeneration branch.

A no-op run is correct behavior, not a failure. If the spec change does not alter the generated SDK (for example, a description-only tweak), Speakeasy detects no diff and exits without opening a PR or cutting a release. Do not add force: true to “fix” this — forcing regeneration creates empty version bumps and noisy releases.

mode: direct skips review. Setting mode: direct commits regenerated SDKs straight to the branch, which is convenient for trusted internal repos but removes the human checkpoint. For public SDKs keep mode: pr so a reviewer can catch breaking changes before they reach npm or PyPI.

Decide between direct publish and review-first before the first release. Both are legitimate and they suit different situations. Direct publish keeps SDKs continuously in step with the contract and suits an API whose consumers are internal and whose spec changes are already carefully reviewed. Review-first opens a pull request per SDK repository so someone reads the regenerated client before it ships, which is what you want for public libraries where the generated diff can reveal a naming regression or an unexpected type change that the contract diff did not.

Switching between them later is easy; discovering you needed the other one after a bad release is not. The question that decides it is whether anyone would benefit from seeing the client diff — if the answer is yes for even one language, take the review-first path for all of them so the pipelines stay uniform.

Give the automation its own identity. A release pipeline that runs under a personal access token breaks when that person changes role, and its actions are attributed to them in every repository it touches. Use a dedicated machine identity with narrowly scoped permissions — write access to the SDK repositories and publish rights to their packages, nothing more — and record the expiry somewhere that alerts before it lapses.

Make failures noisy in the right channel. An SDK release job that fails silently is worse than one that does not exist, because the team believes the SDKs are current when they are several releases behind. Route failures to wherever the owning team actually reads notifications, and include the language and the contract version in the message so the failure can be triaged without opening the log.

Keep the action pinned to a version, not a floating tag. An action referenced by a moving tag can change behaviour without any commit in your repository, which turns a release failure into an investigation of somebody else’s changelog. Pin it, and treat upgrading it as a reviewed change like any other dependency bump.

Release failure surfaces A release can fail on the contract, on generation, or on publication, and each failure is diagnosed in a different place. contract invalid reproduces locally generation fails generator or config publish fails credentials or registry

FAQ

What is the difference between the action’s action and mode inputs?

The action input chooses the operation, with run-workflow executing the targets in .speakeasy/workflow.yaml. The mode input chooses how results are delivered, where pr opens a regeneration pull request for review and direct commits straight to the branch.

When does the Speakeasy action create a release?

When the generated SDK changes and the action is configured with a release step, Speakeasy bumps the version, tags the commit, and publishes per the publish block in workflow.yaml. A run that produces no SDK changes ends without opening a PR or cutting a release.

Do I need a Speakeasy API key in CI?

Yes. The action authenticates with a SPEAKEASY_API_KEY secret to pull your workspace configuration and generator entitlements. Store it as an encrypted GitHub Actions secret and pass it through the env block of the workflow job.

The pipeline is only as trustworthy as its last verified run, so exercise it deliberately rather than only when a release is due. A scheduled dry run against the current contract confirms that credentials are valid, the generator still resolves, and the publish path still works — turning the three failure surfaces above into scheduled maintenance instead of release-day surprises.

One more property worth designing for: the pipeline touches several repositories, so its permissions are broader than a typical workflow’s. Keeping that surface small and reviewable is what stops a convenience today becoming an audit finding later.

Scoping the release automation identity A dedicated machine identity with write access limited to the SDK repositories keeps the release pipeline auditable and survivable when people change role. the pipeline writes to several repositories — scope it deliberately a personal token breaks on a role change actions attributed to a person avoid a broad org token works everywhere far more access than needed tighten a scoped machine identity SDK repos and packages only expiry tracked and alerted the durable choice