Automated SDK preview and breaking change detection for spec PRs
This guide walks through setting up a workflow where every pull request that modifies the OpenAPI spec automatically:
- Generates SDKs for each target language (TypeScript, Python, Go, etc.)
- Opens a PR in each downstream SDK repository showing the exact code changes
- Posts a status comment on the spec PR linking to each SDK PR
- Reports any breaking changes introduced by the spec modification
- Auto-merges or closes SDK PRs when the spec PR is merged or closed
The end result: the team reviews a spec change and immediately sees how it affects every SDK, including whether the change breaks existing consumers.
Architecture overview
Section titled “Architecture overview”This pattern uses a multi-repo layout with two component types:
- Spec repo (one) — holds the OpenAPI spec, triggers downstream generation
- SDK repos (one per language) — each receives a trigger and generates code
spec-repo ├── specs/openapi.yaml ├── .speakeasy/workflow.yaml (registry source config) └── .github/workflows/ ├── trigger-downstream-sdk-generation.yaml └── reconcile-sdk-prs.yaml
sdk-repo-python ├── .speakeasy/workflow.yaml (target + registry source) └── .github/workflows/ └── generate-sdk-from-spec.yaml
sdk-repo-typescript ├── .speakeasy/workflow.yaml └── .github/workflows/ └── generate-sdk-from-spec.yamlFlow:
- A developer opens a PR in the spec repo modifying
specs/openapi.yaml. - The spec repo workflow tags the new spec version in the Speakeasy registry.
- It then triggers
workflow_dispatchin each downstream SDK repo, passing the spec tag. - Each SDK repo generates code from the tagged spec, opens a PR with the diff, and Speakeasy annotates it with a changelog and breaking change report.
- The spec repo polls each downstream workflow and posts a summary comment on the original spec PR.
- When the spec PR is merged, a reconciliation workflow auto-merges the SDK PRs. If the spec PR is closed without merging, the SDK PRs are closed.
Prerequisites
Section titled “Prerequisites”- A Speakeasy account with a valid API key (see core concepts for an overview of how Speakeasy SDK generation works)
- The Speakeasy CLI (
speakeasy) installed in CI (the workflows handle this automatically) - Separate GitHub repositories for each SDK target
- A fine-grained Personal Access Token (PAT) or GitHub App token with permissions to trigger workflows and manage PRs in the downstream SDK repos (see GitHub setup for details)
Step 1: Configure the spec repo
Section titled “Step 1: Configure the spec repo”1.1 Speakeasy workflow configuration
Section titled “1.1 Speakeasy workflow configuration”Create .speakeasy/workflow.yaml in the spec repo. This tells Speakeasy where the spec lives and where to publish it in the registry (see the workflow file reference for all available options):
workflowVersion: 1.0.0speakeasyVersion: latestsources: my-api: inputs: - location: specs/openapi.yaml registry: location: registry.speakeasyapi.dev/<org>/<workspace>/my-apitargets: {}Key points:
- Replace
<org>/<workspace>with the appropriate Speakeasy organization and workspace identifiers. targets: {}is intentionally empty. The spec repo only publishes to the registry; the downstream SDK repos define their own targets.- When
speakeasy runexecutes, it validates the spec and pushes a tagged version to the registry.
1.2 Trigger downstream SDK generation workflow
Section titled “1.2 Trigger downstream SDK generation workflow”Create .github/workflows/trigger-downstream-sdk-generation.yaml:
trigger-downstream-sdk-generation.yaml
name: Trigger Downstream SDK Generation
permissions: checks: write contents: write pull-requests: write statuses: write id-token: write issues: write
on: pull_request: types: [opened, synchronize, reopened] # To avoid triggering on unrelated changes, scope this to the spec path: # paths: # - "specs/**" workflow_dispatch:
jobs: # ------------------------------------------------------------------ # Job 1: Tag the spec in the Speakeasy registry # ------------------------------------------------------------------ update_spec_and_tag_registry: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.head_ref }}
- name: Install Speakeasy CLI run: | curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | sh echo "$HOME/.speakeasy/bin" >> $GITHUB_PATH
- name: Run Speakeasy workflow env: SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} run: speakeasy run
# ------------------------------------------------------------------ # Job 2: Fan out to each downstream SDK repo # ------------------------------------------------------------------ trigger_sdk_workflows: needs: update_spec_and_tag_registry runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - language: typescript owner: your-org repo: your-sdk-typescript - language: python owner: your-org repo: your-sdk-python # Add more SDK targets as needed: # - language: go # owner: your-org # repo: your-sdk-go steps: - name: Prepare branch tag id: tag run: | echo "name=$(echo "${{ github.head_ref }}" | sed 's|/|-|g')" >> $GITHUB_OUTPUT
- name: Initialize status comment env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | language="${{ matrix.language }}" repo="${{ matrix.repo }}" owner="${{ matrix.owner }}" marker="<!-- sdk-generation-status-$language -->"
comment_body="$marker ### ${language} SDK Generation
| Repository | Workflow Run | Status | SDK PR | | --- | --- | --- | --- | | [$repo](https://github.com/$owner/$repo) | Pending | :hourglass_flowing_sand: Pending | Pending |"
existing_comment_id=$(gh api \ "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ --jq "map(select(.body | contains(\"$marker\"))) | last | .id // empty")
if [[ -n "$existing_comment_id" ]]; then gh api --method PATCH \ "repos/${{ github.repository }}/issues/comments/$existing_comment_id" \ -f body="$comment_body" > /dev/null echo "SDK_COMMENT_ID=$existing_comment_id" >> $GITHUB_ENV else new_comment_id=$(gh api --method POST \ "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \ -f body="$comment_body" --jq '.id') echo "SDK_COMMENT_ID=$new_comment_id" >> $GITHUB_ENV fi
- name: Trigger ${{ matrix.language }} SDK generation env: GH_TOKEN: ${{ secrets.DOWNSTREAM_SDK_TOKEN }} run: | owner="${{ matrix.owner }}" repo="${{ matrix.repo }}" language="${{ matrix.language }}" branch="${{ github.head_ref }}" tag_name="${{ steps.tag.outputs.name }}"
gh workflow run generate-sdk-from-spec.yaml \ --repo "$owner/$repo" --ref main \ --field force="true" \ --field feature_branch="$branch" \ --field environment="TAG=$tag_name"
sleep 10 run_id=$(gh run list --repo "$owner/$repo" \ --workflow generate-sdk-from-spec.yaml \ --limit 1 --json databaseId --jq '.[0].databaseId')
if [[ -z "$run_id" ]]; then echo "Failed to get run ID" exit 1 fi
echo "WORKFLOW_RUN_ID=$run_id" >> $GITHUB_ENV run_url="https://github.com/$owner/$repo/actions/runs/$run_id" echo "WORKFLOW_RUN_URL=$run_url" >> $GITHUB_ENV
marker="<!-- sdk-generation-status-$language -->" pending_body="$marker ### ${language} SDK Generation
| Repository | Workflow Run | Status | SDK PR | | --- | --- | --- | --- | | [$repo](https://github.com/$owner/$repo) | [Workflow run]($run_url) | :hourglass_flowing_sand: In Progress | Pending |"
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh api --method PATCH \ "repos/${{ github.repository }}/issues/comments/$SDK_COMMENT_ID" \ -f body="$pending_body" > /dev/null
- name: Poll workflow status env: GH_TOKEN: ${{ secrets.DOWNSTREAM_SDK_TOKEN }} run: | owner="${{ matrix.owner }}" repo="${{ matrix.repo }}" run_id="$WORKFLOW_RUN_ID"
while true; do resp=$(gh api "repos/$owner/$repo/actions/runs/$run_id") status=$(echo "$resp" | jq -r '.status') conclusion=$(echo "$resp" | jq -r '.conclusion')
if [[ "$status" == "completed" ]]; then if [[ "$conclusion" == "success" ]]; then echo "WORKFLOW_SUCCESS=true" >> $GITHUB_ENV else echo "WORKFLOW_SUCCESS=false" >> $GITHUB_ENV fi break fi sleep 30 done
- name: Finalize PR comment if: always() env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SDK_TOKEN: ${{ secrets.DOWNSTREAM_SDK_TOKEN }} run: | language="${{ matrix.language }}" repo="${{ matrix.repo }}" owner="${{ matrix.owner }}" branch="${{ github.head_ref }}" marker="<!-- sdk-generation-status-$language -->"
pr_url=$(GH_TOKEN="$SDK_TOKEN" gh pr list \ --repo "$owner/$repo" --head "$branch" \ --json url --jq '.[0].url' 2>/dev/null || echo "")
if [[ "$WORKFLOW_SUCCESS" == "true" ]]; then status_cell=":white_check_mark: Success" else status_cell=":x: Failed" fi
run_cell="[Workflow run]($WORKFLOW_RUN_URL)"
if [[ -n "$pr_url" && "$pr_url" != "null" ]]; then pr_cell="[SDK PR]($pr_url)" else compare_url="https://github.com/$owner/$repo/compare/main...$branch" pr_cell="[Review Changes]($compare_url)" fi
comment_body="$marker ### ${language} SDK Generation
| Repository | Workflow Run | Status | SDK PR | | --- | --- | --- | --- | | [$repo](https://github.com/$owner/$repo) | $run_cell | $status_cell | $pr_cell |"
gh api --method PATCH \ "repos/${{ github.repository }}/issues/comments/$SDK_COMMENT_ID" \ -f body="$comment_body"
if [[ "$WORKFLOW_SUCCESS" != "true" ]]; then exit 1 fi1.3 Reconcile SDK PRs workflow
Section titled “1.3 Reconcile SDK PRs workflow”Create .github/workflows/reconcile-sdk-prs.yaml. This handles the lifecycle of downstream SDK PRs when the spec PR is resolved:
reconcile-sdk-prs.yaml
name: Reconcile SDK PRs
permissions: contents: read pull-requests: read
on: pull_request: types: [closed]
jobs: manage_sdk_prs: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - language: typescript owner: your-org repo: your-sdk-typescript - language: python owner: your-org repo: your-sdk-python steps: - name: Manage ${{ matrix.language }} SDK PR env: GH_TOKEN: ${{ secrets.DOWNSTREAM_SDK_TOKEN }} run: | branch_name="${{ github.event.pull_request.head.ref }}" owner="${{ matrix.owner }}" repo="${{ matrix.repo }}" was_merged="${{ github.event.pull_request.merged }}"
pr_data=$(gh pr list --repo "$owner/$repo" \ --head "$branch_name" \ --json number,title,url --jq '.[0]' 2>/dev/null || echo "null")
if [[ "$pr_data" != "null" && -n "$pr_data" ]]; then pr_number=$(echo "$pr_data" | jq -r '.number') pr_title=$(echo "$pr_data" | jq -r '.title')
if [[ "$was_merged" == "true" ]]; then echo "Merging SDK PR #$pr_number..." gh pr merge "$pr_number" --repo "$owner/$repo" \ --squash --delete-branch else echo "Closing SDK PR #$pr_number..." gh pr close "$pr_number" --repo "$owner/$repo" \ --comment "Closing: source spec PR was closed without merging." fi else echo "No matching PR found for branch '$branch_name'." fiStep 2: Configure each downstream SDK repo
Section titled “Step 2: Configure each downstream SDK repo”Each SDK repo needs two things: a Speakeasy workflow config and a GitHub Actions workflow that can be triggered remotely.
2.1 Speakeasy workflow configuration
Section titled “2.1 Speakeasy workflow configuration”Create .speakeasy/workflow.yaml in each SDK repo. This tells Speakeasy to pull the spec from the registry (tagged by the spec repo) and generate code for the appropriate language.
Python example:
workflowVersion: 1.0.0speakeasyVersion: latestsources: my-api: inputs: - location: registry.speakeasyapi.dev/<org>/<workspace>/my-api:${TAG:-latest} registry: location: registry.speakeasyapi.dev/<org>/<workspace>/my-apitargets: python: target: python source: my-api output: .TypeScript example:
workflowVersion: 1.0.0speakeasyVersion: latestsources: my-api: inputs: - location: registry.speakeasyapi.dev/<org>/<workspace>/my-api:${TAG:-latest} registry: location: registry.speakeasyapi.dev/<org>/<workspace>/my-apitargets: typescript: target: typescript source: my-api output: .The ${TAG:-latest} variable is the key mechanism. When the spec repo triggers generation, it passes the branch-specific tag (e.g., feature-add-users-endpoint), so the SDK is generated from exactly the spec version in the PR. When TAG is not set (e.g., on a daily schedule), it falls back to latest.
2.2 SDK generation workflow
Section titled “2.2 SDK generation workflow”Create .github/workflows/generate-sdk-from-spec.yaml in each SDK repo:
generate-sdk-from-spec.yaml
name: Generate SDK from Spec
on: workflow_dispatch: inputs: force: description: "Force SDK regeneration" required: false default: "false" type: string feature_branch: description: "Branch for SDK changes" required: false type: string environment: description: "Environment variables (e.g., TAG=branch-name)" required: false type: string schedule: - cron: "0 0 * * *"
permissions: contents: write pull-requests: write checks: write statuses: write
jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: mode: pr force: ${{ inputs.force || 'false' }} feature_branch: ${{ inputs.feature_branch }} environment: ${{ inputs.environment }} secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }}This workflow:
- Accepts a
feature_branchinput so the SDK PR is created on a branch that matches the spec PR branch name (enabling the reconciliation workflow to find it later). - Accepts an
environmentinput to pass theTAGvariable, which the Speakeasy workflow config uses to pull the correct spec version. - Uses
mode: prso changes are opened as a pull request rather than pushed directly. - Also runs on a daily schedule to catch any registry updates outside of the PR flow.
Step 3: Set up secrets
Section titled “Step 3: Set up secrets”Spec repo secrets
Section titled “Spec repo secrets”| Secret | Purpose |
|---|---|
SPEAKEASY_API_KEY | Authenticates with the Speakeasy platform for registry tagging |
DOWNSTREAM_SDK_TOKEN | Fine-grained PAT (or GitHub App token) that can trigger workflow_dispatch and manage PRs in the downstream SDK repos |
Downstream SDK repo secrets
Section titled “Downstream SDK repo secrets”| Secret | Purpose |
|---|---|
SPEAKEASY_API_KEY | Authenticates with the Speakeasy platform for SDK generation |
The DOWNSTREAM_SDK_TOKEN needs the following permissions on each downstream SDK repo:
- Actions: Read and write (to trigger workflows and poll status)
- Contents: Read and write (for branch creation)
- Pull requests: Read and write (to create, merge, and close PRs)
Step 4: Team visibility
Section titled “Step 4: Team visibility”On the spec PR
Section titled “On the spec PR”When a developer opens a PR that modifies the OpenAPI spec, the workflow posts one comment per SDK target:
### typescript SDK Generation
| Repository | Workflow Run | Status | SDK PR || --- | --- | --- | --- || your-sdk-typescript | Workflow run | ✅ Success | SDK PR |Each “SDK PR” link takes the reviewer directly to the downstream SDK repo’s pull request, where they can inspect the generated code diff.
On each SDK PR
Section titled “On each SDK PR”Speakeasy automatically annotates each SDK PR with:
- OpenAPI change report — A summary of what changed in the spec (new endpoints, modified schemas, removed fields, etc.) with a link to a detailed report on the Speakeasy dashboard.
- Breaking change detection — Explicit callout of any changes that would break existing SDK consumers (removed fields, type changes, required parameter additions, etc.). See breaking changes for more on how Speakeasy handles these.
- Linting report — Spec quality and style issues surfaced during generation. See linting for configuration options.
- Version bump recommendation — Whether the change warrants a patch, minor, or major version bump based on the nature of the modifications. See versioning for how Speakeasy determines version bumps.
- Generated changelog — A human-readable summary of what changed in the SDK code. See SDK changelogs for details.
Reviewers can also use the “Files changed” tab on the SDK PR to see the exact diff of every generated file, giving full visibility into how a spec change translates to SDK code.
Step 5: Adding more SDK targets
Section titled “Step 5: Adding more SDK targets”To add a new language (e.g., Go, Java, Ruby):
-
Create the downstream SDK repo with the same structure:
.speakeasy/workflow.yamltargeting the new language and.github/workflows/generate-sdk-from-spec.yaml. -
Add the target to the matrix in both spec repo workflows:
In
trigger-downstream-sdk-generation.yaml:matrix:include:# ... existing targets ...- language: goowner: your-orgrepo: your-sdk-goIn
reconcile-sdk-prs.yaml:matrix:include:# ... existing targets ...- language: goowner: your-orgrepo: your-sdk-go -
Add
SPEAKEASY_API_KEYas a secret in the new repo. -
Ensure
DOWNSTREAM_SDK_TOKENhas permissions for the new repo.
How breaking change detection works
Section titled “How breaking change detection works”Speakeasy tracks breaking changes at two levels:
-
OpenAPI spec level — When the spec is tagged in the registry, Speakeasy compares it against the previous version and identifies breaking changes according to OpenAPI compatibility rules:
- Removed endpoints or operations
- Required parameters added to existing operations
- Response schema fields removed or type-changed
- Authentication requirements changed
- Enum values removed
-
SDK level — The generated SDK PR reflects these as concrete code changes: removed methods, changed function signatures, modified type definitions. Reviewers see the actual impact on the public API surface of each SDK.
The breaking change report appears in the SDK PR description and is also accessible via the Speakeasy dashboard link. This gives both a high-level summary (“1 breaking change detected”) and a detailed breakdown of what changed and why it’s breaking. For a deeper look at how Speakeasy classifies and surfaces breaking changes, see the breaking changes documentation.