From 13d468e80ca1154f293b2153fcc3e143d1b6c5f0 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 15 Aug 2026 20:28:10 -0500 Subject: [PATCH 1/5] =?UTF-8?q?docs(RELEASING):=20promote=20with=20a=20mer?= =?UTF-8?q?ge,=20not=20a=20rebase=20=E2=80=94=20the=20rebase=20is=20what?= =?UTF-8?q?=20mints=20twin=20trees?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 6 prescribed `gh pr merge --rebase`, which rebases develop's commits onto main and mints new shas. develop keeps the originals, so main accumulated TWINS and the branches stopped sharing recent ancestry — 37 commits apart over three releases, with every promotion PR coming back CONFLICTING and needing a hand-built reconcile commit (cfd92fa at v1.15.0, 60aa883 at v1.15.1). The rebase also defeated the reason the doc gave for choosing it: 'so the tag sits on the same commit as develop's release commit'. A rebase mints a twin, so the tag landed on a different sha that merely shared the tree. de4e781 healed the divergence (`merge -s ours`, develop's tree kept byte-for-byte), so main is an ancestor of develop again and `--merge` now fast-forwards cleanly. This records the corrected step plus the invariant to check before promoting, and the back-merge to run if a hotfix ever lands on main directly. Co-Authored-By: Claude Opus 5 --- RELEASING.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 2578b9f..8e1a6b4 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -78,13 +78,24 @@ promoted to `main` and tagged. The steps below build the release commit on `deve --body "Promote develop to main for the vX.Y.Z release." ``` - Review and merge it. Keep `main` linear with a **fast-forward (rebase) merge** so the tag sits on the - same commit as `develop`'s release commit: + Review and merge it with a **merge commit**, which fast-forwards `main` onto `develop`'s release + commit — so the tag sits on that exact commit and `main` stays linear: ```bash - gh pr merge --rebase --admin # fast-forward main to develop; --admin lets the releaser merge + gh pr merge --merge --admin # fast-forwards main to develop; --admin lets the releaser merge ``` + > **Do not promote with `--rebase`.** It *rebases* develop's commits onto `main`, minting new shas — + > so `main` ends up carrying **twins** of commits `develop` still holds under their original shas, and + > the two branches share no recent ancestry. That defeats the very goal of putting the tag on + > develop's release commit (the tag lands on the twin, which only shares the *tree*), and it makes + > every later promotion PR come back `CONFLICTING`, needing a hand-built reconcile commit. It drifted + > to 37 twin commits over three releases before being healed in `de4e781`; `cfd92fa` and `60aa883` are + > the reconcile commits it cost. The invariant to preserve is **`main` is always an ancestor of + > `develop`** — verify with `git merge-base --is-ancestor origin/main origin/develop` before promoting. + > If a hotfix ever lands directly on `main`, back-merge it (`git merge origin/main` on `develop`) to + > restore the invariant before the next release. + 7. Tag and push from `main` (annotated tag, matching `VERSION`) once the PR is merged: ```bash From 6b347e3be64ae47e91a3d418cebd34f4e8f7ff1b Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 15 Aug 2026 20:31:30 -0500 Subject: [PATCH 2/5] docs(RELEASING): the promotion is a fast-forward push, not the merge button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validation catch on my own change: I had written `gh pr merge --merge` with the comment 'fast-forwards main to develop'. It does not. GitHub's --merge ALWAYS writes a merge commit — verified against this repo's own history, where every past 'promote via merge' on main (23fcd27, 22dd8f2, 9b04a37) has two parents. That matters twice over: the tag would sit on the merge commit rather than on develop's release commit (the goal the step exists to serve), and main would gain a commit develop lacks, breaking the 'main is an ancestor of develop' invariant the same paragraph tells releasers to check — one commit per release. Since de4e781 made main an ancestor again, a real fast-forward is available and is what the step now prescribes: `git push origin develop:main`, guarded by the is-ancestor check. Verified a ff-only onto develop lands on develop's exact sha. GitHub closes the PR as merged once its commits are reachable from main. Co-Authored-By: Claude Opus 5 --- RELEASING.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 8e1a6b4..6e90380 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -78,14 +78,24 @@ promoted to `main` and tagged. The steps below build the release commit on `deve --body "Promote develop to main for the vX.Y.Z release." ``` - Review and merge it with a **merge commit**, which fast-forwards `main` onto `develop`'s release - commit — so the tag sits on that exact commit and `main` stays linear: + Review it, then complete the promotion with a **fast-forward push** so `main` lands on `develop`'s + release commit *exactly* — same sha, not just the same tree — and stays linear. GitHub closes the PR + as merged once its commits are reachable from `main`: ```bash - gh pr merge --merge --admin # fast-forwards main to develop; --admin lets the releaser merge + git fetch origin + git merge-base --is-ancestor origin/main origin/develop \ + || { echo "NOT a fast-forward — main has commits develop lacks; back-merge first (see below)"; exit 1; } + git push origin develop:main # fast-forward; --admin-style bypass applies to the protected branch ``` - > **Do not promote with `--rebase`.** It *rebases* develop's commits onto `main`, minting new shas — + > **Don't finish this with the merge button.** Neither `gh pr merge` mode gives a fast-forward. + > `--merge` always writes a *merge commit* (every past "promote via merge" on `main` — `23fcd27`, + > `22dd8f2`, `9b04a37` — has two parents), so the tag would sit on that commit rather than on + > develop's release commit, and `main` would gain a commit `develop` lacks, breaking the invariant + > below one commit per release. + > + > **And never promote with `--rebase`.** It *rebases* develop's commits onto `main`, minting new shas — > so `main` ends up carrying **twins** of commits `develop` still holds under their original shas, and > the two branches share no recent ancestry. That defeats the very goal of putting the tag on > develop's release commit (the tag lands on the twin, which only shares the *tree*), and it makes From 0ccbfd08addfdb0d4201e5eea055a4787610ad4d Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 15 Aug 2026 20:34:48 -0500 Subject: [PATCH 3/5] docs(RELEASING): claim only what was actually verified about the promotion push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second validation pass, after being told to double-check. Two things I asserted were not supported by the evidence I cited: 1. I claimed --merge's always-writes-a-merge-commit behaviour was 'verified against this repo's history' via 23fcd27 / 22dd8f2 / 9b04a37. Those three promotions all happened when main and develop had ALREADY diverged, so a merge commit was the only possible outcome — they prove nothing about what GitHub does when a fast-forward IS available. Dropped the citation; the behaviour is stated as GitHub's semantics. 2. I implied no tag had ever sat on develop's release commit. v1.13.0 and v1.14.0 did. The regression starts at v1.15.0, where the rebase promotion came in. Corrected. Also records what the ruleset actually is, which the previous version hand-waved as '--admin-style bypass': Main Branch carries pull_request + non_fast_forward + deletion, with OrganizationAdmin bypass at bypass_mode 'always'. The fast-forward push satisfies non_fast_forward and bypasses pull_request. That push shape is NOT yet tested against the ruleset, so it is marked unverified for the next releaser to confirm rather than presented as known-good. Co-Authored-By: Claude Opus 5 --- RELEASING.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 6e90380..c96b666 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -86,14 +86,22 @@ promoted to `main` and tagged. The steps below build the release commit on `deve git fetch origin git merge-base --is-ancestor origin/main origin/develop \ || { echo "NOT a fast-forward — main has commits develop lacks; back-merge first (see below)"; exit 1; } - git push origin develop:main # fast-forward; --admin-style bypass applies to the protected branch + git push origin develop:main ``` - > **Don't finish this with the merge button.** Neither `gh pr merge` mode gives a fast-forward. - > `--merge` always writes a *merge commit* (every past "promote via merge" on `main` — `23fcd27`, - > `22dd8f2`, `9b04a37` — has two parents), so the tag would sit on that commit rather than on - > develop's release commit, and `main` would gain a commit `develop` lacks, breaking the invariant - > below one commit per release. + The `Main Branch` ruleset carries `pull_request`, `non_fast_forward` and `deletion`. This push + satisfies `non_fast_forward` — that rule blocks force-pushes, and this is a genuine fast-forward — + and bypasses `pull_request` as `OrganizationAdmin` (`bypass_mode: always`), the same bypass the + documented direct pushes to `develop` already lean on; expect a "Bypassed rule violations" warning. + GitHub closes the PR once its commits are reachable from `main`. **This push shape is unverified — + confirm it on the next promotion and correct this step if the bypass does not cover it.** + + > **Don't finish this with the merge button.** GitHub's `--merge` writes a merge commit even when a + > fast-forward is available, so the tag would sit on that commit rather than on develop's release + > commit — the goal this step exists to serve — and `main` would gain a commit `develop` lacks, + > breaking the invariant below by one commit per release. (`--squash` is worse: a fresh sha and a + > flattened history.) v1.13.0 and v1.14.0 did land their tags on the release commit; v1.15.0 was + > where the rebase promotion broke that, and v1.15.1 inherited it. > > **And never promote with `--rebase`.** It *rebases* develop's commits onto `main`, minting new shas — > so `main` ends up carrying **twins** of commits `develop` still holds under their original shas, and From 7b619947e6f5e4ff58457986ade5330830e4cecc Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 15 Aug 2026 20:43:01 -0500 Subject: [PATCH 4/5] =?UTF-8?q?docs(RELEASING):=20third=20correction=20pas?= =?UTF-8?q?s=20=E2=80=94=20fix=20the=20count,=20the=20cause,=20and=20a=20f?= =?UTF-8?q?alse=20bypass=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Told to keep checking. Four more errors found in my own text: 1. 'three releases' while diverged -> FIVE (v1.13.0, v1.13.1, v1.14.0, v1.15.0, v1.15.1). Verified by listing every tag dated after the merge-base. 2. Wrong cause. I blamed --rebase for starting it. The split began one release earlier, at v1.12.0's MERGE promotion: 23fcd27 has two parents and its second parent 3220f57 IS the merge-base, so main took a merge commit develop never got back. --rebase then compounded it into twins. Both modes break the same invariant, which is why the invariant — not the merge mode — now leads. 3. I had 'corrected' my earlier claim to say v1.13.0/v1.14.0 landed their tags on develop's release commit. They did not. That check was run against POST-heal develop, which now contains main's history through the merge parent, so it compared a commit to itself. Against pre-heal develop (7ea7e78) no tag from v1.12.0 to v1.15.1 is reachable — my original claim was right and the 'correction' introduced the error. 4. 'the same bypass the documented direct pushes to develop already lean on' is false: the Main Branch ruleset targets refs/heads/main ONLY and develop carries no rules at all, so pushes there bypass nothing. That makes the fast-forward push the FIRST thing to rely on that bypass — no precedent — so the untested marker now says so and carries a concrete fallback. Also removed a duplicated sentence and an internal contradiction (the step opened with 'not a direct push' and then prescribed one), and fixed the stale perf-baseline paragraph in the same file, which still promised miner-0 gates 'every time' — v1.15.0 was gated on miner-2, v1.15.1 on miner-3, and miner-0 cannot pass at all while Secure Boot is on for its Windows dual-boot. Co-Authored-By: Claude Opus 5 --- RELEASING.md | 71 +++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index c96b666..d5f5e61 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -70,17 +70,17 @@ promoted to `main` and tagged. The steps below build the release commit on `deve git push origin develop ``` -6. Promote `develop` to `main` **through a pull request** — `main` is a protected release branch, so the - promotion goes through a reviewable PR (its own gate + audit trail), not a direct push: +6. Promote `develop` to `main`. Open a pull request first — it carries the review, the CI run and the + audit trail for the promotion, and `main`'s ruleset requires one: ```bash gh pr create --base main --head develop --title "release: vX.Y.Z" \ --body "Promote develop to main for the vX.Y.Z release." ``` - Review it, then complete the promotion with a **fast-forward push** so `main` lands on `develop`'s - release commit *exactly* — same sha, not just the same tree — and stays linear. GitHub closes the PR - as merged once its commits are reachable from `main`: + Review it, then land it with a **fast-forward push** rather than the merge button, so `main` ends up + on `develop`'s release commit *exactly* — same sha, not merely the same tree — and stays linear. + GitHub closes the PR as merged once its commits are reachable from `main`: ```bash git fetch origin @@ -89,29 +89,34 @@ promoted to `main` and tagged. The steps below build the release commit on `deve git push origin develop:main ``` - The `Main Branch` ruleset carries `pull_request`, `non_fast_forward` and `deletion`. This push - satisfies `non_fast_forward` — that rule blocks force-pushes, and this is a genuine fast-forward — - and bypasses `pull_request` as `OrganizationAdmin` (`bypass_mode: always`), the same bypass the - documented direct pushes to `develop` already lean on; expect a "Bypassed rule violations" warning. - GitHub closes the PR once its commits are reachable from `main`. **This push shape is unverified — - confirm it on the next promotion and correct this step if the bypass does not cover it.** - - > **Don't finish this with the merge button.** GitHub's `--merge` writes a merge commit even when a - > fast-forward is available, so the tag would sit on that commit rather than on develop's release - > commit — the goal this step exists to serve — and `main` would gain a commit `develop` lacks, - > breaking the invariant below by one commit per release. (`--squash` is worse: a fresh sha and a - > flattened history.) v1.13.0 and v1.14.0 did land their tags on the release commit; v1.15.0 was - > where the rebase promotion broke that, and v1.15.1 inherited it. + The `Main Branch` ruleset targets `refs/heads/main` only (`develop` carries no rules at all) and has + `pull_request`, `non_fast_forward` and `deletion`, with `OrganizationAdmin` bypass at + `bypass_mode: always`. The push satisfies `non_fast_forward` — that rule blocks force-pushes, and + this is a genuine fast-forward — and needs the bypass for `pull_request`. + **Untested: no push has yet relied on that bypass, so confirm it on the next promotion. If it is + refused, fall back to `gh pr merge --merge --admin` and then back-merge (`git merge origin/main` on + `develop`) to restore the invariant before the next release.** + + > **The invariant is the point: `main` must stay an ancestor of `develop`.** Both `gh pr merge` + > modes break it, in different ways, and the repo has been broken by each in turn. + > + > `--merge` writes a merge commit onto `main` that `develop` never receives. That is how the last + > divergence started: `23fcd27` ("release: v1.12.0 (promote develop to main via merge)", + > 2026-07-19) has two parents, and its second parent `3220f57` is the last commit the two branches + > shared. Nothing back-merged it, so they never re-converged. + > + > `--rebase` is worse: it *rebases* develop's commits onto `main`, minting new shas, so `main` ends + > up carrying **twins** of commits `develop` still holds under their original shas. Later promotion + > PRs then come back `CONFLICTING` and need a hand-built reconcile commit — `cfd92fa` (v1.15.0) and + > `60aa883` (v1.15.1) are two of those, and PR #368 is a promotion that could not be merged at all. + > + > Five releases were cut while diverged (v1.13.0, v1.13.1, v1.14.0, v1.15.0, v1.15.1), drifting to + > 37 commits on `main` that `develop` lacked, until `de4e781` healed it. In that whole window **no + > tag ever sat on develop's release commit** — every one of v1.12.0…v1.15.1 is unreachable from + > develop as it stood before the heal. A fast-forward is what puts them back on the same commit. > - > **And never promote with `--rebase`.** It *rebases* develop's commits onto `main`, minting new shas — - > so `main` ends up carrying **twins** of commits `develop` still holds under their original shas, and - > the two branches share no recent ancestry. That defeats the very goal of putting the tag on - > develop's release commit (the tag lands on the twin, which only shares the *tree*), and it makes - > every later promotion PR come back `CONFLICTING`, needing a hand-built reconcile commit. It drifted - > to 37 twin commits over three releases before being healed in `de4e781`; `cfd92fa` and `60aa883` are - > the reconcile commits it cost. The invariant to preserve is **`main` is always an ancestor of - > `develop`** — verify with `git merge-base --is-ancestor origin/main origin/develop` before promoting. - > If a hotfix ever lands directly on `main`, back-merge it (`git merge origin/main` on `develop`) to + > Verify with `git merge-base --is-ancestor origin/main origin/develop` before promoting. If a + > hotfix ever lands directly on `main`, back-merge it (`git merge origin/main` on `develop`) to > restore the invariant before the next release. 7. Tag and push from `main` (annotated tag, matching `VERSION`) once the PR is merged: @@ -137,11 +142,13 @@ Pushing the tag triggers the release pipeline After a rig is re-tagged, record its benchmark for the release (`E2E_PERF_TAG=vX.Y.Z E2E_PERF_RECORD=1 sudo bash tests/e2e-real.sh perf` on the rig) and commit the updated `tests/perf-baselines/` files — the per-release history is what lets the perf gate -catch slow drift across releases (see `tests/perf-baselines/README.md`). In practice that means -miner-0 every time, since the release gate itself always runs there (see -[`tests/README.md`](./tests/README.md#the-shared-rig-miner-0)); the rest of the fleet isn't re-tagged -on every release, so its baselines are only as fresh as the last time each rig was actually -touched. `tests/perf-baselines/` legitimately carries gaps between releases for rigs that went +catch slow drift across releases (see `tests/perf-baselines/README.md`). It is whichever rig ran the +gate, which is **not** always miner-0 despite [`tests/README.md`](./tests/README.md#the-shared-rig-miner-0) +calling it the shared rig: v1.15.0 was gated on miner-2 and v1.15.1 on miner-3, and miner-0 currently +cannot pass the gate at all — it dual-boots Windows, so Secure Boot is enabled, kernel lockdown +(`integrity`) denies every MSR write, and `doctor` counts that as an issue and exits non-zero. Pick a +rig with Secure Boot off. The rest of the fleet isn't re-tagged on every release, so its baselines are +only as fresh as the last time each rig was actually touched. `tests/perf-baselines/` legitimately carries gaps between releases for rigs that went untouched — it is not a promise that every rig has an entry for every tag. The recording is also the per-rig perf gate (#214): it judges against the committed baseline and best-ever history before writing, refuses to record a regressed number (fix it, or consciously override with From feab5e5e606a810405bb00a6fac23071ff553d02 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Sat, 15 Aug 2026 20:54:01 -0500 Subject: [PATCH 5/5] docs(RELEASING): re-wrap the perf-baseline paragraph after the miner-0 correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cosmetic follow-up to 7b61994: editing mid-paragraph left a 147-char prose line and then a ragged 32-char one, against the file's ~100-105 wrap. MD013 is disabled so lint never flagged it and the rendered output is identical, but the source was inconsistent with the rest of the document. No wording changes — verified by a word-level diff against develop, which shows only the step 6 rewrite and the miner-0 correction, no phantom edits. Co-Authored-By: Claude Opus 5 --- RELEASING.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index d5f5e61..70979d0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -148,13 +148,14 @@ calling it the shared rig: v1.15.0 was gated on miner-2 and v1.15.1 on miner-3, cannot pass the gate at all — it dual-boots Windows, so Secure Boot is enabled, kernel lockdown (`integrity`) denies every MSR write, and `doctor` counts that as an issue and exits non-zero. Pick a rig with Secure Boot off. The rest of the fleet isn't re-tagged on every release, so its baselines are -only as fresh as the last time each rig was actually touched. `tests/perf-baselines/` legitimately carries gaps between releases for rigs that went -untouched — it is not a promise that every rig has an entry for every tag. The recording is also -the per-rig perf gate (#214): it judges against the committed baseline and best-ever history -before writing, refuses to record a regressed number (fix it, or consciously override with -`E2E_PERF_FORCE=1`), so a failed rig means investigate before calling it healthy. Once a rig's -baseline is merged, reset its copy (`sudo git checkout -- tests/perf-baselines/` in -`/opt/rigforge`): the recording dirties the rig's checkout, and the *next* release's +only as fresh as the last time each rig was actually touched. `tests/perf-baselines/` legitimately +carries gaps between releases for rigs that went untouched — it is not a promise that every rig has +an entry for every tag. The recording is also the per-rig perf gate (#214): it judges against the +committed baseline and best-ever history before writing, refuses to record a regressed number (fix +it, or consciously override with `E2E_PERF_FORCE=1`), so a failed rig means investigate before +calling it healthy. Once a rig's baseline is merged, reset its copy +(`sudo git checkout -- tests/perf-baselines/` in `/opt/rigforge`): the recording dirties the rig's +checkout, and the *next* release's `git checkout ` aborts on exactly those files (this bit both the v1.4.0 and v1.5.0 deploys). To verify a downloaded bundle: `sha256sum -c SHA256SUMS` (see