From 12fb6f3847cb10535547e506dbd3fac4486281d2 Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sat, 6 Jun 2026 08:49:49 -0700 Subject: [PATCH 1/2] ci(go): enforce gofmt -l in PR pipeline Mirrors the gofmt -l check already gating publish-go.yml. Without this in CI, formatting drift only surfaces when a release tag is pushed (caught the 0.3.0 release). Re-formats the two files that had drifted since the last gofmt pass: ahp/client_test.go and examples/reducers_demo/main.go (struct-field alignment only, no semantic change). --- .github/workflows/ci.yml | 9 +++++++++ clients/go/ahp/client_test.go | 10 +++++----- clients/go/examples/reducers_demo/main.go | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7474d844..0521878c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -240,6 +240,15 @@ jobs: - name: Build Go module run: go build ./... + - name: Check Go formatting + run: | + OUT=$(gofmt -l .) + if [ -n "$OUT" ]; then + echo "::error::gofmt -l reported unformatted files. Run 'gofmt -w' on them and commit the result:" + echo "$OUT" + exit 1 + fi + - name: Vet Go module run: go vet ./... diff --git a/clients/go/ahp/client_test.go b/clients/go/ahp/client_test.go index a6de2ac4..0279eebf 100644 --- a/clients/go/ahp/client_test.go +++ b/clients/go/ahp/client_test.go @@ -14,11 +14,11 @@ import ( // memTransport is an in-memory paired transport: the two ends share // each other's send/recv channels. Used as a fake server in tests. type memTransport struct { - inbox chan TransportMessage - outbox chan TransportMessage - closeMu *sync.Mutex - closed *bool - closeCh chan struct{} + inbox chan TransportMessage + outbox chan TransportMessage + closeMu *sync.Mutex + closed *bool + closeCh chan struct{} } func newMemTransportPair() (*memTransport, *memTransport) { diff --git a/clients/go/examples/reducers_demo/main.go b/clients/go/examples/reducers_demo/main.go index 8d65f666..9cfef5e8 100644 --- a/clients/go/examples/reducers_demo/main.go +++ b/clients/go/examples/reducers_demo/main.go @@ -24,8 +24,8 @@ func main() { actions := []ahptypes.StateAction{ {Value: &ahptypes.SessionTurnStartedAction{ - Type: ahptypes.ActionTypeSessionTurnStarted, - TurnId: "t1", + Type: ahptypes.ActionTypeSessionTurnStarted, + TurnId: "t1", Message: ahptypes.Message{Text: "Hello!"}, }}, {Value: &ahptypes.SessionResponsePartAction{ From 4a146be3e9e4013ebf743c5783af7789da55ea0e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Sat, 6 Jun 2026 08:49:49 -0700 Subject: [PATCH 2/2] docs: add release-ahp skill Captures the operational counterpart to RELEASING.md: two-commit PR structure, the six tag schemes, merge-vs-squash gotcha for tag SHA preservation, GPG/pubring-lock recovery, and the delete+re-push recovery for tags whose publish workflow didn't fire. --- .github/skills/release-ahp/SKILL.md | 269 ++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 .github/skills/release-ahp/SKILL.md diff --git a/.github/skills/release-ahp/SKILL.md b/.github/skills/release-ahp/SKILL.md new file mode 100644 index 00000000..f5cce227 --- /dev/null +++ b/.github/skills/release-ahp/SKILL.md @@ -0,0 +1,269 @@ +--- +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`](../../RELEASING.md) +and the cross-cutting [`AGENTS.md`](../../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**: + +1. **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.** +2. **Post-release commit** — bumps `PROTOCOL_VERSION` in + `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-` 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] — `. + - 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].version` **and** the + `version = "X.Y.Z"` pins on `ahp-types`/`ahp` inside + `[workspace.dependencies]`. + - `clients/kotlin/gradle.properties` `VERSION_NAME`. + - `clients/typescript/package.json` `version`. + - `clients/swift/VERSION` (bare semver, no `v` prefix, trailing newline). + - `clients/go/VERSION` (bare semver, no `v` prefix, 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** bump `PROTOCOL_VERSION` in this commit. The tag-push +> workflows validate that the registry version matches the tag's version, +> so commit 1 must still have `PROTOCOL_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 generate` at 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 test` again. + +Commit message: + +``` +chore: bump PROTOCOL_VERSION to X.(Y+1).0 for ongoing development +``` + +### 3. Open the PR with auto-merge + +```sh +git push -u origin release/ahp-X.Y.Z +gh pr create --base main --head release/ahp-X.Y.Z \ + --title "release: AHP X.Y.Z" \ + --body-file <(...) +gh pr merge --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](https://dev.azure.com/vscode/VSCode/_build)), not + GitHub Actions. + +### 4. After merge — verify the release SHA is reachable + +```sh +git checkout main +git pull --ff-only origin main +git cat-file -t # 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 + +```sh +RELEASE_SHA= +git tag spec/v $RELEASE_SHA +git tag rust/v $RELEASE_SHA +git tag kotlin/v $RELEASE_SHA +git tag typescript/v $RELEASE_SHA +git tag v $RELEASE_SHA # Swift — bare per RELEASING.md +git tag clients/go/v $RELEASE_SHA +git push origin \ + spec/v rust/v kotlin/v \ + typescript/v v clients/go/v +``` + +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: + +```sh +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: + +```sh +git push origin --delete +git push origin +``` + +Do **not** include the `--delete`d 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.lock` keeps 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.