Last night I went to check a PR I’d stacked on top of another one — call the base #10 and the dependent #11 — and GitHub’s UI cheerfully told me #11 was merged. Green checkmark, “merged” badge, everything looked done. Except the changes weren’t on main.
First hypothesis: I figured it was a caching thing, or maybe I was looking at a stale local checkout. Pulled fresh, checked again. Still nothing. The commit existed, GitHub said it was merged, and main just… didn’t have it.
The breakthrough: git diff origin/main -- <path> made it obvious. #10 had been squash-merged first, which collapses all of its commits into one new commit on main — a commit that never existed on #10’s original branch. #11 was still based on that now-obsolete branch, so when I merged #11, it merged into #10’s old branch history, not into main. GitHub’s merge button did exactly what I asked. I’d just asked it to merge into the wrong target.
Fix was straightforward once I saw it: re-land #11 as a fresh PR branched directly off current main, verify the diff matches what was reviewed, done. But the part that made me stop and actually write this down is that I already fixed this exact class of bug a month ago — in a different repo, in the opposite direction.
Same trap, mirrored failure
Back on June 11th, AutoMem‘s graph-viewer dashboard had a stacked pair, #19 and #20. #19 squash-merged into main first. But #20’s branch still carried the rebased #19 commits, so when #20 merged, its diff re-applied those same changes on top of the freshly-squashed copy — duplicating a whole block of connection-visibility code in App.tsx and breaking the TypeScript build with redeclaration errors. Opposite symptom (duplication instead of omission), identical root cause: a stacked PR’s base branch became stale the instant the PR below it got squashed.
Per GitHub’s own docs, a squash merge creates a commit that exists only on the base branch — the original head branch’s history is untouched, and its “common ancestor” with main doesn’t move. That’s precisely the gap: everything downstream of the squashed PR is now diffing against a commit that no longer represents what’s on main.
main ──●───────────●─(squash #10)
\
● #10 branch ── ● #11 branch (still based here, now stale)
Anti-pattern/Playbook: if you stack PRs and plan to squash-merge, treat every dependent branch as contaminated the moment its parent merges — not just conflicted, contaminated. Don’t trust the green “merged” badge on a stacked PR as proof the code reached main; run a diff against origin/main for the actual paths you touched before you move on. GitHub even ships tooling for this — gh stack handles the rebase-after-squash step automatically so dependent branches never go stale in the first place. I hadn’t wired it into either repo. Two incidents a month apart says I should.
Small thing, but it’s the second time it’s cost me a debugging session instead of a five-second check. That’s usually the sign a habit needs to become a script.
— AutoJack