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
33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ jobs:

- run: npm test

# The check job covers the cross-cutting verifications that don't
# belong to a single language. `npm test` already runs these, but
# surface them as named steps so a failure has a clear job-step
# name rather than being buried in the test output.
- name: Verify release metadata
run: npm run verify:release-metadata

- name: Verify CHANGELOG entries
run: npm run verify:changelog

rust:
runs-on: ubuntu-latest
defaults:
Expand All @@ -31,6 +41,11 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -39,6 +54,24 @@ jobs:
with:
workspaces: clients/rust

- name: Install Node deps
working-directory: .
run: npm ci

# Verify the committed Rust sources match what `generate:rust`
# would emit. Mirrors the equivalent step in the swift / kotlin /
# typescript CI jobs. Catches "edited a .ts type but forgot to
# regenerate the Rust crate" before the PR ships.
- name: Verify generated Rust is up to date
working-directory: .
run: |
npm run generate:rust
if ! git diff --quiet -- clients/rust; then
echo "::error::Generated Rust sources are out of date. Run 'npm run generate:rust' and commit the result."
git --no-pager diff -- clients/rust
exit 1
fi

- run: cargo clippy --workspace -- -D warnings

- run: cargo test --workspace
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/publish-kotlin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
permissions:
contents: read

concurrency:
group: publish-kotlin-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
working-directory: clients/kotlin
Expand Down Expand Up @@ -51,6 +55,28 @@ jobs:
exit 1
fi

- name: Verify release metadata is consistent
working-directory: .
run: npm run verify:release-metadata

- name: Verify CHANGELOG entry exists
working-directory: .
run: npm run verify:changelog

# Belt and suspenders: also check the tag's exact version against
# the CHANGELOG. The PR-time `npm run verify:changelog` enforces
# this against `VERSION_NAME`, but a maintainer could tag with a
# different version than what's in `gradle.properties` on a racy
# commit — this catches that.
- name: Verify CHANGELOG entry exists for tag version
working-directory: .
run: |
TAG_VERSION="${GITHUB_REF_NAME#kotlin/v}"
if ! grep -qE "^## \[$TAG_VERSION\]" clients/kotlin/CHANGELOG.md; then
echo "::error::clients/kotlin/CHANGELOG.md is missing a '## [$TAG_VERSION]' entry. Move the Unreleased section under that heading before tagging."
exit 1
fi

- name: Run all checks
run: ./gradlew check --stacktrace

Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/publish-rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
permissions:
contents: read

concurrency:
group: publish-rust-${{ github.ref }}
cancel-in-progress: false

defaults:
run:
working-directory: clients/rust
Expand All @@ -23,6 +27,11 @@ jobs:
steps:
- uses: actions/checkout@v6

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
Expand All @@ -31,6 +40,44 @@ jobs:
with:
workspaces: clients/rust

- name: Install Node deps
working-directory: .
run: npm ci

# Verify the committed Rust sources are in sync with the TypeScript
# protocol definitions. Mirrors the equivalent check on the kotlin /
# swift / typescript publish workflows.
- name: Verify generated Rust is up to date
working-directory: .
run: |
npm run generate:rust
if ! git diff --quiet -- clients/rust; then
echo "::error::Generated Rust sources are out of date. Run 'npm run generate:rust' and commit the result before tagging."
git --no-pager diff -- clients/rust
exit 1
fi

- name: Verify release metadata
working-directory: .
run: npm run verify:release-metadata

- name: Verify CHANGELOG entry exists
working-directory: .
run: npm run verify:changelog

# Belt and suspenders: also check the tag's exact version (which
# may differ from the package.json/native-manifest version on a
# racy commit). Catches "tagged a version that isn't in the
# checked-out source's CHANGELOG."
- name: Verify CHANGELOG entry exists for tag version
working-directory: .
run: |
TAG_VERSION="${GITHUB_REF_NAME#rust/v}"
if ! grep -qE "^## \[$TAG_VERSION\]" clients/rust/CHANGELOG.md; then
echo "::error::clients/rust/CHANGELOG.md is missing a '## [$TAG_VERSION]' entry. Move the Unreleased section under that heading before tagging."
exit 1
fi

- name: Check formatting
run: cargo fmt --all -- --check

Expand Down
154 changes: 154 additions & 0 deletions .github/workflows/publish-spec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: Publish Spec

# Triggered when a maintainer pushes a `spec/vX.Y.Z` git tag. The workflow
# verifies the tag matches `PROTOCOL_VERSION` in `types/version/registry.ts`,
# verifies a matching CHANGELOG entry exists, regenerates the JSON schemas
# from the canonical TypeScript source, and publishes a GitHub Release with
# the schema files as assets and a small `registry-snapshot.json` describing
# the per-symbol introduced-in maps as of this commit.
#
# This release intentionally does NOT publish a registry package (npm,
# Maven, crates.io). See `RELEASING.md` for rationale: the canonical
# TypeScript types ship as part of the per-client TS package, and polyglot
# consumers can `curl` the schema assets from this release directly.

on:
push:
tags:
- 'spec/v[0-9]+.[0-9]+.[0-9]+'

permissions:
contents: read

# Only one spec publish per tag may run at a time. `cancel-in-progress:
# false` is deliberate — a re-pushed tag (or a manual re-trigger) queues
# behind the in-flight run rather than aborting it, so we never interrupt
# a half-finished GitHub Release upload.
concurrency:
group: publish-spec-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
# Promote to write only on the publish job so the validate steps can't
# accidentally mutate repo state via the GH token before the tag is
# confirmed correct.
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0

- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm

- run: npm ci

# Strict tag → PROTOCOL_VERSION cross-check. Mirrors the
# equivalent validation in `publish-rust.yml` / `publish-kotlin.yml`.
- name: Verify tag matches PROTOCOL_VERSION
run: |
TAG_VERSION="${GITHUB_REF_NAME#spec/v}"
REGISTRY_VERSION=$(node --input-type=module -e "
import { PROTOCOL_VERSION } from './types/version/registry.ts';
process.stdout.write(PROTOCOL_VERSION);
" 2>/dev/null || true)

# Fallback to a regex read if node's --input-type=module ESM-from-stdin
# can't load a .ts file directly (depends on node version + loader).
if [ -z "$REGISTRY_VERSION" ]; then
REGISTRY_VERSION=$(grep -oE "PROTOCOL_VERSION\s*=\s*'[^']+'" types/version/registry.ts \
| head -n1 | sed -E "s/.*'([^']+)'.*/\1/")
fi

if [ -z "$REGISTRY_VERSION" ]; then
echo "::error::Could not read PROTOCOL_VERSION from types/version/registry.ts"
exit 1
fi

if [ "$TAG_VERSION" != "$REGISTRY_VERSION" ]; then
echo "::error::Tag version ($TAG_VERSION) does not match PROTOCOL_VERSION ($REGISTRY_VERSION) in types/version/registry.ts. Bump PROTOCOL_VERSION (and re-run npm run generate) before tagging."
exit 1
fi

echo "Publishing spec version $REGISTRY_VERSION"
echo "SPEC_VERSION=$REGISTRY_VERSION" >> "$GITHUB_ENV"

# CHANGELOG must have a `## [X.Y.Z]` heading for the version being
# released. The check is intentionally lenient about the rest of the
# line (date / "Unreleased" status / etc.) so editorial freedom is
# preserved.
- name: Verify CHANGELOG entry exists
run: |
if ! grep -qE "^## \[$SPEC_VERSION\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md is missing a '## [$SPEC_VERSION]' entry. Move the Unreleased section under that heading before tagging."
exit 1
fi

# Regenerate from source so the published artifacts always match the
# tagged commit's TypeScript types, regardless of whether the
# committed schema files were stale.
- name: Regenerate JSON schemas
run: npm run generate:schema

# Bundle the schema files as release assets. Polyglot consumers
# (validators, third-party clients, conformance suites) can curl the
# assets directly from the release.
- name: Package schema archive
run: |
mkdir -p release-staging
cp schema/*.schema.json release-staging/
(cd schema && zip -r "../release-staging/ahp-schemas-${SPEC_VERSION}.zip" *.schema.json)

# Capture the per-symbol introduced-in maps as a machine-readable
# snapshot of the version registry. Future tooling can diff two
# snapshots to derive "what's new between spec X and spec Y" without
# parsing TypeScript. We intentionally do NOT compute the diff here
# (a v2 feature) — just snapshot the current state.
- name: Generate registry snapshot
run: |
npx tsx --eval "
import { readFileSync, writeFileSync } from 'node:fs';
import {
PROTOCOL_VERSION,
SUPPORTED_PROTOCOL_VERSIONS,
ACTION_INTRODUCED_IN,
NOTIFICATION_INTRODUCED_IN,
} from './types/version/registry.ts';
const snapshot = {
specVersion: PROTOCOL_VERSION,
supportedProtocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
actionIntroducedIn: ACTION_INTRODUCED_IN,
notificationIntroducedIn: NOTIFICATION_INTRODUCED_IN,
generatedAt: new Date().toISOString(),
commit: process.env.GITHUB_SHA,
};
writeFileSync('release-staging/registry-snapshot.json', JSON.stringify(snapshot, null, 2) + '\n');
"

- name: Extract release notes for this version
run: |
# awk extracts the body between `## [X.Y.Z]` and the next `## ` heading.
awk -v ver="$SPEC_VERSION" '
BEGIN { in_section = 0 }
/^## \[/ {
if (in_section) exit
if ($0 ~ "^## \\[" ver "\\]") { in_section = 1; print; next }
}
in_section { print }
' CHANGELOG.md > release-staging/release-notes.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release create "$GITHUB_REF_NAME" \
--title "AHP Spec v$SPEC_VERSION" \
--notes-file release-staging/release-notes.md \
release-staging/*.schema.json \
"release-staging/ahp-schemas-${SPEC_VERSION}.zip" \
release-staging/registry-snapshot.json
Loading