Protecting Your Main Branch Is the Cheapest Quality Gate You Have

by Arif Ikhsanudin, Backend Developer

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

Auckland Keeps Losing Its Best Backend Developers to Sydney and London — Here Is How Startups Adapt

Your best backend engineer just told you she's moving to Melbourne. The one before her went to London. You're running out of people to lose.

Read more

Learning a New Technology Without Abandoning the Fundamentals

Frameworks, languages, and tools change. The underlying concepts they implement — data modeling, concurrency, network communication, failure handling — do not. Engineers who learn new technology through the lens of fundamentals learn faster and more durably.

Read more

Writing Useful Unit Tests for Spring Boot Services — Patterns That Catch Real Bugs

Most unit tests verify that code does what it already does — they pass when the code is written and continue passing through every refactor, catching nothing. Here is how to write tests that fail when something breaks and survive when nothing does.

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