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 tomaintouchingapp/**): regenerates the spec, pushes it toinvoicepdfs-openapi, and dispatches every client repo.
OpenAPI 3.0 for generators. FastAPI emits OpenAPI 3.1, which OpenAPI Generator handles poorly — broken TypeScript,
typing.Anyin Python. The sync workflow down-converts toopenapi-3.0.json(via@apiture/openapi-down-convert) and the clients generate from that file.openapi.jsonstays 3.1 as the canonical spec. - Each client repo →generate.yml(workflow_dispatchorrepository_dispatch: spec-updated): fetches the spec, runs OpenAPI Generator, and commits the regenerated client. A.openapi-generator-ignoreprotectsREADME.mdand.github/.Generation replaces the tree. Each
generate.ymlbuilds into.gen/and then swaps the tree wholesale, so a file the generator stops emitting is actually removed. Generating in place and committing withgit add -Acannot delete anything — that is how three dead API classes stayed in the published packages for eleven regeneration cycles..gen/is seeded with.openapi-generator-ignorefirst, 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.pyenforces the conventions. -openapi.ymlhere still fails a PR if the committedopenapi.jsonis 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_TOKENas a per-repo secret in each repo that uses it (invoicepdfs+ everyinvoicepdfs-<lang>), andNPM_TOKENininvoicepdfs-node. Likewise, deployment environments aren't available for private repos on Free, sopublish.ymlomitsenvironment: 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¶
- Add
SDK_SYNC_TOKEN(above). - Actions → Sync SDK spec → Run workflow — pushes the spec and triggers all clients.
- 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.