← all articles

Merging Cleanly Is Not The Same As Building

Merging Cleanly Is Not The Same As Building

A merge with no conflicts tells you almost nothing about whether the result works. Git checks whether two branches changed the same lines. It has no idea whether they changed the same idea. Two perfectly good pieces of work can combine into a codebase that no longer compiles, and nothing in the process will warn you until the combined result is actually built. The practical rule I keep coming back to is simple: the thing you tested must be the thing you ship, and a branch tested against yesterday's default branch is not that.

Here is the shape of the failure, which I watched play out again recently. One change removed a concept from a system entirely. A whole entity, its storage and the code that referred to it, deliberately deleted because another part of the platform had become the single source of truth. A second change, started before that deletion landed, added a small piece of code that leaned on the very concept being removed. Each change was reviewed. Each passed its pipeline. When the second one merged, Git reported no conflicts, because the deletion and the new usage lived in different files and different lines.

The default branch stopped building. Not with a subtle behavioural bug, but with the blunt kind of error a compiler produces when something it was promised exists no longer does. Every other open change that rebased onto that branch inherited the breakage, so a mistake made by one person became a stall for everyone. The fix was to revert the offending commit, and the follow-up work went back onto its branch to be redone against the world as it now actually was.

Nobody did anything careless here, and I think that is the important part. The author's branch really was green. The reviewer really did find nothing actionable. Both statements were true of a tree that no longer existed by the time the merge button was pressed.

A green check has a date on it

A passing pipeline is a claim about a specific snapshot: this branch, at this commit, sitting on top of whatever the default branch looked like when it was last updated. The longer a branch lives, the older that snapshot gets. Meanwhile the default branch keeps moving, and the claim quietly goes stale without any visible sign. The badge stays green. It simply no longer describes what will happen next.

This is why I treat the age of a branch as a risk in its own right, separate from its size. A small change on a stale base can be more dangerous than a large change on a fresh one, because the small change looks harmless and gets less scrutiny. Deletions and renames are the sharpest edge of this. Additions rarely break other people's work. Removing something that other work might still be reaching for is exactly where a textual merge and a semantic one come apart.

Making the combined result the thing that gets tested

There are a few ways to close the gap, and they sit at different points on the cost curve. The cheapest is habit: before merging anything that has been open for a while, bring it up to date with the default branch and let the pipeline run again. It costs minutes and catches precisely this class of problem.

The more reliable option is to have the platform do it. Many CI systems can build the result of merging your branch into the current default branch, rather than your branch alone, and some can queue merges so each one is tested on top of the one before it. Both turn "the branch passed" into "the outcome passed", which is the claim you actually care about. There is a throughput cost, since queued merges wait their turn and a team that merges constantly will feel it. I would still pay that cost on any codebase where more than a couple of people are changing shared structure at once, because the alternative is paying it unpredictably, at the worst moment, in front of everyone.

There is also a human side to the fix. Decisions that remove something from a system should be announced, not just merged. If a deletion is a deliberate change of direction, the people with work in flight deserve to hear about it before their branch collides with it. A short message to the team costs almost nothing, and it prompts the question the pipeline cannot ask: is what I am building still aimed at something that exists?

Treat a broken default branch as everyone's problem

The other lesson sits in how the team responds. A broken default branch is not a private embarrassment for whoever merged last. It is a blocked road for every colleague who needs to get through it. The healthy reflex is to stop, restore the branch to a working state quickly, and sort out the longer repair afterwards, rather than debating whose fault it was or letting people work around it. Reverting is unglamorous and usually the right first move, precisely because it returns the shared thing to a known good state while the real fix is considered calmly.

In this case that is what happened. The revert landed, the build recovered, and the remaining work got the time it needed. That is a sensible outcome. The more valuable one is changing the process so the same two perfectly reasonable changes cannot collide invisibly again.

I do not think the answer is more caution or more ceremony. People already review carefully and test their own work. What is missing in these moments is a check on the combination, because the combination is the only thing users, colleagues and production will ever meet. A clean merge is a statement about text. Whether the system still builds, still starts and still behaves is a separate question, and it only gets answered by building it.

Matthew Ratcliffe, software developer and architect, Ballarat
Senior Software Engineer & Architect

20+ years across the technology stack — from greenfield builds to brownfield rescues. Based in Ballarat, VIC, focused on AI, healthcare and high-risk data systems. Full resume →

Share your thoughts