From 4c8a65861e7120c38e4d1886af8d9d094c201ed8 Mon Sep 17 00:00:00 2001 From: Sall Date: Sun, 26 Jul 2026 21:41:00 +0100 Subject: [PATCH 1/2] docs(runbook): require post-promotion reconciliation --- runbooks/branch-protection.md | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/runbooks/branch-protection.md b/runbooks/branch-protection.md index c1b191717..5dfb5e354 100644 --- a/runbooks/branch-protection.md +++ b/runbooks/branch-protection.md @@ -66,10 +66,13 @@ row is `next` → `main`. Skip repositories that are trunk-on-`main`. branch" condition, so this has to be a CI check wired in as `required_status_checks`. See `.github/workflows/main-branch-guard.yml` in `z-shell/src` or `z-shell/zsh-eza` for the reference implementation - (a single `run:` step reading `github.head_ref`, no third-party - actions needed). The check must run at least once on a real PR against - `main` before GitHub will accept its context name in - `required_status_checks`. + (a single `run:` step, no third-party actions needed). The check must + first require `github.event.pull_request.head.repo.full_name` to equal + `github.repository`, because a fork can reuse an allowed branch name. + It can then allow only `github.head_ref == 'next'` or + `github.head_ref` matching `hotfix-*`. The check must run at least once + on a real PR against `main` before GitHub will accept its context name + in `required_status_checks`. ## Squash-merge trailers @@ -85,6 +88,38 @@ the synthesized body. Verify with `gh api repos///commits/ --jq .commit.message` before considering the promotion done. +## Post-promotion reconciliation + +A squash merge keeps `main` linear but creates a new commit that is not in +`next`, even when the resulting branch trees are identical. If that commit is +not merged back, the next promotion uses an older merge base and can show +already-promoted changes or produce avoidable conflicts. + +Immediately after a successful squash promotion: + +1. Fetch the current `main` and `next`, then verify the promoted `main` tree + matches the reviewed `next` tree: + + ```bash + git fetch origin main next + git diff --exit-code origin/main origin/next + ``` + +2. Create an issue branch from the current `next`, merge `origin/main` with a + signed, non-fast-forward merge commit, and preserve the pre-merge `next` + tree exactly. Open a PR from that issue branch into `next` and merge it + with a merge commit so the `main` parent remains in history. +3. Fetch both branches again and verify `main` is now an ancestor of `next`: + + ```bash + git fetch origin main next + git merge-base --is-ancestor origin/main origin/next + ``` + +Do not reset, rebase, force-push, or replace the persistent `next` branch to +perform this reconciliation. If the branch trees differ before the back-merge, +stop and review the delta instead of resolving it as an ancestry-only change. + ## Reference ruleset shape Both `main` and `next` should be a single Repository Ruleset each, scoped by From 0f44c31a260ae50da332f6e9960aceff8692b68d Mon Sep 17 00:00:00 2001 From: Sall Date: Sun, 26 Jul 2026 21:50:50 +0100 Subject: [PATCH 2/2] docs(runbook): clarify reconciliation commands --- runbooks/branch-protection.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/runbooks/branch-protection.md b/runbooks/branch-protection.md index 5dfb5e354..db4727293 100644 --- a/runbooks/branch-protection.md +++ b/runbooks/branch-protection.md @@ -105,10 +105,22 @@ Immediately after a successful squash promotion: git diff --exit-code origin/main origin/next ``` -2. Create an issue branch from the current `next`, merge `origin/main` with a - signed, non-fast-forward merge commit, and preserve the pre-merge `next` - tree exactly. Open a PR from that issue branch into `next` and merge it - with a merge commit so the `main` parent remains in history. +2. Create the signed merge commit on an issue branch from the current `next` + and verify it preserves the pre-merge `next` tree: + + ```bash + next_tree=$(git rev-parse 'origin/next^{tree}') + git switch -c origin/next + git merge --no-ff -S origin/main \ + -m "chore: reconcile promoted main into next (#)" + test "$(git rev-parse 'HEAD^{tree}')" = "$next_tree" + git push -u origin + ``` + + Open a PR from `` into `next`. Merge it with **Create a merge + commit** so the `main` parent remains in history. Do not squash or rebase + this reconciliation PR. + 3. Fetch both branches again and verify `main` is now an ancestor of `next`: ```bash