Protecting Your Main Branch Is the Cheapest Quality Gate You Have

by Eric Hanson, Backend Developer at Clean Systems Consulting

The Rule Nobody Enforces Until It's Too Late

Your team has a shared understanding: don't push directly to main, always get a code review, only merge when CI passes. It works fine — until it doesn't. A developer is debugging a production incident and pushes a quick fix directly to main. Another developer squashes a flaky test by disabling it in CI and pushes before anyone notices. A third accidentally merges a PR with failing checks because the UI was confusing.

None of these are bad developers. They're humans under pressure making mistakes that a fifteen-minute configuration change would have prevented.

Branch protection is the technical enforcement of the agreements your team already made verbally. The verbal agreement relies on everyone remembering and applying the rules consistently under pressure. The configuration doesn't.

The Core Rules and What They Prevent

Require a pull request before merging. Prevents direct pushes to main. Code must go through a PR, which means it goes through CI and is visible for review before it lands in the shared branch.

Without this: someone pushes directly to main at midnight to fix a bug. Nobody reviews it. It breaks something else. Nobody knows what changed.

Require status checks to pass before merging. Specifies which CI checks must succeed before a PR can merge. Prevents "it worked on my machine" merges.

Without this: a developer merges a PR where the database migration test failed. The next morning, everyone's local environments and the staging database are out of sync.

Require branches to be up to date before merging. Ensures the PR's CI results reflect the current state of main, not the state of main when the PR was first opened. Prevents merges where two PRs each pass CI but conflict when combined.

Without this: PR A and PR B both pass CI. PR A merges. PR B's CI results are now stale (they don't include PR A's changes). PR B merges. Main breaks.

Require at least one approving review. Ensures a second set of eyes before any code lands. Minimum viable code review enforcement.

Do not allow bypassing the above settings. Prevents admins from merging without meeting the same requirements. This is the one most teams skip, but it's important — if admins routinely bypass protection rules, the rules have no teeth.

Configuring Branch Protection on GitHub

Settings → Branches → Branch protection rules → Add rule → Branch name pattern: main

Branch protection rule for: main

[x] Require a pull request before merging
    [x] Require approvals: 1
    [x] Dismiss stale pull request approvals when new commits are pushed

[x] Require status checks to pass before merging
    [x] Require branches to be up to date before merging
    Status checks:
      + test / unit-tests
      + test / integration-tests
      + lint / code-quality

[x] Require conversation resolution before merging

[x] Do not allow bypassing the above settings

[ ] Allow force pushes  (leave unchecked)
[ ] Allow deletions     (leave unchecked)

The status check names come from your CI configuration — they must match the job names exactly. Run a push first, look at the checks that appear on the PR, and copy the names.

The Required Status Checks Problem

Required status checks only block if the check actually runs and fails. If the check is optional (not required), a PR can merge even if it didn't run. If the CI job doesn't exist, GitHub shows the check as "expected but not found" — which blocks the merge until you fix the CI configuration.

This is the right behavior: if you said "this check is required," and the check didn't run, you don't know if the code is valid. Blocking is correct.

The common mistake: requiring a status check by name and then renaming the CI job, or changing the job's trigger so it no longer runs on PRs. The PR queue stops because every PR is blocked by a check that never runs. Keep required status check names in sync with CI job names — add a linter or a test for it if you have enough PRs.

For GitLab: Protected Branches

GitLab's equivalent is in Settings → Repository → Protected Branches:

Branch: main
Allowed to merge: Developers + Maintainers
Allowed to push: No one
Allowed to force push: (unchecked)
Code owner approval: Required

Merge checks:
  [x] Pipelines must succeed
  [x] All discussions must be resolved
  [x] All threads must be resolved

The "Allowed to push: No one" setting is the GitLab equivalent of "require a pull request." Nobody can push directly — even maintainers must go through a merge request.

What Happens to Developers

The most common objection to enabling branch protection is "it slows down the team." This is only true if the team was relying on shortcuts — direct pushes, skipping reviews — that branch protection now prevents.

The correct response to "branch protection is slowing us down" is to identify which shortcuts it's preventing and evaluate whether those shortcuts were actually good practice. Direct pushes to main without review are not a productivity tool. They're a risk that hadn't paid off yet.

For legitimate speed requirements (hotfix deployment at 3am), branch protection rules allow configured bypass for specific roles. Configure a "hotfix" role or allow direct pushes from a CI service account. The bypass is explicit and audited, not a gap in the protection.

The Audit Trail

Every merge to a protected branch is logged with: who merged, when, what PR, what review approvals existed, which CI checks passed. This is the audit trail that matters for compliance, security reviews, and post-incident analysis.

"We always require review before merging" is an assertion. Protected branches make it verifiable — you can query the merge history and confirm that every merge had the required approvals and passed the required checks. Without branch protection, the assertion has no evidence behind it.

Set up branch protection before you need to prove you had it. After an incident is the wrong time to discover you couldn't demonstrate your review process was enforced.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

NULL in SQL Does Not Mean What You Think It Means

NULL represents the absence of a value, not zero, not an empty string, and not false — its three-valued logic and propagation rules produce query results that are consistently surprising to developers who treat it as a regular value.

Read more

Why Your API Returns 200 Even When Something Goes Wrong

Returning HTTP 200 for failed operations hides errors, breaks client logic, and makes systems harder to debug. Using proper status codes is not pedantry—it’s critical for correctness and reliability.

Read more

TDD Does Not Mean Writing Tests for Everything. Here Is What It Actually Means.

A common misreading of TDD treats it as a rule requiring a test before every line of code. The actual practice is more selective and more practical — applied where it provides design leverage, skipped where it does not.

Read more

The Hidden Expenses Every Remote Contractor Must Consider

Remote contracting sounds simple: work from anywhere, get paid, repeat. But behind the freedom is a list of costs most people don’t see coming.

Read more