-
Notifications
You must be signed in to change notification settings - Fork 60
docs: add version-bump playbook #2227
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
epipav
merged 4 commits into
feat/IN-1139-deprecation-sunset-headers
from
feat/IN-1140-version-bump-playbook
Sep 21, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,151 @@ | ||
| # Version-bump playbook | ||
|
|
||
| How to introduce `/v2` of an endpoint while `/v1` stays stable. The registry, routing, | ||
| per-version OpenAPI documents, and deprecation headers are already built; this playbook | ||
| is the procedure for using them. "v1 to v2" is used throughout, and the same steps | ||
| apply to any major bump. | ||
|
|
||
| ## When a bump is required | ||
|
|
||
| [Endpoint lifecycle](../site/lifecycle.md) promises callers that a version only ever | ||
| changes additively: new fields, new optional params, new endpoints, new error codes, | ||
| and expanded accepted enum input values. A change breaks that promise, and therefore | ||
| requires a new major version, when it matches the committed Breaking Change definition | ||
| in [CONTEXT.md](CONTEXT.md): | ||
|
|
||
| - removes or renames a field, param, endpoint, or enum value | ||
| - changes a field's type or format | ||
| - tightens a constraint (an optional param becomes required, a `sort` allow-list loses | ||
| a value) | ||
| - changes the meaning of an existing field or status code | ||
| - changes the error envelope shape | ||
| - changes the default or max `pageSize`, or the cursor encoding semantics | ||
| - changes an endpoint's default `sort` value | ||
|
|
||
| Additive changes ship inside `/v1` directly; stop here for those. | ||
|
|
||
| ## Step 1: implement v2 as a sparse version | ||
|
|
||
| Create `src/versions/v2/index.ts` exporting a `FastifyPluginAsyncTypebox`, mirroring | ||
| `src/versions/v1/index.ts`. Implement only the endpoints whose contract breaks. v2 is | ||
| sparse by design: callers use `/v2/foo` for the bumped endpoint and keep calling | ||
| `/v1/bar` for everything else, so a bump stays a per-endpoint decision instead of a | ||
| fork of the whole surface. | ||
|
|
||
| Keep queries and business logic shared upstream of both version plugins; isolate the | ||
| contract difference itself in a version-specific mapper, with the v2 handler importing | ||
| a `v2Mapper` next to the `v1Mapper` the v1 handler already uses. Duplicating logic | ||
| instead of sharing it upstream is how the two versions drift apart. | ||
|
|
||
| ## Step 2: register the prefix | ||
|
|
||
| Add the version to `versionRegistry` in `src/versions/registry.ts`: | ||
|
|
||
| ```ts | ||
| export const versionRegistry: ApiVersion[] = [ | ||
| { prefix: '/v1', plugin: v1Routes }, | ||
| { prefix: '/v2', plugin: v2Routes }, | ||
| ]; | ||
| ``` | ||
|
|
||
| The registry loop in `src/app.ts` gives the rest for free: routing under `/v2/`, a | ||
| `/v2/openapi.json` document with `info.version` `2.0.0` (see `specVersionFor`) | ||
| containing only v2 paths and only the schemas those paths reach, and the same for the | ||
| export script (`scripts/export-openapi.ts`). | ||
|
epipav marked this conversation as resolved.
|
||
|
|
||
| Adding the entry moves the pins in three suites, so update them in the same commit. | ||
| The registry guards that expect the prefix list `['/v1']` | ||
| (`tests/openapi-versions.test.ts` and `tests/version-lifecycle.test.ts`) now expect | ||
| `['/v1', '/v2']`. The 404 cases for an unregistered prefix in | ||
| `tests/version-routing.test.ts` and `tests/openapi-versions.test.ts` currently request | ||
| `/v2/openapi.json`; re-point them at a prefix that is still unregistered, `/v3` for | ||
| example, so they keep proving prefix isolation. | ||
|
|
||
| ## Step 3: prove v1 stable | ||
|
|
||
| The stability bar for the bump PR: registering `/v2` leaves every `/v1` response | ||
| byte-identical in status, body, and headers, and leaves `/v1/openapi.json` unchanged. | ||
| Routing isolation is pinned by `tests/version-routing.test.ts` and | ||
| `tests/openapi-versions.test.ts`; the PR must show the v1 route handlers and schemas | ||
| untouched, and any shared helper it edits needs the existing suites to stay green. | ||
|
|
||
| ## Step 4: signal the deprecation, later and deliberately | ||
|
|
||
| Registering v2 says nothing to v1 callers, on purpose. | ||
|
|
||
| Setting `lifecycle` on a registry entry is prefix-wide: the registry loop in | ||
| `src/app.ts` calls `applyLifecycle` once per version scope, so every `/v1` route picks | ||
| up the `Deprecation`, `Sunset`, and `Link` headers, including routes that never got a | ||
| `/v2` successor. Because v2 stays sparse by design (step 1), only deprecate `/v1` once | ||
| every supported `/v1` route has a `/v2` counterpart; stamping the whole prefix while v2 | ||
| covers a handful of endpoints tells callers of untouched routes to migrate to a version | ||
| that doesn't serve them. Deprecating one endpoint ahead of full v2 coverage needs | ||
| route-scoped lifecycle metadata, which doesn't exist yet; that gap is the same | ||
| follow-up noted below, under Retiring the old version. | ||
|
|
||
| Once the migration guide is published and v2 has a route for every supported v1 | ||
| endpoint, deprecate v1 by adding lifecycle metadata to its registry entry: | ||
|
|
||
| ```ts | ||
| { | ||
| prefix: '/v1', | ||
| plugin: v1Routes, | ||
| lifecycle: { | ||
| deprecatedAt: '2027-01-15', | ||
| sunsetAt: '2027-07-15', | ||
| successorPrefix: '/v2', | ||
| deprecationDocsUrl: 'https://api.insights.linuxfoundation.org/docs/migrate-v2', | ||
| }, | ||
| } | ||
| ``` | ||
|
|
||
| Setting `lifecycle` moves two pins in `tests/version-lifecycle.test.ts`, so update | ||
| them in the same commit. The registry guard asserts | ||
| `versionRegistry.every((entry) => entry.lifecycle === undefined)`; deprecating v1 | ||
| retires that assertion, so replace it with one that checks the `/v1` entry's actual | ||
| `lifecycle` values. The shipped-registry case `emits none of the three headers with | ||
| the shipped registry` asserts `/v1/openapi.json` answers without the lifecycle | ||
| headers; update it to expect the headers `/v1` now carries, moving it in with the | ||
| deprecated-version cases so the unstamped block keeps its meaning and the shipped | ||
| registry keeps real coverage. | ||
|
|
||
| Every `/v1` response then carries `Deprecation`, `Sunset`, and `Link` headers in the | ||
| [ADR-0021](adr/0021-rfc-deprecation-sunset-header-formats.md) wire formats. Headers | ||
| are additive: bodies and status codes stay exactly as they were, and | ||
| `tests/version-lifecycle.test.ts` pins that behavior. Invalid dates fail the build at | ||
| startup, so a bad `lifecycle` entry cannot reach production silently. | ||
|
|
||
| ## Step 5: communicate | ||
|
|
||
| - Add a [changelog](../site/changelog.md) entry announcing `/v2` and what changed. | ||
| - Publish the migration guide at the `deprecationDocsUrl` before setting `lifecycle`. | ||
| - State the sunset window in both. ADR-0003 gates the sunset itself: `/v1` is removed | ||
| only after `/v2` has been stable for a publicly announced minimum period, which the | ||
| ADR leaves TBD and calls at least six months conventional. Settle both numbers, the | ||
| caller notice window and that `/v2` stability period, and record them in | ||
| [Endpoint lifecycle](../site/lifecycle.md) before scheduling the deprecation. | ||
|
|
||
| ## Retiring the old version | ||
|
|
||
| Removing an entry from `versionRegistry` takes its whole prefix down at once, so only | ||
| retire `/v1` once `/v2` covers every route `/v1` still serves. ADR-0003 commits to | ||
| keeping a `/v1` endpoint alive, even as a stub, until `/v1` is fully sunset; pulling the | ||
| prefix while an unbumped endpoint has no `/v2` replacement breaks that promise. Once | ||
| coverage is complete and the sunset date passes, follow the pattern lifecycle.md | ||
| documents for alpha promotions: the retired routes answer `410 Gone` with a | ||
| `Link: </v2/...>; rel="successor-version"` header for a grace window, then the version | ||
| is removed from the registry. Retiring a single endpoint while `/v1` still serves | ||
| others needs the same route-scoped mechanism step 4 needs for deprecation; the | ||
| mechanics for that step are a follow-up, and nothing in this repo automates it yet. | ||
|
|
||
| ## Verification checklist | ||
|
|
||
| Before merging a bump PR, from `api/`: | ||
|
|
||
| 1. `pnpm test` green, in particular `tests/version-routing.test.ts` (prefix isolation), | ||
| `tests/openapi-versions.test.ts` (per-version specs), and | ||
| `tests/version-lifecycle.test.ts` (deprecation headers). | ||
| 2. `pnpm tsc-check` and `pnpm lint` green. | ||
| 3. Diff shows v1 handlers and schemas untouched (step 3's stability bar). | ||
| 4. `/v2/openapi.json` reviewed: only the bumped endpoints, `info.version` `2.0.0`. | ||
| 5. Changelog entry and migration guide linked (step 5). | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.