Open a release PR for the version bump instead of pushing it to main
make release pushed its release-prep commit (the version in nix/package.nix, .claude-plugin/plugin.json and .codex-plugin/plugin.json) straight to main through the ruleset bypass before tagging. 5438c380 for v0.27.0 is the latest one. This change sends that bump through a PR instead, the way hey-cli's release.sh already does.
The new flow
A stable release is two runs of the same command:
make release VERSION=X.Y.Zruns the same validation andmake release-checkas before. Whenmainisn't stamped with that version, it stamps all three files onrelease/vX.Y.Z(still throughscripts/update-nix-flake.shand both plugin stampers), pushes that branch, opens a "Prepare vX.Y.Z release" PR labelledrelease, switches back tomainand stops.- Once the PR is merged, the same command run from an up-to-date
mainfinds all three files stamped with exactly that version, creates the annotated tag atHEADand pushes only the tag. The existing release workflow takes it from there.
The script never pushes main and never runs gh pr merge, --admin or auto-merge. Prereleases leave the metadata alone, so they still tag HEAD in one run, as before. DRY_RUN=1 / --dry-run runs every check and reports which step a real run would take, without creating a branch, PR or tag.
Also:
- The tag check now runs before the release checks. A tag already at an unstamped
HEADis refused. An already-open release PR gets pointed at instead of opened twice. Arelease/vX.Y.Zleft on origin with no open PR is refused, not pushed over. A failure partway through restores the metadata and returns tomain, and a rejected tag push deletes the local tag. make release DRY_RUN=1had been passingDRY_RUN=1intorelease-checkthroughMAKEFLAGS, whichenv -ucan't reach, and test-sync-skills refuses that value. The nested make now getsDRY_RUN=on its command line.- I created a new
releaselabel in this repo and excluded it in.github/release.yml, so the release PR stays out of the generated notes. - RELEASING.md now describes the two runs. The
make helpline and a comment in the release workflow are updated to match. AGENTS.md and.claude/CLAUDE.mdhave no release instructions, so they're unchanged.
Tests
e2e/release.bats (17 cases) runs the real script and both stampers against a local bare origin, with gh and the Docker-backed Nix updater stubbed. A pre-receive hook records every ref pushed, so each case asserts exactly what reached origin and in what order. Against the old release.sh, 15 of the 17 fail. The two that still pass, the prerelease case and the wrong-branch refusal, test behaviour the change keeps. TestReleaseWiringSkipsPluginStampsForPrereleases now checks the stampers live only in open_release_pr, which runs only after the prerelease check.
Summary by cubic
make release no longer pushes the release-prep version bump (in nix/package.nix and both plugin manifests) straight to main. A stable release is now two runs of the same command: the first stamps the metadata on release/vX.Y.Z, opens a "Prepare vX.Y.Z release" PR, and stops; once merged, the second run finds main stamped and pushes only the tag. The script never pushes main and never auto-merges. Prereleases leave the metadata alone and still tag in one run.
Behavior changes
- The tag is validated before release checks run; a tag at an unstamped
HEADis refused, and an already-open release PR is pointed at instead of opened twice. - The release commit is stamped on a detached
HEADand pushed as the branch, so a localrelease/vX.Y.Zfrom an earlier attempt is never reset. - The run after the PR merges verifies the Nix flake at the commit about to be tagged; a flake that would change blocks the tag and asks for a corrective PR.
- A failed stamp or commit restores the metadata and returns to
main; a rejected tag push removes the local tag. make release DRY_RUN=1no longer leaksDRY_RUN=1intorelease-checkthroughMAKEFLAGS.- A new
releaselabel keeps the release PR out of the generated notes. - Adds
e2e/release.batswith 17 cases that run the script against a local bare origin and assert exactly which refs reach it.