feat(api): OpenAPI→TS codegen + contract-drift gate (Wave 1)#191
Merged
Conversation
Wave 1 contract-drift gate — docs/ci/01-CI-INTEGRATION-DESIGN.md. The UI↔API wire contract was hand-mirrored in src/api/types.ts; a backend field rename passed tsc+vitest here and broke prod at runtime (the login- break class). This derives the wire types from the api's OpenAPI snapshot so the same rename now fails tsc in `npm run gate` at PR time. - Add openapi-typescript devDep + `gen:api-types` script → src/api/generated.ts generated from a committed openapi.snapshot.json (byte-identical copy synced from the api repo; committed for CI determinism over fetching prod). npm `overrides` satisfies openapi-typescript@7's typescript@^5 peer with the repo's TS 6 — avoids --legacy-peer-deps (which drops @testing-library/dom). - Derive the highest-value wire shapes (the ones that broke before) from generated.ts and consume them in index.ts: WireAuthMe (fetchMe), WireResourceItem/ResourceListResponse (listResources/getResource/adaptResource), WireBillingState (fetchBilling/mapBillingState), WireDeployItem (listDeployments/getDeployment/adaptDeployment). TODO in types.ts lists the remaining hand-typed wire types + gate blind spots. - gen:api-types:check (scripts/check-api-types.mjs) fails CI if generated.ts is stale vs the snapshot; wired into ci.yml. prebuild regenerates it. - Exclude generated.ts from coverage (type-only codegen artifact). - Proven: renaming ResourceItem.storage_bytes in the snapshot + regenerating fails tsc at adaptResource (src/api/index.ts:669). gate green; patch cov 100%. Latent gaps surfaced (see report/types.ts TODO): DeployItem.failure has no exit_code on the wire though the UI reads it; BillingStateResponse lacks razorpay_configured in the spec though the UI consumes it; /api/v1/capabilities has no web consumer so it can't be tsc-gated. Cross-ref: api PR adds the oasdiff breaking-change gate (producer side). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
size-limit report 📦
|
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.
Wave 1 contract-drift gate —
docs/ci/01-CI-INTEGRATION-DESIGN.md(HIGHEST ROI).Consumer-side half of the UI↔API contract-drift gate. Producer-side half (oasdiff breaking-change gate): InstaNode-dev/api#264.
Problem
The UI↔API wire contract was hand-mirrored in
src/api/types.ts. A backend field rename passedtsc+vitesthere (the web mirrored the old shape by hand) and broke prod at runtime — the login-break class.What
gen:api-types→src/api/generated.ts, generated from a committedopenapi.snapshot.json(byte-identical copy of the api repo's; committed for CI determinism rather than fetching prod at gen time).overridessatisfies openapi-typescript@7'stypescript@^5peer with the repo's TS 6 — avoids--legacy-peer-deps(which drops the auto-installed@testing-library/dompeer and breaks the test type surface).npm cireproduces the correct tree.generated.tsand consume them inindex.ts(this is what makes a rename failtsc):WireAuthMe→fetchMe(the field that broke login)WireResourceItem/WireResourceListResponse→listResources/getResource/adaptResourceWireBillingState→fetchBilling/mapBillingStateWireDeployItem→listDeployments/getDeployment/adaptDeploymenttypes.tslists remaining hand-typed wire types + gate blind spots.gen:api-types:check(scripts/check-api-types.mjs) fails CI ifgenerated.tsis stale vs the snapshot; wired intoci.yml.prebuildregenerates it.generated.tsexcluded from coverage (type-only codegen artifact).The proof that the gate works
Renaming
ResourceItem.storage_bytes→storageBytesin the snapshot +npm run gen:api-typesmakestscfail at the consumer:gen:api-types:checkalso reds on a stalegenerated.ts. Reverted after proving.Verify
npm run gategreen (tsc + build + prerender + vitest: 1129 passed / 3 skipped).gen:api-types:checkpasses; snapshot byte-identical to api repo's.listResourcestest exercising the new fallbacks.generated.tsis type-only →npm run buildoutput functionally identical (no new runtime).Latent drift bugs surfaced (worth fixing in follow-up)
DeployItem.failurehas noexit_codeon the wire, but the UI'sadaptFailurereads it (alwaysundefined→nullat runtime). Reconcile: addexit_codetoDeployItem, or read it only from the events surface (rule 27).BillingStateResponselacksrazorpay_configuredin the spec though the UI consumes it (fail-closed default preserved via local intersection)./api/v1/capabilities(CapabilitiesResponse/TierCapabilities) has no web consumer (static PricingGrid), so it can't be tsc-gated until a consumer exists.🤖 Generated with Claude Code