Release runbook — binding tags & publishes

Human-assisted (RS-6). This is a runbook, not automation: every step here needs rights a CI session doesn't have (pushing tags, pausing/resuming workflows, PyPI/npm publish credentials). No step in this document has been executed by an agent — verify the "current state" section against git tag, npm view, and PyPI before acting, since it can drift the moment someone else runs a step.

Why this exists

The three language-binding release workflows (bindings-release.yml for acdp-node, acdp-py-release.yml, acdp-wasm-release.yml) are tag-triggered: pushing a tag matching acdp-node-v* / acdp-py-v* / acdp-wasm-v* runs the full build-and-publish job unconditionally (if: github.event_name == 'push' || !inputs.dry_run — the dry_run input only guards a manual workflow_dispatch, never a tag push). Some past releases were published via manual workflow_dispatch without a corresponding tag ever being pushed, so git tag now understates what's actually live on npm/PyPI.

⚠ H11: pushing a retroactive tag for a version that is already published will refire the matching workflow and attempt to publish that exact version again — npm/PyPI reject a republish of an existing version, so the run fails loudly (a red CI run, no data corruption) rather than silently succeeding or overwriting anything. Loud-but-safe is still not free: it wastes a CI run, pages whoever watches Actions, and (on the node workflow specifically) sits before the acdp-released dispatch step to acdp-control-plane, which only fires on the steps after a successful publish — so a failed retroactive-tag run does not spuriously trigger a downstream auto-bump PR there. Still, don't rely on "it'll just fail safely" as the plan — pick one of the three options below deliberately.

Automated tag-on-publish (as of 2026-08-30)

release-plz.yml has a "Release SDK bindings at the acdp version" step that dispatches all three binding release workflows (acdp-py-release.yml, bindings-release.yml, acdp-wasm-release.yml) via workflow_dispatch -f dry_run=false whenever the core acdp crate itself releases — that's the SDK cascade referenced throughout this doc.

As of 2026-08-30, each of those three workflows now pushes its own matching git tag (acdp-py-v$VER / acdp-node-v$VER / acdp-wasm-v$VER) automatically, in a "Tag the release" step that runs immediately after a successful workflow_dispatch-triggered publish (gated on github.event_name == 'workflow_dispatch' && !inputs.dry_run, so it never fires on an actual tag push — the tag already exists in that case — or on a dry run — nothing was published). This closes the root cause behind the "Why this exists" section above: a workflow_dispatch publish can no longer land without a tag.

That same "Tag the release" step also fails the job if the version input was left blank on a manual dispatch — deliberately, since a blank version can't be turned into a sane tag name. This runs after the publish itself, so a manual non-dry-run dispatch with a blank version will now publish successfully and then fail the job at the tag step (a previously-green pattern that is red now). Always pass an explicit -f version=... on a manual non-dry-run dispatch.

This does not retire the manual tag-push instructions elsewhere in this document (Steps 1–2 below, and the RS-8 addendum's Steps further down). Those remain exactly what you want for the manual release path — a human deliberately dispatching a workflow or pushing a tag themselves outside the automated cascade, e.g. testing a binding release before the core crate is ready to cut, or recovering from a failed automated cascade. The automated tagging above is simply no longer the only path that produces a correctly-tagged release; it's the path the SDK cascade takes by default.

Two deliberate scope limits on this automation, so this doc doesn't imply broader coverage than what actually shipped:

  • Consumer-bump notification stays manual-path-only. The new tag push does not itself re-trigger the workflow that pushed it — a GITHUB_TOKEN-authored push does not start new workflow runs. release-plz.yml's own comment notes the opposite case: workflow_dispatch is exempt from that "no recursive workflow runs" rule, which is why the SDK cascade's dispatched runs execute at all — but a plain tag push (like the one the new "Tag the release" step performs here) gets no such exemption, so it does not start a new run. That means the push-gated consumer-notification dispatch steps (acdp-py-release.yml / bindings-release.yml, gated if: github.event_name == 'push', which notify acdp-playground and acdp-control-plane respectively) still only fire on an actual tag-push trigger, never on the automated workflow_dispatch cascade. Downstream consumer-bump notification remains a manual-path-only concern; this automation does not close that separate gap.
  • A partial-failure recovery re-run correctly skips the tag step. The "Tag the release" step uses a plain if: (carrying only its own gate above), which implicitly requires success() on everything before it in the job. If an earlier step in that publish job fails — including on a partial-failure recovery re-run — the tag step is skipped. This is intentional, not a bug: an incomplete publish should not get tagged as if it fully succeeded.

Current state (verified 2026-08-29 — re-check before acting)

The sibling plan (agentcontextdistributionprotocol/plans/siblings/acdp-rs.md, RS-6) was written 2026-08-28 and states PyPI is "still 0.7.0." That's now stale — live PyPI/npm state as of this runbook is materially ahead of what the plan assumed:

ArtifactLast tagged releasePublished versions (npm/PyPI, ground truth)Local manifest version
acdp crate (this repo)acdp-v0.8.1—0.8.1 (Cargo.toml workspace version)
acdp-node (npm @agentcontextdistributionprotocol/acdp)acdp-node-v0.7.00.7.0, 0.8.0, 0.8.1 — 0.8.0 and 0.8.1 both published tag-less via workflow_dispatch on 2026-07-10 (runs at commits c2a89032 and c4c9be8d)0.8.0 (bindings/acdp-node/package.json, stale relative to what's already published)
acdp-py (PyPI acdp)acdp-py-v0.7.00.7.0, 0.8.0, 0.8.1 — 0.8.0 and 0.8.1 both published tag-less via workflow_dispatch on 2026-07-10, same two commits as node0.8.0 (bindings/acdp-py/Cargo.toml, stale)
acdp-wasm (npm @agentcontextdistributionprotocol/acdp-wasm)acdp-wasm-v0.7.00.7.0, 0.8.0 — the 0.8.0 workflow_dispatch at commit be72ca24 succeeded; a same-day follow-up attempt at 0.8.1 (commit c4c9be8d, run 29129059112) failed during cargo metadata on a getrandom version conflict (acdp-wasm required getrandom ^0.4 with feature js, which only exists on 0.2.x) — that bug was the dual-major getrandom pin mixup fixed later by PR #153 (commit c55eacd, 2026-08-28). 0.8.1 was never published for wasm (stale as of 2026-08-30 — a 0.8.1 retry did later succeed, tagged acdp-wasm-v0.8.1, and npm now holds 0.7.0 through 0.8.4; see the manifest column and re-run npm view/git tag before trusting this cell).0.8.4 (bindings/acdp-wasm/Cargo.toml, bumped as of 2026-08-30 to match npm's actual latest published version, 0.8.4 — the bump is purely cosmetic per the workflow's re-stamp-at-build-time design and not itself a publish; see "Automated tag-on-publish" above)

Per "Automated tag-on-publish" above, any future workflow_dispatch publish (manual or via the automated cascade) now tags itself automatically; no human backfill should be needed going forward for new releases.

Re-run these checks before executing anything below — another session or the bot may have moved the state again:

git tag -l "acdp-node-v*" "acdp-py-v*" "acdp-wasm-v*" | sort -V
npm view @agentcontextdistributionprotocol/acdp versions --json
npm view @agentcontextdistributionprotocol/acdp-wasm versions --json
curl -s https://pypi.org/pypi/acdp/json | python3 -c "import json,sys; print(sorted(json.load(sys.stdin)['releases']))"

The plan

Target: all three bindings published and tagged at the crate family version, 0.8.1 — matching the accept criterion ("node/py/wasm published at the family version with matching git tags"). Node and py content is already there (published, just untagged); wasm genuinely needs one more publish.

Step 0 — pause the tag-triggered workflows

Do this before pushing any tag in this runbook, retroactive or new — it's what makes every following step safe regardless of which option below you pick for the retroactive markers:

gh workflow disable bindings-release.yml
gh workflow disable acdp-py-release.yml
gh workflow disable acdp-wasm-release.yml

(gh workflow enable <name> reverses this — needed again at the end of Step 2.)

Step 1 — retroactive markers for what's already published, tag-less

With the workflows paused, tags can be pushed safely — nothing will fire. Point each tag at the exact commit that workflow run actually published from (not current HEAD), so the tag is historically accurate:

git tag acdp-node-v0.8.0 c2a89032a5b706d95efd1e5e69e1f7df22aa6084
git tag acdp-node-v0.8.1 c4c9be8d68dda2842b6a9b06efc2157d79090198
git tag acdp-py-v0.8.0   c2a89032a5b706d95efd1e5e69e1f7df22aa6084
git tag acdp-py-v0.8.1   c4c9be8d68dda2842b6a9b06efc2157d79090198
git tag acdp-wasm-v0.8.0 be72ca24fe7d0246b1fe132ef10c0eaed7ecfc0b
git push origin acdp-node-v0.8.0 acdp-node-v0.8.1 acdp-py-v0.8.0 acdp-py-v0.8.1 acdp-wasm-v0.8.0

Do not tag acdp-wasm-v0.8.1 here — it was never actually published (see the table above), so tagging it would be a false historical record. It gets a real, live tag in Step 2 instead.

This is "tag with workflows paused" (H11 option 1) rather than non-triggering tag names: it keeps the permanent tag names consistent with the live ones future releases will use (acdp-node-v0.8.0, not some retroactive/… variant that git describe/tooling won't recognize), at the cost of the two-step pause/resume — a fine trade for a one-time backfill.

Step 2 — the real, live wasm 0.8.1 publish

This is the one artifact that genuinely isn't at the family version yet.

  1. Confirm the getrandom fix from PR #153 is present on the commit you're releasing from (it is, as of main post-2026-08-28) — spot-check:
    grep -n "getrandom" bindings/acdp-wasm/Cargo.toml
    # expect: getrandom = { version = "0.2", features = ["js"] }
    #         getrandom_wasm = { package = "getrandom", version = "0.4", features = ["wasm_js"] }
  2. Bumping bindings/acdp-wasm/Cargo.toml's version ahead of a release is optional, low-priority cleanup, same as the equivalent node/py manifest bump in Step 3 below — the workflow re-stamps this from the tag name at build time regardless, so the committed value is never functionally required for a publish to succeed; it only keeps the working tree honest for a human reader. (As of 2026-08-30, the manifest already reads 0.8.4, ahead of any version this Step 2 was originally written against — bump it again to whatever version you're actually releasing before dispatching, or skip it and let the workflow's re-stamp handle it.)
  3. Re-enable just the wasm workflow and do a dry run first, to catch any other drift before spending a real publish attempt:
    gh workflow enable acdp-wasm-release.yml
    gh workflow run acdp-wasm-release.yml -f version=0.8.1 -f dry_run=true
    # watch it green, then:
    git tag acdp-wasm-v0.8.1 <the commit the dry run built from>
    git push origin acdp-wasm-v0.8.1
    The pushed tag triggers the real (non-dry-run) publish.
  4. Re-enable the other two workflows now that no more retroactive tags are pending:
    gh workflow enable bindings-release.yml
    gh workflow enable acdp-py-release.yml

Step 3 — local manifest hygiene (optional, low-risk cleanup)

bindings/acdp-node/package.json and bindings/acdp-py/Cargo.toml still declare 0.8.0 locally even though 0.8.1 is already published — harmless (the release workflows stamp the version from the tag at build time, not from the committed manifest) but confusing to a reader. Bump both to 0.8.1 in a normal PR, no tag implications either way.

Step 4 — unblocks

Once this runbook's tags exist, per 00-overview.md §3: PG-1 (PyPI→family version, already effectively true, just needed the tag) and UI-1 (wasm bump + tagged release) are unblocked, and CI-3 (tag-triggered publish = propagation head) can record this as the policy going forward.

RS-8 binding follow-up release (anchors, RFC-ACDP-0016) — DONE (2026-08-30)

PR #175 merged to main as 510ca40 (2026-08-30). Both dry runs went green (acdp-py-release.yml, bindings-release.yml, -f version=0.8.3), then real tags acdp-py-v0.8.3 and acdp-node-v0.8.3 were pushed against 510ca40. Both release workflows completed successfully — acdp 0.8.3 is live on PyPI, and @agentcontextdistributionprotocol/acdp 0.8.3 plus all four platform packages (acdp-darwin-arm64, acdp-darwin-x64, acdp-linux-x64-gnu, acdp-linux-arm64-gnu) are live on npm. Verified via real pip install acdp==0.8.3 / npm install @agentcontextdistributionprotocol/acdp@0.8.3 installs plus an end-to-end publish-request-with-anchors + verify_content_hash smoke test against each installed package, not just the registry listing.

One transient hiccup, not a real failure: acdp-linux-x64-gnu@0.8.3's versioned registry endpoint 404'd for about a minute after the CI publish step reported success (the CI log showed the correct + @agentcontextdistributionprotocol/acdp-linux-x64-gnu@0.8.3 line) — the same npm read-replica propagation lag this runbook's own earlier section already diagnosed for a different package. Resolved on its own; no republish needed.

acdp-py/acdp-node are now genuinely in lock-step at 0.8.3 (previously drifted, 0.8.1/0.8.2 respectively — see the superseded "Current state" section below, kept for historical context on the drift pattern rather than deleted).

Current state (as of 2026-08-29, BEFORE the above — historical, do not use for a

future release; re-check git tag / the registries fresh each time)

ArtifactLatest git tagActually liveCommitted manifest
acdp (PyPI)acdp-py-v0.8.10.8.2 (verified via pypi.org/pypi/acdp/json)bindings/acdp-py/Cargo.toml: 0.8.0 (stale either way — irrelevant to publish, see below)
@agentcontextdistributionprotocol/acdp (npm)acdp-node-v0.8.20.8.2 (verified via registry.npmjs.org)bindings/acdp-node/package.json: 0.8.0 (same)

The git tag for acdp-py understates reality by one patch version — a 0.8.2 was published to PyPI without a matching tag ever being pushed, the exact pattern this runbook's "Why this exists" section describes. This doesn't block anything below; it just means the target version is computed from the live registry, not from git tag.

Neither committed manifest (Cargo.toml/package.json) needs to change before tagging — confirmed for both workflows, not assumed: bindings-release.yml's and acdp-py-release.yml's "Stamp release version" steps both compute the published version as ${{ inputs.version }} (dispatch) or ${GITHUB_REF_NAME#acdp-*-v} (tag push), then overwrite the manifest file at build time — the committed value is never read. Only the CHANGELOG.md entries need to already exist (they do — see the ## Unreleased sections in both bindings/acdp-py/CHANGELOG.md and bindings/acdp-node/CHANGELOG.md).

Target versions

Following the precedent set by RS-8's own core-crate release (0.8.1 → 0.8.2, a patch bump for the same purely-additive, non-breaking anchors field): both bindings go from their live 0.8.2 to 0.8.3. This happens to restore py/node lock-step (both currently live at 0.8.2, matching for once) — a side effect, not a guarantee this holds for the next release after this one.

Steps

  1. Dry-run first, both workflows, to catch a build break before it's irreversible:
    gh workflow run acdp-py-release.yml --ref feat/rs8-bindings-anchors -f dry_run=true -f version=0.8.3
    gh workflow run bindings-release.yml --ref feat/rs8-bindings-anchors -f dry_run=true -f version=0.8.3
    Wait for both to go green (gh run watch <id> --exit-status or gh run list --workflow=&lt;name&gt; --limit 1) before continuing. If either fails, fix and re-dry-run — do not proceed to a real tag push on a red dry run. Note: dispatch this against feat/rs8-bindings-anchors only until it merges to main; once merged, dispatch (and the real tag, below) should point at main.
  2. Push the real tags, once merged to main and both dry runs are green:
    git tag -a acdp-py-v0.8.3 -m "acdp-py v0.8.3" <main-HEAD-sha>
    git tag -a acdp-node-v0.8.3 -m "acdp-node v0.8.3" <main-HEAD-sha>
    git push origin acdp-py-v0.8.3 acdp-node-v0.8.3
    Each push triggers its workflow for real (if: github.event_name == 'push' skips the dry_run gate entirely — a tag push always publishes).
  3. Verify live, the same way the earlier 0.8.2 npm incident was diagnosed in this family's history — don't trust the packument/listing endpoint alone (it can lag); check the versioned endpoint directly:
    curl -s -H "User-Agent: acdp-rs-session (you@example.com)" https://pypi.org/pypi/acdp/0.8.3/json | head -c 200
    curl -s -H "User-Agent: acdp-rs-session (you@example.com)" https://registry.npmjs.org/@agentcontextdistributionprotocol/acdp/0.8.3
    For npm specifically, also confirm the 4 platform packages (acdp-darwin-arm64/acdp-darwin-x64/acdp-linux-x64-gnu/acdp-linux-arm64-gnu) published at 0.8.3 too, not just the root loader package — the bindings-release.yml publish job runs napi pre-publish (platform packages) and npm publish (root) as two separate steps; a partial success there is a known past failure mode for this pipeline (napi-cli 3.x's --gh-release default caused exactly this on the 0.8.2 release — since fixed, but worth an explicit check here rather than assuming).
  4. H11 reminder: if step 1's dry run or step 2's tag push needs a re-run for any reason, do NOT re-push a tag for a version that already published successfully — npm/PyPI reject the republish (harmless 403, per this family's own incident history) but it's a wasted, alarming red CI run. Bump to the next patch instead if a real do-over is needed.

Coordination note for SPEC-11 (docs/version-matrix.md refresh, spec repo)

This is a read, not an edit — docs/version-matrix.md lives in the spec repo (agentcontextdistributionprotocol/agentcontextdistributionprotocol), not here; per this family's cross-repo convention, the spec-repo session doing SPEC-11 should pull the following facts from this runbook rather than this repo editing that file directly:

  • acdp (Rust, this repo): crate 0.8.1, tags acdp-v0.8.1 etc. — already current.
  • acdp-node / acdp-py / acdp-wasm: once this runbook's Steps 1–2 are executed, all three sit at 0.8.1 with matching git tags (acdp-node-v0.8.1, acdp-py-v0.8.1, acdp-wasm-v0.8.1) and their published registries (npm, npm, PyPI respectively).
  • The RFC coverage is identical across all four artifacts (RFC-ACDP-0001 through 0015, 0009 reserved) — acdp-wasm is verification-only (no producer/signing surface), the other three implement both sides.
  • If Step 2 hasn't run yet when SPEC-11 executes, say so explicitly in the version matrix rather than asserting 0.8.1 for wasm — check npm view @agentcontextdistributionprotocol/acdp-wasm versions first.