You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Extends the version registry (api/src/versions/registry.ts) so an ApiVersion can carry optional lifecycle metadata: deprecatedAt, sunsetAt, successorPrefix, deprecationDocsUrl. The shipped /v1 entry carries none, so production behavior is unchanged.
New api/src/versions/lifecycle.ts validates the metadata at build time (unparseable dates and a sunset earlier than the deprecation reject buildApp) and precomputes the header values once. A scoped onSend hook stamps every response of a deprecated version — success and error replies alike, the version's openapi.json route included — with:
Sunset: <IMF-fixdate> (RFC 8594), when a sunset date is planned
Link with rel="successor-version" and/or rel="deprecation", when configured
The registry loop in app.ts now registers each version inside its own scope; the per-version openapi.json route moved inside that scope (public URL unchanged) so it inherits version-scoped behavior.
Docs: new "Deprecation signals" section in docs/site/lifecycle.md for callers; ADR 0003's deprecation-process step updated to the RFC 9745/RFC 8594 header formats with an amendment note (the ADR predates RFC 9745).
Groundwork from IN-1137's registry typing; the promotion-time 410 Gone flow stays with the version-bumping playbook (IN-1140).
Test plan
cd api && pnpm test — 85/85 green, including new tests/version-lifecycle.test.ts (18 tests: header formats, error responses, build-time validation, live versions staying unstamped)
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Date validation currently accepts timestamps and impossible calendar dates despite requiring strict YYYY-MM-DD values.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
api/src/versions/lifecycle.ts:9
Date.parse accepts full timestamps and normalizes impossible dates (2026-02-30 becomes March 2), so invalid registry metadata can emit headers for a different instant. Enforce exact YYYY-MM-DD syntax and calendar validity.
const ms = Date.parse(value);
if (Number.isNaN(ms)) {
throw new Error(`lifecycle ${field} must be a valid YYYY-MM-DD date: "${value}"`);
The reason will be displayed to describe this comment to others. Learn more.
Verified /v1 stays fully unstamped, the openapi.json route keeps its exact pre-PR URL through the scope move, and the Sunset header is genuine IMF-fixdate rather than an ISO date. Build-time validation runs before buildApp() resolves, confirmed by the reject-on-bad-config tests. Nice work here, approving.
ADR-0003 was added on 2026-09-08, but RFC 9745 was published in March 2025, so “after ADR-0003 was written” reverses the chronology. Remove that claim and describe the older syntax without the incorrect timeline.
sunsetAt, successorPrefix, and deprecationDocsUrl are optional, so deprecated versions may emit only Deprecation. Qualify Sunset and Link as optional; otherwise callers are told to expect headers that the implementation deliberately omits.
The reason will be displayed to describe this comment to others. Learn more.
this takes over the root 404 for the whole app, not just the deprecated scopes - /v1/does-not-exist and /docsomething go through it now too. the body is byte-identical to fastify's default, but I think the default calls .type('application/json') while this one falls through to fastify's object default, application/json; charset=utf-8, so the content-type shifts on every 404 in the API. tests/docs-static-serving.test.ts:76 is also still named "returns Fastify's own default 404" and only matches /application\/json/, so it wouldn't catch that. is owning the root 404 the intent here, or would keeping it in the version scopes be enough?
The reason will be displayed to describe this comment to others. Learn more.
yes, owning root is the intent, it keeps root and the deprecated scopes on one body. fastify 5.12's basic404 never calls .type(), so both already send application/json; charset=utf-8 -- 461c9bab renames the stale test and pins that exact value
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
The successor-version link currently targets a version prefix instead of the corresponding successor resource.
Review effort: Balanced Findings: None
Previously missed (1)
In code that hasn't changed since last review
Build successor-version links from the request URL
api/src/versions/lifecycle.ts:40
successor-version identifies a successor of the current resource, but this sends the same bare prefix for every endpoint (for example, /v1/projects/x?cursor=y points to /v2, which may itself 404). Build this relation from the request URL by replacing the current prefix and preserving the path/query; the promotion example in version-bump-playbook.md:173-174 already uses that pattern. Update the corresponding tests and lifecycle docs as well.
re copilot's successor-version note on lifecycle.ts:40: the prefix target is on purpose, v2 can rename routes or change cursor encoding, so rewriting /v1/x?cursor=y to /v2/x?cursor=y could point at nothing. The request.url rewrite only fits alpha promotion, where the path stays the same
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
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
api/src/versions/registry.ts) so anApiVersioncan carry optional lifecycle metadata:deprecatedAt,sunsetAt,successorPrefix,deprecationDocsUrl. The shipped/v1entry carries none, so production behavior is unchanged.api/src/versions/lifecycle.tsvalidates the metadata at build time (unparseable dates and a sunset earlier than the deprecation rejectbuildApp) and precomputes the header values once. A scopedonSendhook stamps every response of a deprecated version — success and error replies alike, the version'sopenapi.jsonroute included — with:Deprecation: @<unix-timestamp>(RFC 9745)Sunset: <IMF-fixdate>(RFC 8594), when a sunset date is plannedLinkwithrel="successor-version"and/orrel="deprecation", when configuredapp.tsnow registers each version inside its own scope; the per-versionopenapi.jsonroute moved inside that scope (public URL unchanged) so it inherits version-scoped behavior.docs/site/lifecycle.mdfor callers; ADR 0003's deprecation-process step updated to the RFC 9745/RFC 8594 header formats with an amendment note (the ADR predates RFC 9745).Groundwork from IN-1137's registry typing; the promotion-time
410 Goneflow stays with the version-bumping playbook (IN-1140).Test plan
cd api && pnpm test— 85/85 green, including newtests/version-lifecycle.test.ts(18 tests: header formats, error responses, build-time validation, live versions staying unstamped)pnpm tsc-checkandpnpm lintcleanJira: IN-1139
main