Skip to content

ci: release the CLI from a tag - #76

Merged
jiashuoz merged 2 commits into
mainfrom
ci/cli-release-workflow
Sep 10, 2026
Merged

ci: release the CLI from a tag#76
jiashuoz merged 2 commits into
mainfrom
ci/cli-release-workflow

Conversation

@jiashuoz

Copy link
Copy Markdown
Member

Push v<semver>, get a release. Replaces the eleven-step manual sequence that produced v0.0.10.

What it does

Build four platform archives → publish the GitHub release → compute the pins from the uploaded artifacts → publish the npm wrapper → set beta and latest → open a PR recording the pins on main.

The ordering the release depends on is enforced rather than remembered: archives are uploaded before the wrapper that pins them is published, so the wrapper can only ever describe bytes that are actually downloadable.

Gates

Each one guards a failure the manual path allowed:

Gate Prevents
make verify + race checks on the tagged tree Publishing an unverified tree
Registry pre-check A half-finished release — npm forbids overwriting a version, and finding that out after the GitHub release exists leaves a mess
Binary reports the tag's version; help ≤ 24 lines Pinning correct hashes to the wrong build
Exactly six allowlisted files, no deps, no lifecycle scripts A supply-chain change riding along in a release
Install from the registry and run A wrapper that passes tests but fails the real download or checksum gate

Two bugs from the manual path, fixed

latest is set explicitly every release. Left alone it stays wherever npm first put it — which is how it came to sit on 0.0.3 while beta moved to 0.0.9. Anyone running a bare npm install @tokencanopy/rainier today gets a build from before the hosted v0 surface existed.

The version rewrite matches the outgoing version literally, not a 0.0.x pattern that would silently stop matching at 0.1.0 — the same quiet drift this workflow exists to end.

Archives use deterministic tar metadata, so identical source yields identical hashes and a pin can be reproduced.

Publishing: OIDC, not a token

npm is restricting tokens that bypass 2FA for direct publishing, and the manual publish of 0.0.10 failed on EOTP. Trusted Publishing has no one-time password and stores no secret, and adds provenance attestation. NPM_TOKEN is still honored as a fallback.

One-time setup required before the first automated release: add this repository and release-cli.yml as a trusted publisher for @tokencanopy/rainier on npmjs.com.

Verification

The workflow cannot run until it is on main and a tag is pushed, so it is not yet exercised end to end. What was verified:

  • The release.js generator reproduces v0.0.10's published manifest byte-for-byte.
  • The version rewrite runs clean against the real 0.0.9 → 0.0.10 case with no stale references left.
  • The package-contents check passes against the actual package.
  • Tag parsing accepts v0.0.10, v1.2.3, v0.0.11-rc.1 and rejects 0.0.10 and vfoo.
  • YAML parses; 17 steps.

🤖 Generated with Claude Code

https://claude.ai/code/session_019SDSieWT6mnHB1EYXUyz7H

jiashuoz and others added 2 commits September 9, 2026 17:33
Cutting v0.0.10 by hand took eleven steps: cross-compile four targets with a
recipe recovered from the previous release's BUILDINFO, tar them, hash the
archives and the binaries inside them, create the release, upload six assets,
hand-write the eight hashes into npm/release.js, bump four files that name the
version, publish, and move the dist-tags. Every one of them is a place to get
a hash wrong or forget a file, and the whole sequence lives in whoever last
did it.

Pushing `v<semver>` now does it. The ordering the release depends on is
enforced rather than remembered: the archives are uploaded before the wrapper
that pins them is published, and the pins are computed from the uploaded
artifacts, so the wrapper can only ever describe bytes that are actually
downloadable.

Gates, each guarding a failure the manual path allowed:

- `make verify` and the race checks run against the tagged tree; an unverified
  tree is never published.
- A version already on the registry stops the run before the GitHub release
  exists, rather than after, since npm forbids overwriting one.
- The built binary must report the tag's version and keep its help within one
  screen, so correct hashes cannot be pinned to the wrong build.
- The package must contain exactly the six allowlisted files.
- The published package is installed from the registry and run, exercising the
  download and both checksum gates a user hits.

Two things the manual path got wrong are fixed here. `latest` is set
explicitly on every release: left alone it stays wherever npm first put it,
which is how it came to sit on 0.0.3 while beta moved to 0.0.9. And the
outgoing version is rewritten literally rather than by a `0.0.x` pattern that
would silently stop matching at 0.1.0.

Archives are built with deterministic tar metadata, so identical source
produces identical hashes and a pin can be reproduced.

Publishing uses npm Trusted Publishing over OIDC. npm is restricting tokens
that bypass 2FA for direct publishing, and the manual publish of 0.0.10 failed
on EOTP; OIDC has no one-time password and stores no secret. `NPM_TOKEN` is
still honored as a fallback.

Not yet exercised end to end: the workflow cannot run until it is on main and
a tag is pushed. The release.js generator was verified separately by
reproducing v0.0.10's published manifest byte-for-byte, and the version
rewrite, package-contents check, and tag parsing were each run against the
real 0.0.9 to 0.0.10 case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SDSieWT6mnHB1EYXUyz7H
The final step opens a PR recording the published pins on main. Without
pull-requests: write the token is read-only for that API and gh pr create
fails with 403 — after the release and the npm publish have already
succeeded, which is the worst place to lose the record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SDSieWT6mnHB1EYXUyz7H
@jiashuoz
jiashuoz merged commit a2c09f1 into main Sep 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant