ci: snapshot OpenAPI spec for cross-stack contract testing#202
Merged
Conversation
The bug class: today's prod login broke for ~24h because /auth/exchange
(AUTH-004) shipped server-side without the dashboard's client being
updated. Per-repo unit tests on both sides stayed green because each repo
only saw its own half of the contract.
This PR makes the contract a committed artifact (openapi.snapshot.json)
so the dashboard + instanode-web repos can generate typed clients from it
deterministically. When the api spec changes, this CI gate fails the PR
until the snapshot is regenerated — surfacing the contract change so the
engineer can also update the client repos.
What ships:
- handlers.OpenAPISpecProduction() — exported accessor for the
production-rendered spec (with the dev-only /internal/set-tier path
stripped). Source of truth for downstream snapshotting.
- cmd/openapi-snapshot/ — small Go tool that canonicalises the spec
(sorted keys, 2-space indent) so whitespace-only edits to the const
do not flip the snapshot.
- openapi.snapshot.json — the committed snapshot (9931 lines, 384KB).
- make openapi-snapshot / openapi-snapshot-check — local regen +
drift check.
- .github/workflows/openapi-snapshot.yml — fast (<30s) PR gate that
fails with a clear "run make openapi-snapshot" message when the
committed file drifts from a fresh regeneration. Emits a structured
log line per run (rule 25 observability) so we can track contract
drift rate over time.
Coverage block (rule 17):
Symptom: Contract drift between api spec and TS clients goes
undetected at PR time → runtime login break.
Enumeration: rg -F 'openAPISpec' --include='*.go'
Sites found: Single const + ServeOpenAPI handler in
internal/handlers/openapi.go.
Sites touched: Added OpenAPISpecProduction() accessor next to
ServeOpenAPI; both go through the same
stripInternalSetTierPath path so what the snapshot
emits == what production serves.
Coverage test: make openapi-snapshot-check (CI job
snapshot-drift) regenerates and diffs on every PR
touching openapi.go / cmd/openapi-snapshot/ /
openapi.snapshot.json.
Live verified: make openapi-snapshot wrote 383616 bytes;
make openapi-snapshot-check returns "snapshot
matches handlers.OpenAPISpecProduction()". The
/internal/set-tier dev-only path is correctly
stripped (grep -c returns 0).
Follow-ups (separate PRs, scoped):
- dashboard PR: openapi-typescript dev dep + npm run generate:api-types
fetches this snapshot from origin/master and regenerates
src/lib/api.generated.ts; CI diffs the regenerated file vs committed.
Plus a Playwright AUTH-004 smoke test (POST /auth/email/start, OPTIONS
/auth/exchange CORS preflight, POST /auth/exchange with credentials).
- instanode-web PR: same pattern.
- infra PR: NR dashboard tile + alert on cross_stack_contract_drift
CI log events (rule 25).
Note: /auth/exchange is currently NOT in the OpenAPI spec — that gap
is the proximate cause of today's outage. This PR builds the
infrastructure to catch the next gap; adding /auth/exchange to the
spec is a separate fix that should follow this PR (it will trigger
this gate and force the regeneration discipline from day one).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ew branches Splits main() into a thin os.Exit wrapper around run(args, stdout, stderr), the idiomatic Go pattern for testable mains. Adds 4 new tests covering stdout mode, file mode, bad-flag path, and unwritable -out path (Linux-only via /dev/full, skipped elsewhere). Removes the unreachable encode-error fallback in canonicalise (re-encoding a valid-JSON-derived value cannot fail) per Go's defensive-coding guidelines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 30, 2026
…0 outage (#203) * docs(openapi): add /auth/exchange — closes contract gap that hid 2026-05-30 outage The 2026-05-30 prod-login outage chained 3 failures along /auth/exchange (client missing, preflight rejected, ACAC missing). Even with the new contract-CI gate (api #202), the bug class wasn't catchable because /auth/exchange was literally NOT in the OpenAPI spec — agents and the typed-client codegen pipeline had no contract to enforce. Adds the path with the full CORS contract documented in the description so any future regression to the headers / preflight rules is visible in the diff that breaks the snapshot-drift gate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(openapi): drop redeploy_requires_name from DeployRequest description That error code was emitted by an unreachable defence-in-depth arm that landed in api#201 and was removed before merge (requireName already rejects empty/whitespace name upstream of the redeploy branch). The DeployRequest description still mentioned it; tidied + clarified the upstream-rejection path. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
openapi.snapshot.jsonas the canonical, committed OpenAPI 3.1 contract — generated fromhandlers.OpenAPISpecProduction()(same spec served atGET /openapi.jsonin production, with the dev-only/internal/set-tierpath stripped).cmd/openapi-snapshot/+make openapi-snapshot/make openapi-snapshot-checkfor local regen + drift detection..github/workflows/openapi-snapshot.yml) that fails the PR with a clear "runmake openapi-snapshot" message wheninternal/handlers/openapi.gochanges but the snapshot is not regenerated.Why
Today's prod login broke for ~24h because
/auth/exchange(AUTH-004) shipped server-side without the dashboard's client being updated. Per-repo unit tests on both sides stayed green because each repo only saw its own half of the contract.This PR makes the contract a committed artifact so the dashboard + instanode-web repos can deterministically generate typed clients via
openapi-typescript. Future PRs (separately scoped) in those repos will:origin/masterofapi.src/lib/api.generated.ts.This is rule 22 (contract surface checklist) made enforceable at PR time.
Note on
/auth/exchange: it is currently not in the OpenAPI spec — that gap is the proximate cause of today's outage. This PR builds the infrastructure to catch the next gap; adding/auth/exchangeto the spec is a separate fix that should follow this PR (it will trigger this gate and force the regeneration discipline from day one).Coverage block (rule 17)
Test plan
snapshot-driftjob passes (snapshot is consistent with the const).build-and-teststill green.make openapi-snapshot && git diff --exit-code openapi.snapshot.json→ no drift.openapi.go, runmake openapi-snapshot-check→ fails with the "out of date" message; runmake openapi-snapshot→ passes.cmd/openapi-snapshot/is excluded from existing test gates (no test files;go vetclean).Follow-ups (separately scoped PRs)
openapi-typescriptdev dep +npm run generate:api-types(fetches this snapshot viagh apifromorigin/master) + CI diff job. Plus a Playwright AUTH-004 smoke test againstE2E_API_URL.cross_stack_contract_driftlog events emitted by this workflow (rule 25 observability).🤖 Generated with Claude Code