Skip to content

ci: snapshot OpenAPI spec for cross-stack contract testing#202

Merged
mastermanas805 merged 11 commits into
masterfrom
ci/cross-stack-contract-test
May 30, 2026
Merged

ci: snapshot OpenAPI spec for cross-stack contract testing#202
mastermanas805 merged 11 commits into
masterfrom
ci/cross-stack-contract-test

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Adds openapi.snapshot.json as the canonical, committed OpenAPI 3.1 contract — generated from handlers.OpenAPISpecProduction() (same spec served at GET /openapi.json in production, with the dev-only /internal/set-tier path stripped).
  • Adds cmd/openapi-snapshot/ + make openapi-snapshot / make openapi-snapshot-check for local regen + drift detection.
  • Adds a fast (<30s) PR-time CI gate (.github/workflows/openapi-snapshot.yml) that fails the PR with a clear "run make openapi-snapshot" message when internal/handlers/openapi.go changes 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:

  1. Pull this snapshot from origin/master of api.
  2. Regenerate src/lib/api.generated.ts.
  3. CI-diff the regenerated file vs the committed one → fail if the api contract changed without a corresponding client regen.

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/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).

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 route through stripInternalSetTierPath
                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()". /internal/set-tier
                correctly stripped (grep -c returns 0). go build ./...
                + go vet ./... clean. handlers OpenAPI test suite green.

Test plan

  • CI: snapshot-drift job passes (snapshot is consistent with the const).
  • CI: build-and-test still green.
  • Local: make openapi-snapshot && git diff --exit-code openapi.snapshot.json → no drift.
  • Local: edit openapi.go, run make openapi-snapshot-check → fails with the "out of date" message; run make openapi-snapshot → passes.
  • Verify cmd/openapi-snapshot/ is excluded from existing test gates (no test files; go vet clean).

Follow-ups (separately scoped PRs)

  • dashboard: add openapi-typescript dev dep + npm run generate:api-types (fetches this snapshot via gh api from origin/master) + CI diff job. Plus a Playwright AUTH-004 smoke test against E2E_API_URL.
  • instanode-web: same pattern.
  • infra (optional): NR dashboard tile + alert on cross_stack_contract_drift log events emitted by this workflow (rule 25 observability).

🤖 Generated with Claude Code

mastermanas805 and others added 11 commits May 30, 2026 17:21
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
mastermanas805 merged commit 8db52f7 into master May 30, 2026
15 checks passed
@mastermanas805
mastermanas805 deleted the ci/cross-stack-contract-test branch May 30, 2026 13:36
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant