Skill v1.0.0
Trusted Publisher100/100version: "1.0.0" name: release-ahp description: Cut a coordinated AHP release (spec + all 5 clients) — bumps every manifest, dates the CHANGELOGs, opens the release PR with auto-merge, and walks through pushing the six per-artifact tags. Use when the user says "release AHP X.Y.Z", "publish AHP", "cut a release", "tag the release", or similar. The skill also captures the recovery procedure for tags whose publish workflow didn't fire.
Releasing AHP
This skill is the operational counterpart to `RELEASING.md` and the cross-cutting `AGENTS.md`. RELEASING.md is the canonical reference for what to do per artifact; this skill captures the order of operations and the foot-guns that bite during a multi-artifact release.
The repo ships six independently-versioned artifacts but they are released together by convention — pin the same X.Y.Z across all five client manifests and the spec on every release unless the user explicitly asks otherwise.
When to use this skill
The user wants to release AHP. Typical asks:
- "release AHP 0.3.0"
- "tag the release", "push the release tags", "run the publish workflows"
- "the next release will be X.Y.Z" (implies post-release bump)
- "the publish workflow didn't fire on tag T, can you re-trigger it"
If the user asks for a single-client release (e.g. "just bump Rust"), prefer the .github/prompts/publish-*.prompt.md files instead — this skill is for coordinated cross-artifact releases.
Mental model
A release is two commits in one PR, plus six tags pushed at the first commit's SHA after merge:
- Release commit — date-stamps every
## [X.Y.Z]CHANGELOG heading,
bumps every client manifest from the previous version → X.Y.Z, and regenerates release-metadata.json for every client. All six tags must point at this commit's SHA.
- Post-release commit — bumps
PROTOCOL_VERSIONin
types/version/registry.ts to the next planned version, prepends it to SUPPORTED_PROTOCOL_VERSIONS, regenerates each Version.generated.{rs,kt,swift,go}, and reopens ## [Unreleased] in every CHANGELOG (plus a ## [X.Y+1.0] — Unreleased placeholder in the root spec CHANGELOG.md).
Step-by-step
0. Capture the inputs
Before touching anything, confirm with the user:
- Release version (
X.Y.Z) — must equal the## [X.Y.Z]heading
already accumulated under ## [Unreleased] in every CHANGELOG.
- Next development version (default: bump minor, i.e.
X.(Y+1).0). - Release scope (default: all six artifacts; rare to release one in
isolation).
1. Branch + commit 1 (release commit)
Create release/ahp-<version> off main. Then in a single commit:
- CHANGELOGs — for each of the 6 files (
CHANGELOG.md,
clients/{rust,kotlin,typescript,swift,go}/CHANGELOG.md):
- Replace
## [Unreleased](or## [X.Y.Z] — Unreleased) with
## [X.Y.Z] — <today YYYY-MM-DD>.
- In each per-client CHANGELOG, add a single line
Implements AHP X.Y.Z.
under the new heading if not already present.
- Manifests — bump every native version file to
X.Y.Z: clients/rust/Cargo.toml[workspace.package].versionand the
version = "X.Y.Z" pins on ahp-types/ahp inside [workspace.dependencies].
clients/kotlin/gradle.propertiesVERSION_NAME.clients/typescript/package.jsonversion.clients/swift/VERSION(bare semver, novprefix, trailing newline).clients/go/VERSION(bare semver, novprefix, trailing newline).- Lockfiles — refresh after the manifest bump:
- In
clients/typescript/:npm install --no-audit --no-fund(no-op if
already up to date but writes the version into package-lock.json).
- In
clients/rust/:cargo update -w(rewrites only the workspace
crates in Cargo.lock — outside crates stay pinned).
- Metadata — at repo root:
npm run generate:metadata. - Verify — at repo root:
npm test. This runs both
verify:release-metadata and verify:changelog, which together gate the release: a mismatch between a manifest version and its CHANGELOG heading will fail here and not at tag-push time.
Do not bumpPROTOCOL_VERSIONin this commit. The tag-pushworkflows validate that the registry version matches the tag's version,so commit 1 must still havePROTOCOL_VERSION = X.Y.Z.
Commit message:
release: AHP X.Y.Z
Record this commit's SHA. Every release tag points at it.
2. Commit 2 (post-release bump)
On the same branch, in a second commit:
types/version/registry.ts:PROTOCOL_VERSION = 'X.(Y+1).0'.SUPPORTED_PROTOCOL_VERSIONS = ['X.(Y+1).0', 'X.Y.Z', ...](newest
first; keep any older entries the previous list had).
- Run
npm run generateat repo root — this regenerates every client's
Version.generated.* and every release-metadata.json.
- Reopen
## [Unreleased]at the top of every CHANGELOG, above the
newly date-stamped ## [X.Y.Z] heading. In the root spec CHANGELOG.md, also add a ## [X.(Y+1).0] — Unreleased placeholder (with a Spec version: \X.(Y+1).0\`` line).
- Run
npm testagain.
Commit message:
chore: bump PROTOCOL_VERSION to X.(Y+1).0 for ongoing development
3. Open the PR with auto-merge
git push -u origin release/ahp-X.Y.Zgh pr create --base main --head release/ahp-X.Y.Z \--title "release: AHP X.Y.Z" \--body-file <(...)gh pr merge <num> --auto --merge
Use `--merge`, not `--squash` — the tags need commit 1's SHA to survive the merge. A squash collapses both commits into a new SHA on main, and the workflows' PROTOCOL_VERSION check would then fail because post-merge HEAD is on the next-dev version. A merge commit preserves both original commits as reachable parents.
If the repo's branch protection forces squash, fall back to either:
- Tag the SHA before the squash-merge completes (the commit stays
retained once tagged), or
- Use the "Rebase and merge" strategy (also preserves both SHAs).
Always confirm before merging which strategy preserves commit 1's SHA on main, and resolve any ambiguity with the user.
The PR body should include:
- The release SHA in a copy-pastable variable assignment.
- The six tag-push commands (see step 5 below).
- A note that Kotlin and TypeScript publish via Azure DevOps pipelines
(they appear under agent-host-protocol pipelines in vscode-engineering), not GitHub Actions.
4. After merge — verify the release SHA is reachable
git checkout maingit pull --ff-only origin maingit cat-file -t <RELEASE_SHA> # must print "commit"
If it doesn't exist (squash happened despite intent), open a follow-up discussion with the user before tagging. Do not tag the squashed commit on main — its registry shows the post-release version, and every publish workflow will reject the tag.
5. Push the six tags
RELEASE_SHA=<sha from step 1>git tag spec/v<X.Y.Z> $RELEASE_SHAgit tag rust/v<X.Y.Z> $RELEASE_SHAgit tag kotlin/v<X.Y.Z> $RELEASE_SHAgit tag typescript/v<X.Y.Z> $RELEASE_SHAgit tag v<X.Y.Z> $RELEASE_SHA # Swift — bare per RELEASING.mdgit tag clients/go/v<X.Y.Z> $RELEASE_SHAgit push origin \spec/v<X.Y.Z> rust/v<X.Y.Z> kotlin/v<X.Y.Z> \typescript/v<X.Y.Z> v<X.Y.Z> clients/go/v<X.Y.Z>
The six tag schemes are deliberate and not interchangeable. Bare vX.Y.Z is reserved for Swift (SwiftPM only resolves root-level tags); clients/go/vX.Y.Z is required by the Go module proxy's sub-module resolution.
6. Confirm publish runs started
For each tag, verify the corresponding workflow run is queued or in progress:
GH_PAGER=cat gh run list --limit 10 \--json databaseId,event,headBranch,status,conclusion,workflowName,displayTitle
Expect to see four GH Actions runs (one per tag below) within seconds:
| Tag | Workflow | Publishes to | |
|---|---|---|---|
spec/vX.Y.Z | Publish Spec | GitHub Release (schema assets) | |
rust/vX.Y.Z | Publish Rust Crates | crates.io (ahp-types, ahp, ahp-ws) | |
vX.Y.Z | Publish Swift Package | SwiftPM (tag-resolved) | |
clients/go/vX.Y.Z | Publish Go Module | Go module proxy (tag-resolved) |
The remaining two tags trigger Azure DevOps pipelines that don't appear in gh run list:
| Tag | ADO Pipeline | Publishes to | |
|---|---|---|---|
kotlin/vX.Y.Z | clients/kotlin/pipeline.yml | Maven Central via ESRP | |
typescript/vX.Y.Z | clients/typescript/pipeline.yml | npm via ESRP |
For Kotlin/TypeScript, link the user to the AHP pipelines in ADO (vscode-engineering tenant) to confirm the runs started. Both pipelines can also be triggered manually from the ADO UI as a hotfix escape hatch — the validation steps inside each pipeline are identical to the tag-triggered path.
Recovery — tag pushed but no publish run fired
This has happened. Symptom: tag exists on origin but gh run list shows no corresponding workflow run.
None of the four GH Actions publish workflows currently declare workflow_dispatch, so manual dispatch is not available. The recovery is to delete the remote tag and re-push it at the same SHA — this emits a fresh push event without changing the tagged commit:
git push origin --delete <tag>git push origin <tag>
Do not include the --deleted tag in the same git push command as the re-push — push them in separate git push invocations so each tag emits its own push event. Re-pushing several tags in a single command sometimes coalesces into a single event and only one workflow fires.
Once recovery is verified, suggest a follow-up PR adding workflow_dispatch: to the four publish workflows so the next missed trigger can be re-run from the Actions UI without tag thrash.
Common foot-guns
- GPG signing lock during `git commit` — if you see
gpg: keydb_search failed: Operation timed out, run gpgconf --kill all && rm -f ~/.gnupg/public-keys.d/pubring.db.lock and retry the commit.
- Bumping `PROTOCOL_VERSION` in commit 1 — the publish workflows
re-validate the tag against the tagged commit's PROTOCOL_VERSION, so a release tag pointing at the post-release commit fails the "Verify tag matches" step. Always keep the bump in commit 2.
- Forgetting the Rust `[workspace.dependencies]` pins — bumping just
[workspace.package].version leaves the cross-crate ahp-types/ahp pins on the old version. verify-release-metadata does not catch this but cargo publish --dry-run will.
- Forgetting `cargo update -w` — without it,
Cargo.lockkeeps the
old version for ahp-types/ahp/ahp-ws and the resulting commit is partially-bumped. CI's per-language drift check catches it.
- Forgetting the Swift/Go trailing newline in `VERSION` — the
read*PackageVersion helpers trim, so functionally it's fine, but the convention is 0.3.0\n.
- Mixing up the Swift tag namespace — Swift uses bare
vX.Y.Z
(no prefix). Every other artifact has a prefix. Putting swift/vX.Y.Z on Swift's tag silently breaks SwiftPM resolution.