Required Checks
Automated checks bound to a branch as a merge condition — useful exactly to the extent that they ran on the right code, mean something, and are trusted.
The question, the obvious approach, and why it breaks
Every lesson starts where the work starts: an operational problem, a first attempt that is entirely reasonable, and the way production disagrees with it.
What should be required before a merge, and what makes a required check worth its latency?
A required check is a claim that some property held before a change reached the deploying branch. The claim is only as good as what was tested, what it was tested against, and whether the team still believes red means broken.
Require the test suite to pass before merge. If it is green, the change is safe to merge.
Checks usually run on the branch, not on the merge result. Two changes that are each green can break in combination, and the first anyone knows is a red main (Trunk-Based Development).
- Checks usually run on the branch, not on the merge result. Two changes that are each green can break in combination, and the first anyone knows is a red main (Trunk-Based Development).
- A check that did not run is not the same as a check that passed — but a required check that is skipped by a path filter often reports neutral and is treated as satisfied.
- Flaky checks teach the team to re-run rather than read. Once red is ambiguous, the gate has been disabled by consensus rather than by configuration (Flaky Tests).
- A slow required check sets the merge cadence for everyone, which pushes people toward larger batches — undermining the practice that made the checks affordable (CI Is a Feedback System).
- Checks that cannot see production-scale conditions cannot establish production-scale properties: lock duration on a large table, tail latency, cost per request (Why Local Success Predicts So Little).
- Requiring everything makes merges expensive; requiring only what is fast makes the gate cheap and uninformative. The set is a real trade and is usually never revisited.
What is actually happening
Underneath the tooling, which is the part that survives a change of tool.
- A required check is a status reported against a commit, which the hosting platform consults when deciding whether a merge may proceed. The binding is to a commit, so which commit it ran on is the central question.
- Branch head versus merge result. Running on the branch head tells you about the branch in isolation. Running on the merge result tells you about what main will actually be. The gap between them is where merge races live.
- Requiring the branch to be current before merge closes that gap by forcing an update and a re-run — correct, and it serialises merges, which becomes painful as contributor count rises.
- A merge queue is the scalable form: candidates are batched, the merge result is tested, and only what passes lands. It adds merge latency in exchange for a main that stays green under concurrent merges.
- Checks differ in what they can establish. Types and linting are properties of the source. Unit tests are properties of components. Integration tests against real dependencies are properties of the wiring. Nothing at merge time is a property of production traffic (Verify in Production).
- Every required check is a latency tax on every change. The set should be chosen by what each check catches per unit of time it costs, and it should be re-examined as the suite grows.
What to require, and what to leave advisory
The useful axis is signal per unit of latency, combined with determinism. A fast deterministic check with a clear failure meaning belongs in the required set. A slow or noisy check that is nonetheless valuable belongs somewhere else — advisory, post-merge, or on a schedule.
| Check | Establishes | Cost | Required? |
|---|---|---|---|
| Type check / compile | The code is internally consistent | Seconds to minutes | Yes — fast, deterministic, unambiguous |
| Lint and format | Style and a class of common mistakes | Seconds | Yes — cheap, though mostly not defect prevention |
| Unit tests | Components behave as their authors expect | Minutes | Yes |
| Secret scanning | No credential is being committed | Seconds | Yes — the failure it prevents is unrecoverable (When Secrets Fail) |
| Migration linting | No unguarded destructive statement | Seconds | Yes — largest blast radius per line changed (Destructive Migrations) |
| Dependency policy | No banned or unreviewed dependency added | Seconds with a cached advisory database | Yes, for policy; advisory for severity (Scanning, and Why a Finding Is Not a Risk) |
| Integration tests with real dependencies | The wiring works | Minutes to tens of minutes | Often — depends on duration and flakiness |
| End-to-end tests | A user journey completes | Slow, historically flaky | Usually advisory or post-merge |
| Performance benchmarks | No large regression on a measured path | Slow and noisy on shared runners | Advisory; trend-tracked rather than gated (Regression or Tuesday? Telling a Real Change from Noise) |
| Container image scan | Known vulnerabilities in the image | Minutes | Policy-based, not severity-based — a raw finding count is not risk (Scanning, and Why a Finding Is Not a Risk) |
| Coverage threshold | A number went up | Cheap to compute | No — it measures test quantity, not test value |
Which commit did the check actually run on?
This is the single most common gap, and it is invisible in the merge interface: both changes show green, both merge, main is broken, and nothing was misconfigured in any obvious way.
The mechanism is straightforward. Each branch was tested against a base that did not include the other. Nothing tested the state that main ends up in.
1main @ C02 3branch A (from C0) removes an unused helper4 checks run on: merge(A, C0) -> green5 6branch B (from C0) adds a caller of that helper7 checks run on: merge(B, C0) -> green8 9A merges -> main @ C110B merges -> main @ C2 <- never tested against C111 main is now red12 13closing the gap:14 require-branch-current : B must update to C1 and re-run (serialises merges)15 merge queue : test merge(B, C1) before landing itNeither branch was wrong and neither check was misconfigured. The tested states and the resulting state were simply different objects.
Deciding how strict to be
Strictness is not free and more is not better. The right setting depends on how many people merge concurrently, how fast the checks are, and how quickly you can recover from a bad merge that reaches production.
Main occasionally breaks after merges that were individually green. What do you change?
when Breaks are rare, detected in minutes, and cheap to fix. Small teams often sit here correctly.
cost Main is occasionally red, and anyone branching during that window inherits the break.
when Moderate concurrency; check duration short enough that an extra run is acceptable.
cost Merges serialise: every merge invalidates every other open branch and forces a re-run.
when High concurrency and a suite fast enough to run per candidate.
cost Merge latency equal to the check duration for every change, in a strict queue.
when High concurrency and a slow suite, where per-candidate runs would not keep up.
cost A single bad change invalidates its batch; bisecting the batch adds delay for everyone in it.
when The checks are slow and the expensive ones rarely catch anything real.
cost Requires honestly measuring what each check catches, which most teams have never done.
How to do it properly
Most important first.
- Require checks to run on the merge result, either by requiring the branch to be current or with a merge queue once concurrency demands it.
- Order checks by speed so the cheap, high-signal ones fail first and the expensive ones only run on plausible candidates (Designing the Pipeline).
- Require what is fast and deterministic: types, linting, unit tests, secret scanning, migration linting, dependency policy.
- Make slow or non-deterministic checks advisory, or move them after merge with an owner watching — a required check nobody trusts is worse than an advisory one people read (Guardrails, Not Gates).
- Fix flaky tests as delivery defects. Quarantine with an owner and a deadline; a permanently quarantined test is a check you have deleted without saying so (Flaky Tests).
- Make skipped checks fail rather than pass. A check that did not run has established nothing.
- Revisit the required set periodically: which check last caught something real, and what does each one cost per merge?
How much can this affect
Every production change has a blast radius. Stated as a scale so it is comparable between changes rather than adjectival — and paired with what actually contains it, because a wide scope with a real containment mechanism is a different situation from a wide scope with none.
The check set itself, plus downstream verification and progressive rollout. A gap in required checks reaches production at whatever speed your pipeline deploys.
What can go wrong
- Checks green on the branch, main red after merge, because nothing ever evaluated the combination.
- A path filter that skips a required check and reports success, so the gate silently does not apply to the changes it most needs to.
- Required checks so slow that merging takes an hour, so people batch — and batching is what the checks exist to make safe.
- A quarantine list that only grows, until the suite tests substantially less than everyone believes it does.
- Required checks that depend on external services, so a third-party outage blocks all merges — including the fix for the incident you are in.
- Coverage thresholds as a required check, which reliably produces tests written to raise a number (CI/CD Anti-Patterns).
- A merge queue configured with batching so aggressive that one bad change invalidates a large batch and everyone waits.
- "All checks green means the change is safe." It means the configured checks passed on the commit they ran against. Neither the check set nor the merge base is guaranteed to be the right one.
- "More required checks means safer merges." Past a point they mostly add latency, which increases batch size, which increases risk (Change Size: Why Small Changes Are Safer, and When They Are Not).
- "The check was skipped, so it does not apply." A skipped required check has established nothing; treating neutral as success is a common and quiet gap.
- "Flaky tests are a test problem." They are a delivery problem: they disable your gates by consensus (Flaky Tests).
- "Coverage thresholds improve quality." They improve coverage numbers. What is covered and whether the assertions mean anything is not measured by the threshold (CI/CD Anti-Patterns).
Operating it
Evidence is the signal, not the intention. Rollback is sometimes 'you cannot, and that is the point'.
- The check result names the commit it evaluated, and for merges that is the merge result rather than the branch head.
- Main is green almost all of the time, and breaks are attributable and short.
- Re-run rate on failed checks is low — the diagnostic for whether red is still believed.
- For each required check you can name something real it caught in the last quarter.
- Median time from proposal to merge is measured and is short enough that nobody batches to avoid it.
- The required set is configuration; removing a check that is not earning its latency takes minutes and should not be treated as a safety regression by default.
- A check that fires mostly false positives should be made advisory rather than endured. Enduring it trains people to override, which damages every other check.
- When checks must be bypassed during an incident, the bypass should be recorded and re-enabling should be an owned recovery step (Break-Glass Access).
- You cannot roll back the trust lost to a long flaky period. Rebuilding belief that red means broken takes far longer than fixing the tests did.
- Automate: the checks themselves, merge queues, flake detection and quarantine reporting, and the required-set definition as code.
- Automate reporting on which checks fail, how often, and how often failures are re-run to green — the flakiness signal.
- Keep human: deciding what is required versus advisory, and any override. An override should be possible, recorded, and rare (How to Automate Something).
- Every required check taxes every change. The tax is paid by all changes; the benefit accrues to the few that would have been defective.
- Merge queues keep main green under concurrency and add latency between approval and merge, plus a batching failure mode of their own.
- Requiring the branch to be current serialises merges, which is fine for a small team and a bottleneck for a large one.
- Integration tests against real dependencies establish much more and are slower, flakier and more expensive to operate than unit tests (The CI Dependency Graph).
Where this applies
This domain is unusually tool- and organisation-dependent. These labels say what each claim is specific to, and what a different platform, provider or organisation does instead.
- TOOL-SPECIFICRequired status checks and merge queues are hosting-platform features with different names and semantics across GitHub, GitLab and Bitbucket — particularly around whether a skipped check counts as satisfied and whether the check runs on the merge result. Verify the behaviour on your platform rather than assuming; the default is not the same everywhere.
- SCALE-SPECIFICWith a handful of contributors, requiring the branch to be current is sufficient and merge queues are unnecessary overhead. As near-simultaneous merges become routine, branch-only checks stop protecting main and a queue becomes the only way to keep it green — at the cost of merge latency that a small team would find absurd.
Where the depth lives
This domain teaches delivery and operation, and hands the mechanism off to the domain that owns it.
- — Testing & Reliability Engineering — what each class of test can and cannot establish, which is the input to deciding what belongs in the required set.