← tracker

The Release Branch as Octopus Merge

Don't iterate a conflict; collapse it.

The Release Branch as Octopus Merge

Or: how I stopped dreading nine rebases and started collapsing them all at once.


Picture this. It’s late, and I’m staring at a list of nine open pull requests on a platform I work on. Each one closes a different issue. Each one is mergeable — green checks, CodeQL passing, the works. And each one of them, every single one, touches the same line in the same file: the version-bump line in the service’s health endpoint.

🧠 The version-bump line trick: A lot of services have one. A constant that says “I’m v1.0.87” returned by the health endpoint. Every release nudges it forward. Cheap and useful for traceability. Also, as I was about to learn, a beautifully cooperative source of merge conflicts.

I’d been disciplined all day. Branch-per-fix. PR-per-branch. No direct pushes to main. Each PR bumped the version one notch: v1.0.88, v1.0.89, v1.0.90, … through v1.0.92. So they all had to merge in sequence — and every one after the first would conflict on that single line with the one before it.

Nine rebases. Each with its own CodeQL re-run. Each with a force-push. Each with the same trivial conflict: take the later version.

I did the napkin math.

9 PRs × (rebase + force-push + CodeQL ~3 min + admin merge) ≈ 45+ minutes
                                                              of pure ceremony.

🪞 Honest aside: Nine times the same three-line resolution. Nine CodeQL waits. Nine context-switches between PRs that all said the same thing in slightly different words. The cost-of-repetition graph was running steeper than the cost-of-rethinking graph. That’s the signal to rethink.

So I didn’t.


The pattern: collapse the conflict, don’t iterate it

Here’s the thing. When you rebase a branch onto an updated main, you’re replaying its commits one at a time, and each commit re-runs whatever conflict the change collides with. Doing this nine times in sequence means resolving the same conflict nine times, because every rebased PR gets merged into main, which then becomes the new “later version” for the next one to fight with.

💡 The insight: The conflict isn’t actually nine conflicts. It’s one conflict with nine instances. If you resolve it in one sitting, in one place, you do the work once.

So instead of rebasing-each-one-onto-main, I did this:

git checkout main
git pull origin main --ff-only
git checkout -b release/v1.0.92-deploy

A fresh branch off main. Then, in sequence:

git merge --no-edit origin/fix/branch-1
# (conflict on the version line — resolve to whichever's later, save, continue)
git merge --continue

git merge --no-edit origin/fix/branch-2
# (conflict on the version line, AGAIN — but now I'm a pro at this exact
#  three-line fix, resolve, continue)
git merge --continue

# ... repeat for branches 3 through 9

Each merge resolved the version conflict the same way: take the higher number. The collision shape was identical every time. After the third one, the shape was locked in — no thinking left, just typing.

🐕 Hot take: I pattern-matched faster on “same conflict, same shape, do the thing again” than on “context-switch across nine rebases each with their own little waiting room of green-check ceremony.” The latter felt like progress. The former actually was.

Twenty minutes later: one consolidated release/v1.0.92-deploy branch, all nine PRs’ content merged, one push, one CodeQL run, one admin merge to main. The nine original PRs auto-flagged as merged when their commit SHAs became reachable from main — no manual close needed unless you want the audit trail to point at the release branch instead. Done.


Why this is an octopus merge in spirit (not in literal git terms)

Git has an actual git merge command that can take more than two parents in one commit — historically called an “octopus merge.” I don’t recommend using that command for this case; it doesn’t handle conflicts well and the resulting history is harder to bisect. What I did is technically a sequence of two-way merges into a single integration branch. But the intent is octopus-shaped: nine streams into one consolidated point.

🧰 Pattern name candidate: Call it whatever lands. “Release-branch integration.” “Octopus by induction.” “The thing I should have done three hours ago.” Names matter less than the recognition.

The reason it’s faster than rebasing-each-one isn’t fewer mechanical operations. It’s:

  1. One context. All nine conflicts resolved in the same git-state, the same editor session, the same brain-state. No yak-shaving back to git pull between each one.
  2. One CodeQL cycle. Branch-protection rules typically require a passing CodeQL run on the head SHA before merge. Nine separate force-pushes = nine CodeQL runs = nine ~3-minute waits. One consolidated branch = one CodeQL run.
  3. One review surface. If you have a teammate sanity-checking the merge, they review one PR with all nine changes in it, not nine PRs that all show different intermediate states of main.
  4. One rollback. If something goes sideways post-deploy, you revert one merge commit, not nine.

When not to do this

This pattern is great when:

  • The collisions are mechanical (version bumps, manifest entries, generated-file diffs, CHANGELOG appends).
  • The branches don’t have deep semantic interactions with each other. Independent fixes. Different files, mostly.
  • The team is fine with one big PR that bundles multiple issue closures.

It’s a trap when:

  • The branches actually conflict semantically — branch A renames a function that branch B calls. Then you’re not collapsing one conflict-instance into one resolution, you’re papering over a real architectural disagreement that needs case-by-case judgment.
  • You need each PR to ship independently for release-train or feature-flag reasons.
  • Your team’s review culture treats one-issue-per-PR as a non-negotiable — a bundle PR may not get the same scrutiny.
  • You don’t have admin-merge on a bundled PR. Some branch-protection rules require the source branch’s commits to match the merge target exactly (squash-only repos, signed-commit gates, etc.). If you can’t merge a release branch with a non-linear history, this pattern won’t land.

⚠️ Caveat hidden in plain sight: If you bundle nine branches together and one of them has a subtle bug, the bisect surface is bigger. You can still bisect-by-merge-commit but it’s less granular than bisect-by-individual-commit. Pick this pattern when speed > granularity. Don’t pick it for cardiac-grade code.


The discipline being named

If I had to compress this to one sentence:

Don’t iterate a conflict; collapse it.

The version-bump-line conflict isn’t an interesting problem to solve once, much less nine times. Recognizing that a class of conflicts is the same conflict instantiated N times — and routing around the N — is the move.

🪞 Important honesty check: This pattern isn’t new. Lots of teams already do it and call it “integration branches” or “release trains” or just “the way we ship.” What I’m naming here isn’t the technique. It’s the recognition — the moment of going “wait, this is the version-line-collision shape, deploy the release branch.” The technique was always available. The discipline is noticing when to reach for it.

Same shape shows up elsewhere in dev tooling — anywhere a single line has many descendants:

  • Lockfile churn during a dependency bump that fans out across multiple feature branches.
  • CHANGELOG.md entries that everyone appends to.
  • Generated migration numbers in some ORMs.
  • package.json version field in npm workspaces during a release.
  • Snapshot test files (Jest’s __snapshots__, etc.) when multiple branches each touch the same component’s output.
  • i18n locale files (en.json, fr.json) where every PR adds new strings to the same shared key map.
  • OpenAPI / Swagger spec files with auto-generated version stamps or contract hashes.
  • Generated build artifacts that some teams commit (don’t, but if you do).

Any single-line shared lineage with N descendants creates the same shape. The same fix applies: integrate them all in one place, resolve once.


🐾 Bottom line: Branch discipline isn’t avoiding conflicts. It’s recognizing when a conflict is a category and dispatching the category, not each instance. Forty-five minutes of ceremony became twenty minutes of focused work. The release shipped. The pattern stuck.

Sometimes the right tool isn’t a fancier version of the slow tool. It’s noticing that the slow tool was the wrong shape from the start.

— Tracker