Merge Conflict vs CI Check: Why a Pull Request Can Be “Mergeable” but Red

Date:

Open a pull request and you may see something that looks like a contradiction: a green “This branch has no conflicts” line, and right below it a red ✗ on the checks. Both are true at once — because they are answering two completely different questions.

The short answer: a merge conflict check and a CI check are not the same gate. The conflict check asks “can these two sets of edits be combined?” — a pure text comparison your forge does for free, no build required. CI asks “does the combined code actually work?” — it takes the merged result and runs it (build, tests, rules). Code can pass the first and fail the second, because two edits can slot together without overlapping yet still produce something broken. That is why a pull request can report mergeable (no conflicts) and a failing check in the same breath: the pieces fit, but “the pieces fit” was never a promise that the result runs. Both gates exist because each catches a class of problem the other is blind to.

A merge is the moment two lines of development — your branch and the main branch — become one. Before a forge like GitHub lets that happen, it forms an opinion along two independent axes: mergeability (is there a text conflict to resolve?) and checks (did the automated jobs pass?). They show up next to each other on the pull-request page, which is exactly why they get confused for one thing.

Two questions, not one

Picture two editors marking up the same short document, then handing their copies back to be combined.

The first question is mechanical: did they scribble over the same sentences? If editor A rewrote paragraph two and editor B rewrote paragraph five, you can paste both sets of changes together without either one fighting the other. If they both rewrote paragraph two differently, someone has to decide which wording wins. That is a conflict — and noticing it requires nothing more than lining the two copies up side by side. You do not need to read the document to see that the same lines were touched twice.

The second question is about meaning: after you paste the changes together, does the document still make sense? Two edits can land in different paragraphs — no conflict at all — and still leave the whole thing self-contradictory, because editor A deleted the definition that editor B’s new sentence relied on. To catch that, you actually have to read the combined result and check it against reality.

A conflict check asks whether two edits overlap. CI asks whether the combined result is true.

Software works the same way. The conflict check is the first question — a line-by-line comparison. CI is the second — it reads and runs the combined result.

The conflict check: can these be combined?

When your branch and the main branch have both changed since they parted ways, the forge does a trial merge in memory. It walks the two sets of changes and asks, file by file, line by line: did they touch the same spot? If not, it can weave them together automatically and reports mergeable — no conflicts. If they did touch the same lines in incompatible ways, it reports a merge conflict and refuses to merge until a human resolves it.

Two things are worth knowing about this check:

  • It is free and instant. It is just text comparison — no code is compiled or executed. It costs no build minutes and needs no test runner. (You can even run the same trial merge on your own machine with a single git command, entirely offline.)
  • It is a hard gate. A forge will not let you complete a merge that has an unresolved conflict. There is no “merge anyway” for conflicts — the ambiguity has to be settled first, because the computer genuinely cannot know which version of the clashing line you meant.

What the conflict check emphatically does not do is run your program. It has no idea whether the code compiles, whether the tests pass, or whether the feature works. “Mergeable” means only “these edits can be stitched together,” not “the stitched-together result is any good.”

The CI check: does the combined result work?

CI — continuous integration — is the second gate, and it is a different kind of thing entirely. CI is a program that takes the merged code and runs it: it installs the dependencies, builds the project, executes the test suite, and enforces whatever rules the project cares about (formatting, security scans, architecture constraints). Then it reports a single ✓ or ✗ as a status check on the pull request.

Because CI actually executes the code, it catches the whole family of problems the conflict check is blind to. The classic one is an integration break:

Here is the classic case. On the main branch, someone renames a function getPrice to getCost. On your branch — in a different file — you add a new call to getPrice. Neither of you touched the same line, so there is no conflict: the forge happily reports “mergeable.” But the merged code now calls getPrice, which no longer exists. Only CI, by running the tests, catches that the combination is broken.

That is the crux of why both gates exist. The conflict check protected you from a textual clash; CI protects you from a behavioral one. Neither can do the other’s job.

Two edits in different files never conflict — and can still break the build together. That gap is exactly what CI is for.

Reading the pull-request page without the confusion

Once you know there are two axes, the pull-request page stops looking contradictory. On GitHub, for example, the two live in separate fields:

What you seeWhich gatePlain meaning
“This branch has no conflicts” / mergeable: MERGEABLEconflictthe edits combine cleanly
“This branch has conflicts” / mergeable: CONFLICTINGconflictyou must resolve overlapping lines first
A green ✓ on the checksCIthe automated jobs ran and passed
A red ✗ on the checksCIa job ran and failed — or couldn’t run at all
mergeStateStatus: CLEANbothmergeable and checks green
mergeStateStatus: UNSTABLEbothmergeable, but a check is failing or missing (not a conflict)
mergeStateStatus: DIRTYconflictthere is an actual conflict to resolve

The pairing that trips people up is mergeable + red check (UNSTABLE). It feels wrong, but it is perfectly ordinary: the edits fit together fine (conflict gate: pass), and separately, a test or rule did not come back green (CI gate: fail). Two gates, two verdicts.

A red check has one more subtlety worth naming, because it hides a very different situation: a check can be red because a test ran and failed, or because the job never ran at all — for instance when a hosted CI service is out of build minutes and the run dies on startup. Same red ✗, opposite meanings. If every check across every branch turns red at the same instant and each “fails” in a couple of seconds, that is almost never your code — it is the runner failing to start. (If you are wondering why a free CI budget drains so fast in the first place, where those minutes actually go.)

So can I just run CI later?

Yes — CI is not a one-shot event welded to the merge button. It is a program that runs against a commit, and you can run it against any commit, any time: re-run the failed job when your build minutes come back, push a fresh commit to re-trigger it, or trigger it by hand. Running it after a merge, on the main branch, is a completely normal thing to do.

The only trade-off is order, and it is about safety, not possibility. The tidy sequence is CI-passes-then-merge, so broken code never reaches the main branch in the first place. If you merge first and run CI later, there is a window where the main branch is unverified — if the delayed run comes back red, you have already merged and now have to fix it forward. Whether that window is acceptable is a judgment call: if the same tests already passed on your own machine, the risk is small; if you are merging something you have not run at all, it is not.

One caveat sits underneath all of this: whether the forge lets you merge on a red check depends on the project’s rules. If a check is marked required, the forge blocks the merge until it is green — you cannot merge-now-and-check-later without an override. If it is only advisory, the red ✗ is a warning, not a lock, and you are free to merge and verify afterward. So “can I run CI later” has two layers: the run itself is always repeatable; whether you may merge before it passes is a policy the repository sets.

The short version

  • A merge has two independent gates. The conflict check asks “can these edits be combined?”; CI asks “does the combined result work?” They are not the same question.
  • The conflict check is free, instant, and a hard gate. It is a line-by-line text comparison — no code runs — and a forge will not merge over an unresolved conflict.
  • CI runs the code. It builds and tests the merged result, so it catches breakages the conflict check cannot see — most classically an integration break, where two non-overlapping edits break each other.
  • “Mergeable + red check” is normal, not a bug. The edits fit (conflict gate passed) while a test or rule failed (CI gate failed). On GitHub that reads as mergeable: MERGEABLE, mergeStateStatus: UNSTABLE.
  • A red check can mean “failed” or “never ran.” A uniform, two-second red across every branch is a runner/billing failure, not a test failure.
  • You can run CI later. The run is always repeatable against any commit; the only question is order (CI-first is safer) and whether the check is required (which decides if you may merge before it is green).

Frequently Asked Questions

Why does my pull request say “no conflicts” but still show a red X?

Because those are two separate gates. “No conflicts” is the mergeability check — your edits and the main branch don’t touch the same lines, so they can be combined. The red X is a CI check — an automated job that builds and tests the code either failed or couldn’t run. Edits fitting together cleanly says nothing about whether the combined code works, which is what CI measures.

What is the difference between a merge conflict and a failing CI check?

A merge conflict is textual: two branches changed the same lines, and the computer can’t know which version you meant, so it refuses to merge until you choose. A failing CI check is behavioral: the merged code was built and run, and something didn’t pass — a test, a lint rule, a build step. The first is caught by comparing text; the second only by executing the code.

Can code merge cleanly and still be broken?

Yes — that is exactly why CI exists. If one branch renames a function and another branch (in a different file) still calls the old name, the two edits don’t overlap, so there’s no conflict and the merge is “clean.” But the combined code now calls something that no longer exists. The tests catch it; the conflict check never could.

Can I run CI after I’ve already merged?

Yes. CI is a program you can run against any commit at any time — re-run the failed job, push a new commit to re-trigger it, or start it manually. The catch is order: running CI before merging keeps broken code off the main branch, whereas merging first leaves a window where the branch is unverified until the later run confirms it.

Does checking for conflicts cost build minutes?

No. The conflict check is a text comparison the forge (or your own machine, via git) does instantly and for free — no build, no test runner, no minutes. Only CI, which actually builds and runs the code, uses build minutes. This is why you can always tell whether a branch is mergeable even when a hosted CI service is completely out of minutes.

What does “mergeable but UNSTABLE” mean on GitHub?

mergeable: MERGEABLE means there is no text conflict — the branch can be combined. mergeStateStatus: UNSTABLE means it is mergeable but at least one check isn’t green (failing or missing). Together they say: “nothing to resolve, but a check hasn’t passed.” That is different from DIRTY, which does mean a real conflict, and from CLEAN, which means mergeable and all checks green.

Share post:

Subscribe

Popular

More like this
Related

How to Merge Spreadsheets by URL (and Why Our Tool Reported 5 Misses That Weren’t)

Matching two spreadsheets on a URL column is the same job as matching a stock list to a price list by product code. It goes wrong in the same two places: how you clean the code, and how you count what didn't match.

Many-to-One: Why a Mapping Backlog Outgrows Its Target Taxonomy

You're mapping a messy source vocabulary onto a clean target taxonomy. The target has about 5,600 slots, so the job must be roughly 5,600 decisions — right? The backlog is already past 8,900, and still climbing.

The Fix That Didn’t Take: When a Content-Hash Gate Skips Your Code Change

You shipped a fix to how one field is computed, re-ran the pipeline across everything, and it reported a clean run. Then the consistency check still flags those records as wrong. The fix is in the code, the rebuild "ran" — and the records never changed. Your own optimisation skipped them.

Your Dedup Scan Is Over-Counting — Because It Forgot What You Already Fixed

The report said 80 duplicate groups still to resolve. You'd been resolving them for days. You run it again after a big batch and it *still* says 80-ish. The backlog isn't stubborn — the scan is counting work you already finished.