Skip to content

docs(RELEASING): promote with a merge, not a rebase — the rebase is what mints twin trees - #370

Merged
VijitSingh97 merged 5 commits into
developfrom
docs/ff-only-promotion
Aug 16, 2026
Merged

docs(RELEASING): promote with a merge, not a rebase — the rebase is what mints twin trees#370
VijitSingh97 merged 5 commits into
developfrom
docs/ff-only-promotion

Conversation

@VijitSingh97

Copy link
Copy Markdown
Contributor

Follow-up to the v1.15.1 cut, which cost a hand-built reconcile commit for the third release running.

The bug in the process

Step 6 prescribed gh pr merge --rebase --admin. That rebases develop's commits onto main, minting new shas — so main carries twins of commits develop still holds under their original shas, and the branches stop sharing recent ancestry.

Measured before the fix:

Last shared ancestor 3220f57, 2026-07-19 — three releases ago
Commits on main not in develop 37 (all twins)
Commits on develop not in main 44
Cost per release promotion PR CONFLICTING, hand-built reconcile commit (cfd92fa v1.15.0, 60aa883 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. The process failed its own stated goal.

The fix

de4e781 on develop healed the divergence with git merge -s ours origin/main: develop's tree kept byte-for-byte, main recorded as a parent. The -s ours assertion that main's changes were already incorporated was verified true first — git diff origin/main origin/develop showed only the v1.15.1 perf baseline, i.e. main carried nothing develop lacked. No history rewritten, no force-push, no content change.

Divergence went 37 → 0, and main is an ancestor of develop again.

This PR corrects step 6 to --merge (which now fast-forwards cleanly onto develop's release commit, actually delivering what the doc wanted), and records:

  • the invariant — main is always an ancestor of develop — with the git merge-base --is-ancestor check to run before promoting;
  • the back-merge to restore it if a hotfix ever lands on main directly;
  • why --rebase must not come back, so the next releaser doesn't reintroduce it.

Docs only; make lint-md clean.

VijitSingh97 and others added 2 commits August 15, 2026 20:28
…hat mints twin trees

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 <noreply@anthropic.com>
…button

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 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Validation catch on my own change — pushed 6b347e3.

The first version of this PR said gh pr merge --merge --admin # fast-forwards main to develop. It does not. GitHub's --merge always writes a merge commit, verified against this repo's own history rather than from memory:

promotion parents
23fcd27 release: v1.12.0 (promote develop to main via merge) 2
22dd8f2 release: v1.11.2 (promote develop to main via merge) 2
9b04a37 release: v1.11.0 (promote develop to main via merge) 2

So --merge would have been wrong in two ways at once: the tag would sit on the merge commit instead of develop's release commit — the exact goal step 6 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 verify, one commit per release. I'd have shipped a doc that contradicted itself.

Corrected to a real fast-forward push, which is available now that de4e781 made main an ancestor again:

git merge-base --is-ancestor origin/main origin/develop || { echo 'NOT a fast-forward'; exit 1; }
git push origin develop:main

Verified by simulation on a throwaway ref: git merge --ff-only origin/develop from origin/main lands on develop's exact sha, not merely the same tree. GitHub closes the PR as merged once its commits are reachable from main.

Also re-checked the rest of the doc's claims rather than assuming: the git merge-base --is-ancestor command it prescribes exits 0 today, and all four cited shas resolve to what the text says they are (3220f57 the last shared ancestor, cfd92fa/60aa883 the two reconcile commits, de4e781 the heal).

…otion push

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 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Retracting the evidence table in my previous comment — pushed 0ccbfd0.

I cited 23fcd27 / 22dd8f2 / 9b04a37 (all two-parent) as proof that --merge refuses to fast-forward. That proves nothing. All three of those promotions happened when main and develop had already diverged, so a merge commit was the only possible outcome — the test is whether --merge writes one when a fast-forward is available, and those commits cannot answer it:

23fcd27  first-parent=22dd8f2  second=3220f57  ->  FF was NOT possible
22dd8f2  first-parent=ee4dd10  second=3070d99  ->  FF was NOT possible
9b04a37  first-parent=32cba4f  second=59f078d  ->  FF was NOT possible

The conclusion still holds (GitHub's --merge is --no-ff by design), but it's GitHub's documented semantics, not something this repo's history demonstrates. Stated that way now.

Second correction: I implied no tag had ever sat on develop's release commit. v1.13.0 and v1.14.0 did — the regression begins at v1.15.0, when the rebase promotion came in. (My check for this was also methodologically unsound after de4e781, since develop now contains main's history through the merge parent.)

Third, and the one that actually mattered operationally: the previous version hand-waved the push as "--admin-style bypass applies". The real config, verified:

Ruleset Main Branch, enforcement active
Rules pull_request, non_fast_forward, deletion
Bypass OrganizationAdmin, bypass_mode: always

So the push satisfies non_fast_forward (that rule blocks force-pushes; this is a real fast-forward) and needs the OrganizationAdmin bypass for pull_request. I have not tested that push against the ruleset — doing so would mean pushing to main outside a release, which would drag post-release commits onto it. It's marked unverified in the doc for the next releaser to confirm, rather than presented as known-good.

Net: the direction is unchanged and the healing (de4e781, 37 → 0) stands on its own. What changed is that the doc now claims only what was actually checked.

…d a false bypass claim

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 <noreply@anthropic.com>
@VijitSingh97

Copy link
Copy Markdown
Contributor Author

Third pass — four more errors in my own text, pushed 7b61994. Summary of what changed and why, since I've now corrected this twice and got one of the corrections wrong.

# Claim I made Reality
1 "three releases" while diverged Five — v1.13.0, v1.13.1, v1.14.0, v1.15.0, v1.15.1
2 --rebase started the divergence It started one release earlier, at v1.12.0's --merge promotion. 23fcd27 has two parents and its second parent 3220f57 is the merge-base — main took a merge commit develop never got back. --rebase compounded it into twins.
3 "v1.13.0/v1.14.0 did land their tags on the release commit" False, and it was my own correction that broke it. I ran that check 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; the "fix" introduced the error.
4 the push uses "the same bypass direct pushes to develop lean on" False. The Main Branch ruleset targets refs/heads/main only; develop carries **no rules at all**, so pushes there bypass nothing. The fast-forward push would be the *first* thing to rely on that bypass — no precedent — so it's marked untested and now carries a concrete fallback (--merge --admin` + back-merge).

Because of #2, the doc now leads with the invariant (main stays an ancestor of develop) rather than with a merge mode — both --merge and --rebase break it, and the repo has been broken by each in turn.

Also fixed: a duplicated sentence, and an internal contradiction where the step opened with "not a direct push" and then prescribed one.

One extra fix in the same file, outside the original scope: the perf-baseline paragraph still promised the recording "means miner-0 every time, since the release gate itself always runs there". That is false and would send the next releaser to a box that cannot pass — v1.15.0 was gated on miner-2, v1.15.1 on miner-3, and miner-0 can't pass at all while Secure Boot is on for its Windows dual-boot (lockdown integrity denies MSR writes → doctor exits non-zero). Corrected to "whichever rig ran the gate — pick one with Secure Boot off".

Audit of the pushed file: all five cited shas resolve to what the text says they are, all eight version strings are real tags, markdownlint clean, no test asserts on this file, all relative links resolve.

…0 correction

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 <noreply@anthropic.com>
@VijitSingh97
VijitSingh97 merged commit 79747b8 into develop Aug 16, 2026
9 checks passed
@VijitSingh97
VijitSingh97 deleted the docs/ff-only-promotion branch August 16, 2026 02:02
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