Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ with its own release cadence. Each client release advertises which protocol
versions it supports via a generated `SUPPORTED_PROTOCOL_VERSIONS` constant
and a checked-in `clients/<lang>/release-metadata.json`.

For a coordinated release, use **one PR with two commits**:

1. Prepare and validate the release manifests, changelogs, and generated files
at `X.Y.Z`. Record this release commit's SHA; all seven tags point here.
2. Advance `PROTOCOL_VERSION` to the next development version and promote
`X.Y.Z` in `STABLE_DOC_VERSIONS` **in the same commit and PR**. Keep the
released compatibility baselines unchanged unless intentionally revising
the compatibility policy.

Create the bare `vX.Y.Z` tag locally at the first commit before validating the
second commit with `npm test` and `npm run docs:build`. After all required CI
checks and collaborator approvals pass, push each release tag separately,
verify the remote tags point at the release SHA, then merge the PR with a merge
commit. **Do not enable auto-merge before pushing the tags.** Tag pushes start
publishing before the PR merges; this ordering ensures the tag needed by the
documentation build already exists when the merge triggers deployment.

See [Updating published documentation](#updating-published-documentation) and
the [release skill](skills/release-ahp/SKILL.md) for the full coordinated flow.
Do not defer documentation promotion to another PR.

## Changelog fragments

Normal feature/fix PRs do **not** edit `CHANGELOG.md` files directly. They add
Expand Down Expand Up @@ -77,6 +98,10 @@ want to validate fragment JSON without consuming it.

## Per-client release flow

These recipes describe individual artifact releases. For coordinated releases,
use the two-commit PR and pre-merge tag order above instead of each recipe's
merge-then-tag order.

### Rust (`rust/vX.Y.Z`)

1. Bump `[workspace.package].version` in `clients/rust/Cargo.toml` (this
Expand Down Expand Up @@ -212,9 +237,27 @@ trigger started the run.

## Updating published documentation

After pushing the release's `vX.Y.Z` tag, prepend `X.Y.Z` to
`STABLE_DOC_VERSIONS` in `docs/.vitepress/versions.ts` and merge that change
to `main` so the new release becomes the default documentation version.
In the **same commit that bumps the next development version**, prepend the
released `X.Y.Z` to `STABLE_DOC_VERSIONS` in `docs/.vitepress/versions.ts`,
preserving older entries. For example, releasing `1.1.0` and starting `1.2.0`
sets `PROTOCOL_VERSION = '1.2.0'` and
`STABLE_DOC_VERSIONS = ['1.1.0', '1.0.0']` in the same PR.

The homepage's specification label and the default docs version both use the
first stable entry; the draft label follows `PROTOCOL_VERSION` separately.
Pushing release tags alone does not promote the website.

The docs builder materializes stable pages from bare `vX.Y.Z` tags. Create the
new tag locally at the release commit for `npm run docs:build`, then push it
with the other release tags after CI and review approval but **before merging**
the combined PR. Never tag the development-bump commit or merge while the
required remote tag is missing.

After merge, confirm `Deploy Documentation` succeeds and check the live
homepage label, default docs, and version selector: `X.Y.Z` must be the default,
older releases must remain available, and only the next development version
must be labelled `(draft)`. The release is not complete until this deployment
is verified; a passing local build is not sufficient.

## What CI guards against

Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* matching `v<version>` git tag; `npm run docs:versions` materializes it
* into `docs/versions/<version>/`.
*/
export const STABLE_DOC_VERSIONS: readonly string[] = ['1.0.0']
export const STABLE_DOC_VERSIONS: readonly string[] = ['1.1.0', '1.0.0']

/** Top-level docs sections that are versioned. */
export const VERSIONED_SECTIONS = ['guide', 'specification', 'reference'] as const
Expand Down
8 changes: 5 additions & 3 deletions scripts/build-doc-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import os from 'os';
import path from 'path';
import { pipeline } from 'stream/promises';
import { fileURLToPath } from 'url';
import { readDocSidebarSource } from './read-doc-sidebar.js';
import {
STABLE_DOC_VERSIONS,
VERSIONED_SECTIONS,
Expand Down Expand Up @@ -76,11 +77,12 @@ async function materialize(version: string): Promise<void> {
});

const sidebarFile = path.join(directory, 'sidebar.json');
const sidebarSource = readDocSidebarSource(
fs.readFileSync(path.join(directory, 'docs/.vitepress/config.mts'), 'utf8'),
);
execFileSync('npx', ['tsx', '--eval', `
import { writeFileSync } from 'node:fs';
import('./docs/.vitepress/config.mts').then(({ default: config }) => {
writeFileSync(process.env.AHP_DOCS_SIDEBAR, JSON.stringify(config.themeConfig.sidebar));
});
writeFileSync(process.env.AHP_DOCS_SIDEBAR, JSON.stringify(${sidebarSource}));
`], {
cwd: directory,
stdio: 'inherit',
Expand Down
30 changes: 30 additions & 0 deletions scripts/read-doc-sidebar.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { readDocSidebarSource } from './read-doc-sidebar.js';

test('reads the plain sidebar used by older documentation releases', () => {
const sidebar = "{ '/guide/': [{ text: 'Guide', link: '/guide/start' }] }";
assert.equal(readDocSidebarSource(
`export default defineConfig({ themeConfig: { sidebar: ${sidebar} } });`,
), sidebar);
});

test('reads the release sidebar without importing versioned documentation', () => {
const sidebar = "{ '/guide/': [{ text: 'New page', link: '/guide/new' }] }";
assert.equal(readDocSidebarSource(`
import { createVersionedSidebar } from './missing-doc-versions';
export default withMermaid(defineConfig({
themeConfig: { sidebar: createVersionedSidebar(${sidebar}) },
}));
`), sidebar);
});

test('rejects missing and unsupported sidebars explicitly', () => {
for (const config of [
'export default {};',
'export default { themeConfig: { sidebar: loadSidebar() } };',
'export default { themeConfig: { sidebar: createVersionedSidebar() } };',
]) {
assert.throws(() => readDocSidebarSource(config), /Expected themeConfig.sidebar/);
}
});
22 changes: 22 additions & 0 deletions scripts/read-doc-sidebar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Node, Project, SyntaxKind } from 'ts-morph';

/** Read the release's own sidebar without loading its versioned site config. */
export function readDocSidebarSource(config: string): string {
const project = new Project({ useInMemoryFileSystem: true });
const source = project.createSourceFile('config.mts', config);
const theme = source.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
.find((property) => property.getName() === 'themeConfig')
?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
const property = theme?.getProperty('sidebar');
const initializer = property && Node.isPropertyAssignment(property)
? property.getInitializer()
: undefined;
const sidebar = initializer && Node.isCallExpression(initializer)
&& initializer.getExpression().getText() === 'createVersionedSidebar'
? initializer.getArguments()[0]
: initializer;
if (!sidebar || !Node.isObjectLiteralExpression(sidebar)) {
throw new Error('Expected themeConfig.sidebar to be an object or createVersionedSidebar(object).');
}
return sidebar.getText();
}
Loading
Loading