Skip to content

SDK generation & publishing

InvoicePDFs is API-first. The OpenAPI spec is the source of truth, and each language client lives in its own repo, generated from the spec.

Repos

Repo Role
invoicepdfs (this repo) The API. Owns openapi.json; drives the sync.
invoicepdfs-openapi Public spec (openapi.json + openapi.yaml). Source of truth for clients.
invoicepdfs-node · -python · -go · -java · -dotnet · -php · -ruby One generated client each.

Flow

invoicepdfs (API)
   │  Sync SDK spec workflow
   │  1. make openapi  → openapi.json
   │  2. push spec → invoicepdfs-openapi
   │  3. repository_dispatch → each invoicepdfs-<lang>
invoicepdfs-openapi (spec)
invoicepdfs-<lang>  ── Generate SDK workflow ──►  fetch spec, run OpenAPI
                                                   Generator, commit client
  • This repo → sync-sdks.yml (workflow_dispatch, or push to main touching app/**): regenerates the spec, pushes it to invoicepdfs-openapi, and dispatches every client repo.

OpenAPI 3.0 for generators. FastAPI emits OpenAPI 3.1, which OpenAPI Generator handles poorly — broken TypeScript, typing.Any in Python. The sync workflow down-converts to openapi-3.0.json (via @apiture/openapi-down-convert) and the clients generate from that file. openapi.json stays 3.1 as the canonical spec. - Each client repo → generate.yml (workflow_dispatch or repository_dispatch: spec-updated): fetches the spec, runs OpenAPI Generator, and commits the regenerated client. A .openapi-generator-ignore protects README.md and .github/.

Generation replaces the tree. Each generate.yml builds into .gen/ and then swaps the tree wholesale, so a file the generator stops emitting is actually removed. Generating in place and committing with git add -A cannot delete anything — that is how three dead API classes stayed in the published packages for eleven regeneration cycles. .gen/ is seeded with .openapi-generator-ignore first, because the generator reads that file from its output directory.

Method names come from the route handler's function name. FastAPI is configured so an operationId is route.name, and OpenAPI Generator maps each operationId straight to a method name — so the SDK surface is decided by how handlers are named in this repo, not in the client repos. Renaming a handler renames a method in every published SDK; treat it as a breaking change once they ship. tests/test_operation_ids.py enforces the conventions. - openapi.yml here still fails a PR if the committed openapi.json is stale.

Required secret — SDK_SYNC_TOKEN

Cross-repo automation needs a token (workflow GITHUB_TOKEN only reaches its own repo). Create a fine-grained PAT (or GitHub App) with, on the org's invoicepdfs-* repos:

  • Contents: read & write (push spec to invoicepdfs-openapi; commit clients)
  • Metadata: read

Used by sync-sdks.yml (push + dispatch) and each generate.yml (fetch the private spec).

Free org + private repos. Organization Actions secrets only reach public repos on GitHub Free — so add SDK_SYNC_TOKEN as a per-repo secret in each repo that uses it (invoicepdfs + every invoicepdfs-<lang>), and NPM_TOKEN in invoicepdfs-node. Likewise, deployment environments aren't available for private repos on Free, so publish.yml omits environment: pypi (configure the PyPI pending publisher with a blank Environment field). Making the SDK repos public removes both limits (org secrets + environments work, and Actions minutes are unlimited) — they're open-source clients anyway.

Running it

  1. Add SDK_SYNC_TOKEN (above).
  2. Actions → Sync SDK specRun workflow — pushes the spec and triggers all clients.
  3. Or run an individual client's Generate SDK workflow directly.

Versioning

Tag a release vX.Y.Z; publish steps (added per client) strip the leading v for registries that require PEP 440 / SemVer.

Naming — invoicepdfs on PyPI/npm

The client repos target the bare name (invoicepdfs on PyPI, npm, RubyGems…). This repo's pyproject.toml currently names the server app invoicepdfs — if you also publish the server, rename it (e.g. invoicepdfs-server) so the client SDK owns the public name.

Publishing (next step)

The client repos regenerate today; publishing to each registry is a per-repo job to add as you provision credentials:

Language Registry Mechanism Secret
Python PyPI Trusted Publishing (OIDC) — (pending publisher)
Node npm npm publish NPM_TOKEN
.NET NuGet dotnet nuget push NUGET_API_KEY
Ruby RubyGems gem push RUBYGEMS_API_KEY
Java Maven Central Gradle/Maven + GPG Sonatype + signing key
Go pkg.go.dev git tag (this repo is the module)
PHP Packagist webhook from this repo

For idiomatic, production-grade SDKs, a managed generator (Speakeasy, Stainless, Fern) can replace OpenAPI Generator and publish from the same spec.