release · v1.0 · 2026-11-08 · sha256 f73c5f7668d59ee9
release v1.0A
Immutable. This exact content never changes and is served at /api/v1/blob/f73c5f7668d59ee9.
--- name: release description: Cut and verify a toolnexus release across all eight registries, with the guardrails that a green CI run does not give you. Use whenever the user says publish, release, ship it, cut a release, bump the version, or asks whether a release actually landed. Also use to re-publish a single failed registry leg. license: MIT compatibility: Requires gh (authenticated), node, git. Read-only checks run anywhere; publishing needs owner authorization. metadata: author: toolnexus version: "1.0" --- # Releasing toolnexus Publishing is **outward-facing and irreversible**: a version number can never be reused on npm, PyPI, NuGet, Maven Central, Hex, Clojars or crates.io. This skill exists because the obvious signals lie in both directions — a green workflow can ship nothing, and a red workflow can be a complete success. ## The two rules that override intuition **1. A green run is not evidence a package landed.** ADR-0012 exists because NuGet and Maven have reported success without publishing. **Always verify from outside, as a consumer would** (step 6). **2. A red run is not evidence of failure.** Re-dispatching a completed release makes the already-published legs refuse a duplicate — npm `cannot publish over the previously published versions`, Clojars `403 redeploying non-snapshots is not allowed`. That is immutability working. Read the **job list**, not the conclusion. ## Never do these - **Never publish unless the owner asked in this conversation.** "Is it ready?" is a question, not authorization. - **Never release unmerged code.** The release is cut from `main` after the PR merges. - **Never bump one port "to unblock".** `preflight` refuses it, and that refusal is the point. - **Never turn `ENABLE_*` variables off to isolate one registry.** A disabled leg is skipped *silently*; forgetting to restore it makes a future release quietly incomplete. Accept a red run whose only real job succeeds instead. - **Never print, log, echo or paste a registry token.** They are use-only env vars read at the point of use. ## Steps ### 1. Run the readiness gate ```sh .claude/skills/release/scripts/check-release-readiness.sh <version> ``` It replicates `release.yml`'s `preflight` locally **and adds the check preflight does not have** — that the versioned install coordinates in the docs match the release. It checks: seven manifests in lockstep (golang has none — it releases as a tag), docs install coordinates, a dated `## <version>` changelog section with a fresh `## Unreleased` above it, clean tree on `main` in sync with origin, the tag being free, all eight `ENABLE_*` gates (including `ENABLE_CRATES`), and **licensing**. The licensing check covers what actually drifts: a LICENSE file in every port, `THIRD-PARTY-NOTICES.md` present, the elixir Hex package still shipping LICENSE in its `files:` list, and **every declared dependency appearing in the notices** — the real failure mode being a dependency added and never listed. It matches names only across shipped configurations (test/dev deps are excluded: java `testImplementation`, elixir `excoveralls`/`ex_doc`). It does **not** verify the license of each pinned version — that needs a per-ecosystem resolver, and the notices file instead states the rule: re-verify on add/remove/major-version change, because an upstream can relicense between releases (`ModelContextProtocol` for C# is Apache-2.0 while the other MCP SDKs are MIT). Fix every `✗` before continuing. ### 2. Choose the version honestly Patch for pure fixes. **Minor when observable behavior moves**, even without an API break — a consumer pinning `~0.13.0` should not silently inherit changed output. If in doubt, ask; it cannot be undone. ### 3. Bump and roll, as one commit on its own branch All seven manifests (js, python, csharp, java, elixir, `clojure/build.clj`, `rust/Cargo.toml`) to the same version, and `## Unreleased` → `## X.Y.Z — YYYY-MM-DD` with a fresh empty `## Unreleased` above it. **Update the docs install coordinates in the same commit** — `README.md`, `java/README.md`, `clojure/README.md`, `elixir/README.md`, `clojure/examples/*/deps.edn`, `rust/README.md`, `site/src/content/docs/install.mdx`, `quickstart.mdx`, `api/clojure.mdx`, and the in-process-client recipe sources `site/recipes/in-process-client/*` (then run `node site/scripts/sync-recipes.mjs`, or CI's "API reference coverage" fails on stale pages). Leave historical statements alone: "on Clojars since v0.13.0", "Relay ships in golang/ as of 0.12.0", and `docs/performance-benchmarks.md`'s "Versions measured". Rewriting those falsifies the record. ### 3b. Know that the release gates on a LIVE model run `release.yml` has a `live-scenarios` job — **golang only**, behind the `live` build tag — that runs the harness scenarios against a real model on OpenRouter before **any** registry leg publishes (every leg `needs: [preflight, live-scenarios]`). The other six ports prove parity hermetically in CI; this proves the mechanisms actually work against a provider before anything ships. It needs **`OPENROUTER_API_KEY` in the `prod` environment**. If it is missing the whole release fails at the gate, before publishing — which is the correct failure, but check it is present before cutting rather than discovering it mid-release. Run the same suite locally first: ```sh cd golang && OPENROUTER_API_KEY=… go test -tags live -run TestLive -v ./... ``` The tag keeps these out of normal CI: `go test ./...` never sees them, and the tests skip cleanly when the key is absent. ### 4. Open the PR and wait for every CI job "Docs examples (eight ports)" legitimately takes **13–18 minutes**. It is not hung. Do not merge on all-but-one. ### 5. Merge, then cut the Release The GitHub Release **is** the trigger. Its body is the changelog section — extract it, never hand-write a second account: ```sh gh release create v<version> --title "v<version>" --notes-file <extracted-section> --target main ``` ### 6. Verify every registry from outside Do this even when the run is green. Do not report a release as done without it. ```sh npm view toolnexus version curl -s https://pypi.org/pypi/toolnexus/json | python3 -c 'import sys,json;print(json.load(sys.stdin)["info"]["version"])' curl -s https://api.nuget.org/v3-flatcontainer/toolnexus/index.json | python3 -c 'import sys,json;print(json.load(sys.stdin)["versions"][-1])' curl -s https://repo1.maven.org/maven2/io/github/muthuishere/toolnexus/ | grep -oE '0\.[0-9]+\.[0-9]+' | sort -uV | tail -1 curl -s https://clojars.org/api/artifacts/net.clojars.muthuishere/toolnexus | python3 -c 'import sys,json;print(json.load(sys.stdin)["latest_version"])' curl -s https://hex.pm/api/packages/toolnexus | python3 -c 'import sys,json;print(json.load(sys.stdin)["releases"][0]["version"])' curl -s https://crates.io/api/v1/crates/toolnexus | python3 -c 'import sys,json;print(json.load(sys.stdin)["crate"]["max_version"])' git ls-remote --tags origin | grep golang/v<version> ``` crates.io needs a `User-Agent`-friendly client; if the API refuses a bare curl, add `-A toolnexus-release-check`. Verify a consumer build too: `cargo new t && cd t && cargo add toolnexus@<version> tokio -F tokio/full && cargo build`. **Indexing lag is normal and is not failure**: NuGet ~5 min, Maven Central ~15–20 min. Poll before concluding anything. Clojars is `net.clojars.muthuishere`, **not** `io.github.muthuishere`. ### 7. Marketing Run the `marketing` skill for this version (drafts only; the owner posts). It builds `~/.marketing/toolnexus/<version>/` from the changelog section and the standing page. ### 8. If a leg failed Diagnose first — `gh run view <id> --log-failed`. Credential failures (`API key revoked`, `401`, `403`) are owner fixes; report them and **file an issue** rather than retrying blindly. To re-publish one leg after the fix: `gh workflow run release.yml --ref main -f version=<version>`. This re-runs everything. Expect the already-published legs to fail on duplicate rejection and the run to go red — check that the leg you cared about says `success`, then verify from outside. ## Registry facts worth not re-deriving | registry | auth | duplicate behavior | |---|---|---| | npm, PyPI, NuGet | OIDC Trusted Publishing, no stored secret | npm hard-fails; PyPI/NuGet skip | | Maven Central | `CENTRAL_USERNAME`/`CENTRAL_PASSWORD` + GPG, `prod` env | rejects | | Hex | `HEX_API_KEY`, `prod` env, gated by `ENABLE_ELIXIR` (not `ENABLE_HEX`); needs `api:write` | rejects | | Clojars | `CLOJARS_USERNAME`/`CLOJARS_PASSWORD` (a deploy token) | `403 non-snapshots` | | Go | none — tag push `golang/vX.Y.Z` | idempotent, skips | | crates.io | `CARGO_REGISTRY_TOKEN`, `prod` env, gated by `ENABLE_CRATES` (first publish cannot use Trusted Publishing; switch later) | rejects; the leg checks the crates.io API first and skips | ## Report honestly State which registries are confirmed **from the registry**, which are still indexing, and which failed and why. A partial release is a partial release; say so, and say what a consumer of the lagging port gets in the meantime.