| name | release |
| description | Cut an r3 release โ draft the CHANGELOG entry, bump every version string in lockstep, commit, then cut the annotated `vX.Y.Z` tag on that commit (the release CI lifts the changelog section into the GitHub release notes). Use when the user wants to release a new version, bump the version, write or update the changelog, or tag a release. |
Releasing r3
A release is one version-bump commit plus an annotated tag on it. The whole job
is: write the changelog, move every version string to the new number
together, commit, tag that commit, and push. The release CI reads
CHANGELOG.md out of the tagged tree for the GitHub release notes, so the
changelog entry is the release's public face and the tag carries no notes of its
own.
The one rule: bump before you tag
The tag must point at the version-bump commit. Cutting the tag first and bumping
afterward leaves the tag on a commit whose in-code version is still the previous
release โ this skill exists because that exact mistake happened once. Do the
changelog and the version bump in the release commit, then tag that commit.
Version sources โ all must read the same number
Three files hold the version. The release-build scripts
(scripts/release-binaries.ts, scripts/stage-npm-packages.ts) refuse to build
if they drift, so keep them in lockstep:
shared/version.ts โ R3_VERSION (baked into the binary + CLI, reported
by /api/health; the CLI warns on skew).
package.json โ top-level "version".
npm/package.json โ "version" and all four optionalDependencies
pins: @hyperlogue/r3-darwin-arm64, -darwin-x64, -linux-x64,
-linux-arm64. All four must equal the new version (the launcher resolves the
matching per-platform binary at exactly its own version).
Steps
-
Pick the version (SemVer). Previous tag: git describe --tags --abbrev=0.
-
Draft the CHANGELOG entry (CHANGELOG.md, Keep a Changelog format).
Survey what shipped since the last tag โ git log v<prev>..HEAD --oneline โ
then:
- Insert a new
## [X.Y.Z] - YYYY-MM-DD section directly under the intro
block, above the previous version. Use the release date.
- Group bullets under
### Added / ### Changed / ### Fixed / ### Removed
โ only the groups that apply, in that order.
- Write from the user's vantage point: what they can now do, or no longer
run into โ not the internal mechanics, refactors, or scaffolding that got it
there. Frame each entry as what shipped, not what was turned off or reworked
mid-development; internal churn a user never observes doesn't belong in the
log at all. Match the existing voice: a bold lead-in (
**Feature.**) then a
sentence or two on the change and why it matters; fold related commits into
one bullet.
- Add the compare link at the very bottom, with the others:
[X.Y.Z]: https://github.com/hyperlogue/r3/compare/v<prev>...vX.Y.Z
This entry is the release's public face โ the CI publishes this exact section
as the GitHub release notes. Show the draft to the user and get their
sign-off before you commit.
-
Bump all three version sources to X.Y.Z โ do not forget the four npm
pins in npm/package.json.
-
Verify they agree, then run the checks:
grep -rn '"version"\|R3_VERSION\|@hyperlogue/r3-' \
package.json npm/package.json shared/version.ts
bun run typecheck
biome check .
-
Commit โ Conventional Commit, and keep the Co-Authored-By: Claude โฆ
trailer (this repo uses it; see AGENTS.md):
git add CHANGELOG.md shared/version.ts package.json npm/package.json
git commit -m "chore: release vX.Y.Z"
-
Tag โ annotated, on the commit you just made (the r3 vX.Y.Z subject is
the convention since v0.2.0). The tag needs no body: CI reads the changelog
section from the tagged tree. The awk is the same extraction CI runs, used
here only as a guard โ it refuses to tag when the ## [X.Y.Z] section is
missing or empty, which is exactly when the release would silently fall back
to GitHub's auto-generated notes:
V=X.Y.Z
NOTES=$(awk -v v="$V" 'index($0, "## [" v "]")==1{f=1; next} /^## \[/{f=0} f' CHANGELOG.md | sed '/./,$!d')
test -n "$NOTES" &&
git tag -a "v$V" -m "r3 v$V" ||
echo "refusing to tag: no populated '## [$V]' section in CHANGELOG.md" >&2
Eyeball what CI will publish before pushing โ that's $NOTES, or
git show "v$V":CHANGELOG.md | head -40.
-
Push โ leave the actual push to the user unless they ask, and note that
this environment often has no push credentials (SSH key / gh auth may be
absent โ surface that instead of silently failing):
git push origin main && git push origin vX.Y.Z
After the tag lands on GitHub
The tag-driven pipeline (.github/workflows/release.yml) cross-compiles the four
r3-<os>-<arch> binaries (GitHub Release: curl / Homebrew) and publishes the npm
launcher (@hyperlogue/r3) with its per-platform optional-dependency packages.
It fills the release description from the ## [X.Y.Z] section of
CHANGELOG.md in the tagged tree โ step 2's entry, verbatim โ
falling back to GitHub's auto-generated notes only if that section is
missing or empty (the version guard warns when it is). This is why the tag must
sit on the bump commit: a tag one commit early carries a changelog that doesn't
describe it yet. The pins were already synced in step 3, so there is nothing else
to bump by hand.
If you botch a release
Immutable releases are enabled, so a published vX.Y.Z tag can't be moved or
deleted through the normal path โ which is the whole reason steps 1โ6 get it right
the first time (bump before tag, changelog into the tag). Recovery depends on how
far it got:
- Not pushed yet โ the tag is still local. Delete and recut it:
git tag -d vX.Y.Z, fix, then redo step 6. Cheap.
- Already pushed / released โ don't fight the immutability. Cut the next
patch version (
vX.Y.(Z+1)) carrying the fix; that is the intended recovery.
Force-moving a published tag is a rare escape hatch that needs the user to
temporarily lift GitHub's immutable-tag / release protection, then git tag -f -a vX.Y.Z -m "r3 vX.Y.Z" <commit> and git push --force origin vX.Y.Z. It's
outward-facing and hard to reverse โ confirm with the user first, and if the
branch and tag can't both push, stop and report rather than half-applying.