GitLab Branch Protection: Lock Down Main and Control Who Can Merge

GitLab Branch Protection: Lock Down Main and Control Who Can Merge

· 7 min read
gitlab-for-your-team

Branches are flowing, staging works. But anyone with Maintainer access can still push directly to main. Fix that.

This guide locks down main so that:

  • No one pushes directly to it
  • Merges only come from staging
  • Only a specific person can approve and merge

Why Protect the Main Branch?

main is production. Every commit should be tested, reviewed, and intentional. Without protection:

  • A dev can accidentally push broken code to production
  • Hotfixes bypass staging
  • No audit trail of approvals

Branch protection forces changes through merge requests, approvals, and CI checks.

Prerequisites

You need Maintainer or Owner access to configure branch protection rules in GitLab. If you’re a developer, ask your team lead to set this up.

Step 1: Protect the Main Branch

In your GitLab project:

  1. Navigate to Settings → Repository
  2. Expand the Protected branches section
  3. Find main in the list (or add it)

Set the following:

SettingValue
Allowed to mergeMaintainers
Allowed to push and mergeNo one
Allowed to force pushOff

Result:

  • No one can push directly to main — not even Maintainers
  • Only Maintainers can merge via merge requests
  • Force push is disabled — history stays intact
# This will now be rejected for everyone:
git push origin main
# remote: GitLab: You are not allowed to push code to protected branches on this project.
Don't Lock Yourself Out

Make sure at least one person (usually the project Owner) retains merge access. If you set “Allowed to merge” to “No one,” nobody will be able to merge — including you.

Step 2: Protect the Staging Branch Too

Same treatment for staging:

  1. In Protected branches, click Add protected branch
  2. Select or type staging
SettingValue
Allowed to mergeMaintainers
Allowed to push and mergeNo one
Allowed to force pushOff

Both branches are now protected. All changes flow through merge requests:

Diagram

No shortcuts.

Step 3: Require Merge Request Approvals

Branch protection stops direct pushes. It doesn’t require review. Add approvals.

Project-Level Approval Rules

  1. Go to Settings → Merge requests
  2. Scroll to Merge request approvals
  3. Click Add approval rule
FieldValue
Rule nameProduction Gatekeeper
Approvals required1
ApproversSelect your team lead or release manager

Every MR now needs approval from the designated person before merging.

Prevent Approval by the Author

In the same section, enable:

  • Prevent approval by author — authors can’t approve their own MRs
  • Prevent editing approval rules in merge requests — devs can’t remove the requirement
  • Remove all approvals when commits are pushed — new commits reset approvals

These close common loopholes.

Step 4: Restrict Which Branches Can Merge to Main

GitLab has no built-in “only allow merges from branch X” setting. Use a CI check instead.

Add this to your .gitlab-ci.yml:

check-source-branch:
  stage: .pre
  script:
    - |
      if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" = "main" ]; then
        if [ "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" != "staging" ]; then
          echo "❌ Error: Only the 'staging' branch can be merged into 'main'."
          echo "Source branch: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
          echo "Please merge your changes to staging first."
          exit 1
        fi
        echo "✅ Source branch is 'staging' — allowed to merge to main."
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Any MR from a non-staging branch to main fails immediately.

Require Pipeline to Pass Before Merging

Make this check block merges:

  1. Go to Settings → Merge requests
  2. Under Merge checks, enable:
    • Pipelines must succeed

The enforced flow:

  1. Developer creates MR from feature branch to develop
  2. develop merges to staging (tested in staging environment)
  3. Team lead creates MR from staging to main
  4. CI validates the source branch
  5. Team lead approves and merges

Any other path to main gets blocked.

💡 What About Hotfixes?

Sometimes you need to bypass the normal flow for critical fixes. Create a hotfix/* branch from main, fix the issue, and merge it to both main and staging. Update the CI check to also allow hotfix/* branches:

if [ "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" != "staging" ] && \
   [[ "$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME" != hotfix/* ]]; then
  echo "❌ Only 'staging' or 'hotfix/*' branches can merge to main."
  exit 1
fi

Step 5: Limit Who Can Merge to Main

You’ve restricted what can merge. Now restrict who.

Using Allowed to Merge Setting

Step 1 set “Allowed to merge” to Maintainers. To limit it to a specific person:

  1. Go to Settings → Repository → Protected branches
  2. For main, click Edit
  3. Under Allowed to merge, select a specific user instead of a role

Only the designated release manager can merge to main, even if there are multiple Maintainers.

Using CODEOWNERS (GitLab Premium)

On GitLab Premium or Ultimate, use a CODEOWNERS file:

# CODEOWNERS file in the root of your repo

# Everything requires approval from the team lead
* @team-lead-username

Enable Require CODEOWNERS approval in Settings → Repository → Protected branches.

The code owner must approve before any merge goes through.

Putting It All Together

Summary of protections:

ProtectionWhat It Does
Protected branchesNo direct pushes to main or staging
Merge request approvalsTeam lead must approve before merge
CI source branch checkOnly staging (or hotfix/*) can merge to main
Pipeline must succeedCI must pass before merge is allowed
Specific merge accessOnly the designated person can click merge

The complete flow:

Diagram

Quick Checklist

Verify everything is in place:

  • main branch protected — push set to “No one”
  • staging branch protected
  • Approval rule created with specific approver
  • “Prevent approval by author” enabled
  • “Remove all approvals when commits are pushed” enabled
  • CI job checks source branch for MRs targeting main
  • “Pipelines must succeed” enabled
  • Specific user set as allowed to merge (not just “Maintainers”)

Wrapping Up

This takes 10 minutes. Every production change becomes intentional, reviewed, and tested. Branch protection + approval rules + CI checks create a gate that’s hard to bypass accidentally. No single mechanism is enough alone.

If you haven’t set up your branching strategy yet, start with the Git Branching Strategy: A Practical Case guide — it covers the foundation this builds on.