API-to-SDK Regeneration with GitHub Actions
Create bot-authored SDK regeneration pull requests from API changes.
This recipe shows how to keep a generated SDK repository in sync with a
REST API repository. When an API pull request merges to main, the API
pipeline validates the backend, dispatches a workflow in the SDK
repository, and the SDK repository regenerates its OpenAPI document and
client code from the exact API commit.
The result is a bot-authored SDK pull request with a clear source commit, OpenAPI change summary, SemVer impact, generated code, and release-ready package version.
When to use this recipe
Use this pattern when:
- The API and SDK live in separate repositories.
- The API exposes an OpenAPI schema generated from server code.
- The SDK is generated from that OpenAPI schema.
- SDK consumers need reviewable pull requests instead of direct commits.
- You want release management to happen from the SDK repository, not from the API repository.
Do not use this pattern when the SDK is hand-written, when the API and SDK are published from one monorepo pipeline, or when consumers do not need a versioned SDK artifact.
Target outcome
After implementation:
- API merges to
mainrun API checks and export OpenAPI. - API CI dispatches SDK CI with only metadata.
- SDK CI checks out the exact API commit.
- SDK CI exports OpenAPI from that API commit.
- SDK CI regenerates the SDK from the exported schema.
- SDK CI classifies the OpenAPI change as major, minor, patch, or none.
- SDK CI chooses the matching Conventional Commit semantics.
- SDK CI opens or updates a GitHub Actions bot pull request.
- SDK CI labels the pull request with
generatedandsemver-*. python-semantic-releasepublishes after the bot pull request merges.
🤖 Reusable agent prompt
Use this prompt when you want an AI coding agent to stand up this recipe quickly for a pair of repositories. It is written to produce a complete implementation, not just a sketch.
End-to-end verification can write to GitHub
This prompt includes verification steps that may push repositories, configure GitHub secrets, trigger workflows, merge pull requests, create Git tags, create GitHub releases, and publish package artifacts. Require explicit user approval before any remote write, merge, tag, release, or registry publish.
Inputs to collect first
Before running the prompt, infer as many values as possible from the repositories and ask only for missing values, credentials, organization settings, or choices with multiple valid options.
Inputs to resolve:
| Input | Example |
|---|---|
| Organization | acme-org |
| API repository | acme-api |
| SDK repository | acme-sdk |
| API framework | FastAPI |
| API OpenAPI export command | mise run openapi |
| SDK language | Python |
| SDK generator | openapi-python-client |
| Generated SDK output path | generated/acme-api-client |
| Package name | acme-api-client |
| Import package name | acme_api_client |
| GitHub App name | acme-sdk-regenerator |
The prompt assumes the repositories already exist and the agent has local checkouts or can clone them.
Copy-paste implementation prompt
You are working in two GitHub repositories.
Before editing files, read the canonical recipe at:
https://bluebook.nightmode.dev/docs/api-to-sdk-regeneration
Treat that page as the source of truth for the implementation order,
workflow structure, verification process, and code snippets. Follow the
numbered steps on the page deterministically and adapt the snippets only
where repository names, package names, paths, or existing project
conventions require it.
First inspect both repositories and infer every input you can. Ask the
user only for missing values, credentials, organization settings, or
choices with multiple valid options before editing files.
- API repository: <ORG>/<API_REPO>
- SDK repository: <ORG>/<SDK_REPO>
Goal: implement a production-ready API-to-SDK regeneration workflow.
When a pull request merges to API main, API CI should validate the API,
verify OpenAPI export, dispatch SDK CI with only source metadata, and the
SDK repository should checkout the exact API commit, export OpenAPI,
regenerate the SDK, classify the OpenAPI change, create a Conventional
Commit for semantic-release, and open or update a bot-authored SDK pull
request.
Use this architecture:
1. API repo owns API tests and OpenAPI export.
2. SDK repo owns code generation, SDK tests, SemVer classification,
generated pull requests, semantic-release, and SDK releases.
3. The API workflow must not pass the whole OpenAPI schema as a dispatch
payload. It must pass only:
- source API repository
- source API commit SHA
- source API workflow run URL
4. The SDK workflow must checkout the API repository at the exact source
SHA and run the API OpenAPI export command itself.
5. Pull requests in the SDK repo must be created by `github-actions[bot]`
or `app/github-actions`, not by a personal token.
6. Use a GitHub App for cross-repository credentials.
7. Keep human PATs only as a temporary fallback during migration, then
remove them after app-token verification.
Repository assumptions:
- API default branch: main
- SDK default branch: main
- Tool runner: mise
- Python package manager: uv
- API OpenAPI export task: mise run openapi
- API quality gates:
- mise run sync
- mise run lint
- mise run test
- mise run openapi
- SDK quality gates:
- mise run sync
- mise run generate
- mise run lint
- mise run test
Implement the following in the API repo:
1. Ensure there is a deterministic OpenAPI export script and a mise task:
- task name: openapi
- output: openapi.json
- no development server required
2. Add `.github/workflows/update-sdk.yml`.
3. The API workflow must:
- run on push to main and workflow_dispatch
- checkout API
- install mise tools
- run API quality gates
- create a GitHub App installation token for the SDK repo using:
- repo variable: SDK_APP_ID
- repo secret: SDK_APP_PRIVATE_KEY
- dispatch the SDK workflow with repository_dispatch event type:
`openapi-schema-updated`
- send source_api_repo, source_api_sha, and source_api_run_url only
4. Do not commit generated OpenAPI to the API repo unless the existing
project already has that convention.
Implement the following in the SDK repo:
1. Ensure code generation is deterministic from checked-in or copied
`openapi.json`.
2. Add or update `.github/workflows/update-from-api.yml`.
3. The SDK workflow must:
- run on repository_dispatch event type `openapi-schema-updated`
- support workflow_dispatch with source_api_repo and source_api_sha
- checkout the SDK repo
- create a GitHub App installation token for the API repo using:
- repo variable: API_APP_ID
- repo secret: API_APP_PRIVATE_KEY
- checkout the API repo at source_api_sha
- run API quality gates and export OpenAPI from that commit
- copy the API-generated openapi.json into the SDK repo
- run SDK generation
- analyze OpenAPI changes and produce a Conventional Commit subject
- run SDK lint and tests
- push branch `bot/update-openapi-from-restapi`
- create or update an SDK PR with GitHub Actions token
- add labels `generated` and one of:
- semver-major
- semver-minor
- semver-patch
- semver-none
- include source API commit, source API workflow run, OpenAPI change
summary, SemVer impact, next SDK version, and checks run in the PR
body
4. Add `.github/workflows/ci.yml` for normal SDK lint and tests.
5. Add `python-semantic-release` configuration and
`.github/workflows/release.yml` that, on SDK main changes to the
generated SDK or openapi.json:
- reads Conventional Commits since the last `sdk-vX.Y.Z` tag
- stamps `generated/acme-api-client/pyproject.toml`
- updates `CHANGELOG.md`
- creates tag and GitHub release `sdk-vX.Y.Z`
- builds wheel and source distribution
- uploads distributions to the GitHub release
- publishes to PyPI only if PYPI_API_TOKEN is configured
6. Add tests that prove generated SDK model fields cover the OpenAPI
schema properties for at least one representative model.
7. Add unit tests for the OpenAPI change classification logic.
Versioning policy:
- major: removed path, operation, schema, property, response code, or
added required request property
- minor: added path, operation, schema, or optional property
- patch: metadata-only OpenAPI change
- none: no OpenAPI change
Security and release-management requirements:
- Prefer GitHub App tokens over personal access tokens.
- Use least-privilege app installation on only the API and SDK repos.
- Never print private keys, tokens, or generated installation tokens.
- Do not publish SDK artifacts from the API workflow.
- Release only from SDK main after the generated SDK PR is reviewed and
merged.
- Keep generator upgrades separate from API schema changes.
- Make every generated PR traceable to the source API commit.
Verification steps:
1. Run local format, lint, and tests in both repositories.
2. Validate workflow YAML syntax.
3. Push both repositories.
4. Configure GitHub App variables and secrets:
- API repo variable: SDK_APP_ID
- API repo secret: SDK_APP_PRIVATE_KEY
- SDK repo variable: API_APP_ID
- SDK repo secret: API_APP_PRIVATE_KEY
5. Trigger the API workflow manually once.
6. Confirm API workflow dispatches SDK workflow successfully.
7. Confirm SDK workflow checks out the API commit and succeeds.
8. Make a schema-visible API change in a PR, merge it to API main, and
confirm an SDK PR is created by GitHub Actions.
9. Confirm the SDK PR includes generated code, openapi.json, SemVer label,
next version, source commit link, and passing checks.
10. Merge the SDK PR and confirm semantic-release creates a version
commit, tag, changelog entry, GitHub release, and artifacts.
Do not stop at writing workflow files. Test the end-to-end flow against
GitHub and fix failures until the flow works.Expected repository settings
After the implementation, each repository should have these settings.
API repository
| Name | Type | Purpose |
|---|---|---|
SDK_APP_ID | variable | GitHub App ID used to dispatch SDK workflow. |
SDK_APP_PRIVATE_KEY | secret | Private key for SDK app token creation. |
SDK repository
| Name | Type | Purpose |
|---|---|---|
API_APP_ID | variable | GitHub App ID used to checkout API source. |
API_APP_PRIVATE_KEY | secret | Private key for API app token creation. |
PYPI_API_TOKEN | secret | Optional token for PyPI publishing. |
The organization must also allow GitHub Actions to create pull requests:
Organization settings → Actions → General → Workflow permissions →
Allow GitHub Actions to create and approve pull requestsWhat good output looks like
A successful generated SDK pull request should have:
- author:
app/github-actionsorgithub-actions[bot] - branch:
bot/update-openapi-from-restapi - title with next SDK version
- source API commit link
- source API workflow run link
- OpenAPI change summary
- SemVer impact
- generated code changes
openapi.jsonchanges- Conventional Commit subject matching the SemVer impact
- labels:
generatedandsemver-* - a passing regeneration check
A successful release should have:
- tag:
sdk-vX.Y.Z - semantic-release version commit
- wheel artifact
- source distribution artifact
- optional PyPI publication when configured
Common follow-up prompts
Use these if the first implementation is close but incomplete.
The SDK PR is authored by my user, not GitHub Actions. Refactor so the
SDK repository workflow creates the PR with `${{ github.token }}` and the
API repository only dispatches metadata.The API workflow currently passes the full OpenAPI JSON in the dispatch
payload. Refactor so it passes only repo, SHA, and run URL. The SDK
workflow should checkout the API commit and export OpenAPI itself.Add release management: SemVer classification, Conventional Commit PR
commits, python-semantic-release on merge, PR labels, SDK artifacts, and
checks that prove generated models match the OpenAPI schema.Replace the remaining personal access token with GitHub App credentials.
Use actions/create-github-app-token and remove PAT fallback after the
workflow is verified.Repository layout
The recipe assumes two repositories:
| Repository | Purpose |
|---|---|
acme-api | FastAPI or another REST API source repository. |
acme-sdk | Generated SDK source, tests, and release workflow. |
The names are examples. Replace them with the client or product names used in the engagement.
High-level flow
The API workflow does not send the OpenAPI file as a payload. It sends only:
- source API repository
- source API commit SHA
- source API workflow run URL
The SDK workflow recreates the OpenAPI file from the checked-out API commit. That makes the result reproducible and keeps dispatch payloads small.
Prerequisites
You need:
- GitHub organization administration access, or a partner who has it.
- A GitHub App installed on the API and SDK repositories.
- A way for the API to export an OpenAPI document in CI.
- A deterministic SDK generation command.
- Branch protection on the SDK repository, or at least review discipline around generated pull requests.
The examples below use:
misefor tool orchestrationuvfor Python dependency and package operationsopenapi-python-clientfor Python SDK generation- GitHub Actions for CI
- GitHub Releases as the default SDK release target
Implementation steps
Step 1: make OpenAPI export deterministic
The API repository should provide a command that exports the OpenAPI schema without running a development server.
For FastAPI, create a small script like this:
"""Export the API OpenAPI document to disk."""
import json
import sys
from pathlib import Path
from acme_api import create_app
def main() -> None:
"""Write the OpenAPI schema to disk."""
output_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(
"openapi.json",
)
app = create_app()
output_path.write_text(
json.dumps(app.openapi(), indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
if __name__ == "__main__":
main()Expose it through mise:
[tasks.openapi]
description = "Export the API OpenAPI document."
run = "uv run python scripts/export_openapi.py openapi.json"The command should be stable across runs. Avoid environment-specific servers, credentials, dynamic timestamps, or feature flags that change the schema without changing code.
Step 2: make SDK generation deterministic
The SDK repository should generate from a checked-in or copied
openapi.json file.
Example mise tasks:
[tasks.generate]
description = "Regenerate the Python SDK from openapi.json."
run = """
rm -rf generated/acme-api-client
uv run openapi-python-client generate \
--path openapi.json \
--config openapi-python-client.yaml \
--meta uv \
--output-path generated/acme-api-client \
--overwrite
"""
[tasks.lint]
description = "Run strict Ruff checks."
run = "uv run ruff check ."
[tasks.test]
description = "Run SDK tests."
run = "uv run pytest"Generated code should be treated as generated-only. Do not hand-edit files under the generated output directory. Put custom helpers outside that directory, or provide custom templates through the code generator.
Step 3: create a GitHub App
Create one GitHub App for the cross-repository automation.
Recommended settings:
| Setting | Value |
|---|---|
| Webhook | Inactive, unless the engagement needs webhooks. |
| Installation | Only the API and SDK repositories. |
| Contents | Read and write. |
| Pull requests | Read and write. |
| Actions | Read and write. |
| Commit statuses | Read and write. |
| Metadata | Read-only. |
Generate a private key and record the app ID.
Install the app on both repositories. Keeping the installation limited to only these repositories reduces blast radius if the private key is ever rotated after exposure.
Step 4: configure repository variables and secrets
In the API repository, store:
| Name | Type | Value |
|---|---|---|
SDK_APP_ID | variable | GitHub App ID. |
SDK_APP_PRIVATE_KEY | secret | App private key PEM. |
In the SDK repository, store:
| Name | Type | Value |
|---|---|---|
API_APP_ID | variable | GitHub App ID. |
API_APP_PRIVATE_KEY | secret | App private key PEM. |
The same GitHub App can be used for both directions if it is installed on both repositories with the permissions listed above.
Use repository secrets, not organization secrets, unless the same app is intended to automate multiple repository pairs.
Step 5: add the API dispatch workflow
Create .github/workflows/update-sdk.yml in the API repository.
name: Update generated SDK
on:
push:
branches:
- main
workflow_dispatch:
concurrency:
group: update-generated-sdk-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
env:
SDK_REPOSITORY: acme-org/acme-sdk
jobs:
update-sdk:
name: Dispatch API commit to SDK
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout REST API
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install mise tools
uses: jdx/mise-action@v2
with:
install: true
cache: true
- name: Verify API can export OpenAPI schema
run: |
mise trust .mise.toml
mise run sync
mise run lint
mise run test
mise run openapi
- name: Create SDK repository app token
id: sdk-app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.SDK_APP_ID }}
private-key: ${{ secrets.SDK_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: acme-sdk
- name: Dispatch SDK regeneration workflow
env:
GH_TOKEN: ${{ steps.sdk-app-token.outputs.token }}
run: |
gh api \
--method POST \
"repos/${SDK_REPOSITORY}/dispatches" \
-f event_type='openapi-schema-updated' \
-f client_payload[source_api_repo]="${GITHUB_REPOSITORY}" \
-f client_payload[source_api_sha]="${GITHUB_SHA}" \
-f client_payload[source_api_run_url]="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"This workflow is intentionally simple. It verifies that the API can export OpenAPI, then dispatches the SDK repository with enough information to reproduce the generated output.
The API workflow does not wait for the SDK workflow to finish. If the engagement requires a single red or green signal on the API merge, add a polling step that waits for the SDK workflow run. Most teams keep these signals separate: API main stays green when dispatch succeeds, while SDK main and SDK pull requests own SDK generation health.
Step 6: add the SDK regeneration workflow
Create .github/workflows/update-from-api.yml in the SDK repository.
name: Update from REST API schema
on:
repository_dispatch:
types:
- openapi-schema-updated
workflow_dispatch:
inputs:
source_api_repo:
required: true
default: acme-org/acme-api
source_api_sha:
required: true
source_api_run_url:
required: false
default: ""
concurrency:
group: update-from-api-schema
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
statuses: write
env:
API_WORKDIR: api-repo
SDK_UPDATE_BRANCH: bot/update-openapi-from-restapi
jobs:
regenerate-sdk:
name: Regenerate SDK and open PR
runs-on: ubuntu-latest
timeout-minutes: 15The workflow needs three phases:
- resolve dispatch payload
- checkout SDK and API repositories
- regenerate, classify, commit, and open the pull request
A production implementation should include these steps:
- name: Resolve dispatch payload
id: payload
run: |
if [ -n "${DISPATCH_SOURCE_API_REPO}" ]; then
source_api_repo="${DISPATCH_SOURCE_API_REPO}"
source_api_sha="${DISPATCH_SOURCE_API_SHA}"
source_api_run_url="${DISPATCH_SOURCE_API_RUN_URL}"
else
source_api_repo="${INPUT_SOURCE_API_REPO}"
source_api_sha="${INPUT_SOURCE_API_SHA}"
source_api_run_url="${INPUT_SOURCE_API_RUN_URL}"
fi
if [ -z "${source_api_repo}" ]; then
echo "Source API repo is required." >&2
exit 1
fi
if [ -z "${source_api_sha}" ]; then
echo "Source API SHA is required." >&2
exit 1
fi
{
echo "source_api_repo=${source_api_repo}"
echo "source_api_sha=${source_api_sha}"
echo "source_api_run_url=${source_api_run_url}"
} >> "${GITHUB_ENV}"
{
echo "source_api_repo=${source_api_repo}"
echo "source_api_sha=${source_api_sha}"
echo "source_api_run_url=${source_api_run_url}"
} >> "${GITHUB_OUTPUT}"
env:
DISPATCH_SOURCE_API_REPO: ${{ github.event.client_payload.source_api_repo }}
DISPATCH_SOURCE_API_SHA: ${{ github.event.client_payload.source_api_sha }}
DISPATCH_SOURCE_API_RUN_URL: ${{ github.event.client_payload.source_api_run_url }}
INPUT_SOURCE_API_REPO: ${{ inputs.source_api_repo }}
INPUT_SOURCE_API_SHA: ${{ inputs.source_api_sha }}
INPUT_SOURCE_API_RUN_URL: ${{ inputs.source_api_run_url }}Then checkout both repositories:
- name: Checkout SDK
uses: actions/checkout@v4
with:
fetch-depth: 0
path: sdk-repo
persist-credentials: false
- name: Create API repository app token
id: api-app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.API_APP_ID }}
private-key: ${{ secrets.API_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: acme-api
- name: Checkout REST API source commit
uses: actions/checkout@v4
with:
repository: ${{ steps.payload.outputs.source_api_repo }}
ref: ${{ steps.payload.outputs.source_api_sha }}
token: ${{ steps.api-app-token.outputs.token }}
path: ${{ env.API_WORKDIR }}
persist-credentials: falseExport the OpenAPI schema from the API checkout and regenerate the SDK:
- name: Export OpenAPI schema from API commit
working-directory: ${{ env.API_WORKDIR }}
run: |
mise trust .mise.toml
mise run sync
mise run lint
mise run test
mise run openapi
- name: Regenerate SDK and analyze OpenAPI change
id: openapi
working-directory: sdk-repo
run: |
cp ../${API_WORKDIR}/openapi.json openapi.json
mise trust .mise.toml
mise run sync
mise run generate
uv run python scripts/analyze_openapi_change.py
cat .openapi-change.env >> "${GITHUB_OUTPUT}"
mise run lint
mise run testCommit the generated output and publish a status on the bot branch:
- name: Commit and push SDK update branch
if: steps.changes.outputs.changed == 'true'
working-directory: sdk-repo
env:
GH_TOKEN: ${{ github.token }}
RELEASE_COMMIT_SUBJECT: ${{ steps.openapi.outputs.release_commit_subject }}
RELEASE_COMMIT_FOOTER: ${{ steps.openapi.outputs.release_commit_footer }}
run: |
gh auth setup-git
git config user.name "github-actions[bot]"
git config user.email \
"41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "${SDK_UPDATE_BRANCH}"
git add openapi.json generated
commit_args=(
-m "${RELEASE_COMMIT_SUBJECT}"
-m "Source REST API commit: https://github.com/${source_api_repo}/commit/${source_api_sha}"
)
if [ -n "${RELEASE_COMMIT_FOOTER}" ]; then
commit_args+=(-m "${RELEASE_COMMIT_FOOTER}")
fi
git commit "${commit_args[@]}"
bot_sha=$(git rev-parse HEAD)
git push --force-with-lease origin "${SDK_UPDATE_BRANCH}"
gh api \
--method POST \
"repos/${GITHUB_REPOSITORY}/statuses/${bot_sha}" \
-f state=success \
-f context='SDK regeneration checks' \
-f description='API export, SDK codegen, Ruff, and tests passed.' \
-f target_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"The explicit status is useful because pushes made with GITHUB_TOKEN do
not always trigger normal pull request CI. It gives branch protection and
reviewers a direct signal for the regeneration job that produced the
commit.
Finally, create or update the pull request:
- name: Create or update SDK pull request
if: steps.changes.outputs.changed == 'true'
working-directory: sdk-repo
env:
GH_TOKEN: ${{ github.token }}
SUMMARY_MARKDOWN: ${{ steps.openapi.outputs.summary_markdown }}
NEXT_VERSION: ${{ steps.openapi.outputs.next_version }}
run: |
title="chore(sdk): regenerate from REST API schema (${NEXT_VERSION})"
body_file=$(mktemp)
{
echo "Regenerated SDK from REST API main."
echo
echo "Source REST API commit:"
echo "https://github.com/${source_api_repo}/commit/${source_api_sha}"
echo
printf '%s\n' "${SUMMARY_MARKDOWN}"
} > "${body_file}"
existing_pr=$(gh pr list \
--head "${SDK_UPDATE_BRANCH}" \
--base main \
--state open \
--json number \
--jq '.[0].number // empty')
if [ -n "${existing_pr}" ]; then
gh pr edit "${existing_pr}" \
--title "${title}" \
--body-file "${body_file}"
pr_number="${existing_pr}"
else
pr_url=$(gh pr create \
--base main \
--head "${SDK_UPDATE_BRANCH}" \
--title "${title}" \
--body-file "${body_file}")
pr_number=$(gh pr view "${pr_url}" \
--json number \
--jq .number)
fi
gh pr edit "${pr_number}" \
--add-label "generated,${{ steps.openapi.outputs.pr_label }}"Keep the full workflow in source control rather than building it through the GitHub UI. The workflow itself is part of the release process and should be reviewed like application code.
Step 7: classify changes for semantic release
The SDK repository needs a small script that compares the new OpenAPI
schema with the schema on SDK main. The script should classify the
contract impact, but it should not own the final release commit. Let
python-semantic-release stamp the package version, changelog, tag, and
GitHub release after the generated SDK pull request merges.
Recommended default policy:
| OpenAPI change | SDK bump |
|---|---|
| Removed path, operation, schema, or property | major |
| Added required request property | major |
| Added path, operation, schema, or optional property | minor |
| Metadata-only change | patch |
| No schema change | none |
The script should:
- read
openapi.jsonfrom the working tree - read the previous
openapi.jsonfromHEAD - read the current generated package version from
HEAD - determine the SemVer impact
- write Markdown summary output for the pull request body
- write
semver-*label output for the workflow - write a Conventional Commit subject for
python-semantic-release - write a
BREAKING CHANGE:footer for major releases
For Python SDKs, the generated package version usually lives in:
generated/acme-api-client/pyproject.tomlKeep this logic simple at first. OpenAPI breaking-change detection can be
surprisingly deep, and a small deterministic policy is easier to trust
than a complex rule set nobody understands. If the client has stricter
contract requirements, replace this script with oasdiff, Optic, or an
approved API compatibility checker.
Map the analysis result to Conventional Commits:
| SemVer impact | Bot commit subject |
|---|---|
| major | feat(sdk)!: regenerate from REST API schema |
| minor | feat(sdk): regenerate from REST API schema |
| patch | fix(sdk): regenerate from REST API schema |
| none | chore(sdk): regenerate from REST API schema |
This keeps release semantics reviewable in the SDK pull request while letting release automation use the same commit history developers see.
Step 8: add SDK CI
The SDK repository should still have a normal pull request CI workflow. Even if the regeneration workflow already ran tests, regular CI helps catch manual changes, dependency updates, and workflow edits.
name: CI
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
jobs:
quality:
name: Lint and test
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout SDK
uses: actions/checkout@v4
- name: Install mise tools
uses: jdx/mise-action@v2
with:
install: true
cache: true
- name: Run quality gates
run: |
mise trust .mise.toml
mise run sync
mise run lint
mise run testBranch protection should require the relevant SDK checks before SDK main
can advance. If GitHub-token bot pushes do not trigger pull request CI,
require the explicit SDK regeneration checks status or switch bot branch
pushes to a GitHub App installation token that is allowed to trigger CI.
Step 9: connect SDK release automation
Release from the SDK repository after the generated SDK pull request is merged. Do not publish SDK artifacts from the API workflow.
For Python SDKs, use python-semantic-release as the release owner. The
OpenAPI analyzer should decide the commit semantics, and semantic-release
should turn those commits into a version, changelog, tag, GitHub release,
and package artifacts.
The SDK regeneration workflow should emit Conventional Commit subjects that match the OpenAPI impact:
| SemVer impact | Bot commit subject |
|---|---|
| major | feat(sdk)!: regenerate from REST API schema |
| minor | feat(sdk): regenerate from REST API schema |
| patch | fix(sdk): regenerate from REST API schema |
| none | chore(sdk): regenerate from REST API schema |
Use the release workflow and pyproject.toml configuration from
Python Package Versioning with Semantic Release.
Step 10: add schema-aware SDK tests
The SDK smoke test should verify more than importability. It should prove that generated models cover the current OpenAPI schema.
Example:
import json
from pathlib import Path
from attrs import fields
from acme_api_client.models import WidgetCreate
def test_widget_create_model_matches_openapi_schema() -> None:
openapi = json.loads(Path("openapi.json").read_text(encoding="utf-8"))
schema = openapi["components"]["schemas"]["WidgetCreate"]
generated_field_names = {field.name for field in fields(WidgetCreate)}
schema_property_names = set(schema["properties"])
assert schema_property_names <= generated_field_namesFor large SDKs, generate these assertions dynamically for all component schemas that map cleanly to model classes.
Operating model
A healthy operating model is:
- API team merges backend changes.
- SDK bot opens the generated SDK pull request.
- API and SDK owners review the SemVer impact.
- SDK pull request merges when checks are green.
- Semantic-release stamps the SDK version and changelog.
- SDK release workflow builds and publishes artifacts.
- Consumers upgrade SDK versions on their own cadence.
This keeps source ownership clear. API changes trigger SDK work, but SDK maintainers still own SDK releases. The API repository should not depend on the generated SDK. Application consumers, integration tests, CLIs, and external clients depend on the SDK.
SDK versioning and release management
Use SDK SemVer to communicate consumer impact, not API deployment impact.
The generated SDK pull request should show the proposed SemVer impact and
labels, but python-semantic-release should own the actual version bump
after the pull request merges to SDK main.
Recommended policy:
majorfor breaking SDK or API contract changesminorfor additive API surface changespatchfor generated metadata, docs, or non-contract changes
For the full release workflow, changelog setup, GitHub release publishing, PyPI publishing, and consumer upgrade automation, see Python Package Versioning with Semantic Release.
Troubleshooting
The PR is authored by a human
The pull request was created with a personal access token. Move PR
creation into the SDK repository workflow and use GH_TOKEN: ${{ github.token }}. The author should become github-actions[bot] or
app/github-actions.
GitHub Actions cannot create the PR
The organization may block Actions from creating pull requests. Enable:
Organization settings → Actions → General →
Workflow permissions → Allow GitHub Actions to create and approve pull
requestsThe SDK workflow cannot checkout the API repo
Check that the GitHub App is installed on the API repository and has
Contents: Read access. Confirm the SDK repository has API_APP_ID and
API_APP_PRIVATE_KEY configured.
The API workflow cannot dispatch the SDK workflow
Check that the GitHub App is installed on the SDK repository and has
Actions: Write access. Confirm the API repository has SDK_APP_ID and
SDK_APP_PRIVATE_KEY configured.
The SDK PR has no checks
Bot pushes made with GITHUB_TOKEN may not trigger pull request CI. Add a
commit status from the regeneration workflow, or push the bot branch with
a GitHub App token if the organization allows that event to trigger CI.
The version bump is wrong
Review the OpenAPI classification script and the generated Conventional Commit subject. If the client needs stricter contract checks, use a dedicated OpenAPI diff tool and map its output to SemVer labels and commit semantics.
The generated diff is huge
Separate API schema changes from generator upgrades. Pin generator versions in the SDK lockfile and upgrade the generator in its own pull request.
Definition of done
The recipe is complete when:
- API main dispatches SDK regeneration successfully.
- SDK regeneration checks out the exact API SHA.
- SDK regeneration opens a bot-authored pull request.
- The pull request includes OpenAPI and generated code changes.
- The pull request includes source API commit and workflow links.
- The pull request includes SemVer impact and next SDK version.
- The pull request has
generatedandsemver-*labels. - SDK tests pass against the generated client.
- Merging the pull request creates a release artifact.
- Human PAT secrets are removed after GitHub App verification.