Open a release PR for the version bump instead of pushing it to main

Merged
JE
4h ago
+647-86

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:

  1. make release VERSION=X.Y.Z runs the same validation and make release-check as before. When main isn't stamped with that version, it stamps all three files on release/vX.Y.Z (still through scripts/update-nix-flake.sh and both plugin stampers), pushes that branch, opens a "Prepare vX.Y.Z release" PR labelled release, switches back to main and stops.
  2. Once the PR is merged, the same command run from an up-to-date main finds all three files stamped with exactly that version, creates the annotated tag at HEAD and 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 HEAD is refused. An already-open release PR gets pointed at instead of opened twice. A release/vX.Y.Z left on origin with no open PR is refused, not pushed over. A failure partway through restores the metadata and returns to main, and a rejected tag push deletes the local tag.
  • make release DRY_RUN=1 had been passing DRY_RUN=1 into release-check through MAKEFLAGS, which env -u can't reach, and test-sync-skills refuses that value. The nested make now gets DRY_RUN= on its command line.
  • I created a new release label 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 help line and a comment in the release workflow are updated to match. AGENTS.md and .claude/CLAUDE.md have 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 HEAD is refused, and an already-open release PR is pointed at instead of opened twice.
  • The release commit is stamped on a detached HEAD and pushed as the branch, so a local release/vX.Y.Z from 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=1 no longer leaks DRY_RUN=1 into release-check through MAKEFLAGS.
  • A new release label keeps the release PR out of the generated notes.
  • Adds e2e/release.bats with 17 cases that run the script against a local bare origin and assert exactly which refs reach it.