Configuring Protected Branches Correctly
AI generated
git
HEAD
Git · Branch Protection · GitHub · GitLab
Configuring Protected Branches Correctly
Required checks, approvals, and force-push protection for main

An unprotected main branch is only one bad force-push away from a complete disaster. This article explains what protected branches on GitHub and GitLab actually prevent, how required status checks and approval rules work, what role CODEOWNERS plays, and what baseline keeps main safe and practical for Magento and Hyva agencies without slowing the team down.

12 min. read Required Checks · Approvals · CODEOWNERS GitHub · GitLab · Magento Teams

1. Protected branches: more than bureaucracy

An unprotected main branch looks like a trivial detail in theory, but in practice it is the exact spot where a single careless command can destroy the entire production state of a Magento store. Protected branches are not bureaucracy, they are a server-side safeguard: Git itself has no concept of important or unimportant, every branch is technically equal until a rule explicitly says otherwise. That rule is exactly what GitHub and GitLab implement with branch protection.

This article covers what protected branches actually prevent, how required status checks and required reviews work together, what role a CODEOWNERS file plays, and how to balance safety against team velocity. It closes with a concrete baseline configuration that has proven itself in practice for main or production at a Magento and Hyva agency like Mironsoft.

2. What protected branches actually prevent

Branch protection acts on three fronts at once, and each one addresses a real damage scenario. First, it blocks direct pushes to the protected branch: changes must go through a pull or merge request, even if a developer technically has write access to the repository. Second, it blocks force-pushes that rewrite commit history and can irrecoverably wipe out other people's commits, for example after a botched rebase. Third, it protects the branch from accidental deletion, which matters especially with automated cleanup scripts or CI jobs holding overly broad permissions.

All three mechanisms operate server-side, not client-side: a local Git client doesn't know the rule and initially allows the command, the rejection only comes back as the server's response to the push. That is an important difference from client-side Git hooks, which a developer can bypass locally. Branch protection cannot be worked around by a single developer, as long as it's configured correctly.

3. Required status checks: CI must be green

Required status checks tie the merge button directly to the outcome of the CI pipeline. As long as a defined check hasn't passed, the merge button stays disabled on GitHub, and GitLab blocks the merge via the pipeline-must-succeed setting. This prevents the most common antipattern in unprotected repositories: merging a red build anyway with the reasoning "it'll probably be fine," because deadline pressure mattered more than a failing test.

The catch is in the detail: GitHub identifies a required check by its exact name, meaning the job name from the GitHub Actions workflow file, not the name of the workflow file itself. If a job gets renamed or a pipeline gets restructured, the expected check disappears and blocks merges permanently, because GitHub keeps waiting for a result that will never be reported again. GitLab instead relies on the project-wide "Pipelines must succeed" setting, combined with optional merge request approval rules for individual jobs.


# .github/workflows/ci.yml
name: CI

on:
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    # This job name is what the branch protection rule references,
    # not the workflow file name "ci.yml".
    name: build-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Run PHPStan
        run: vendor/bin/phpstan analyse app/code --level=5
      - name: Run PHPUnit
        run: vendor/bin/phpunit --testsuite unit

4. Required reviews: approvals and CODEOWNERS

The number of required approvals is the second load-bearing pillar of branch protection. One or two reviewers before merge is the value most commonly used in practice, more usually just creates waiting time in small teams without adding any real safety margin. A CODEOWNERS file in the repository root, or under .github/ or .gitlab/, additionally lets you define which team member or team gets automatically required as a reviewer for which path, for example the frontend team for changes to the Hyva theme and the backend team for changes to db_schema.xml.

A frequently overlooked switch is "Dismiss stale approvals on new commits." Without it, an approval stays valid even if additional commits get pushed after the approval that the reviewer never saw. That opens a gap through which unreviewed code can be smuggled into an already-approved pull request. For main or production, this switch should practically always be enabled, even though it creates some friction when reviewers have to re-approve after every push.


# .github/CODEOWNERS - automatic reviewer assignment for pull requests
# Order matters: the last matching pattern takes precedence

# Default owners for everything in the repo
*                                @mironsoft/core-team

# Magento module code requires backend review
/app/code/                      @mironsoft/backend-team
/app/code/Mironsoft/SeoSuite/    @mironsoft/seo-team

# Hyva theme and frontend assets require frontend review
/app/design/frontend/           @mironsoft/frontend-team

# CI/CD and deployment configuration requires DevOps sign-off
/.github/workflows/              @mironsoft/devops-team
/compose.yaml                   @mironsoft/devops-team

# Database schema changes always need a second pair of eyes
/app/code/**/etc/db_schema.xml  @mironsoft/backend-team @mironsoft/devops-team

5. Blocking force-push and branch deletion at the server

On GitHub, the "Allow force pushes" option, separate from the other rules, controls whether a force-push to the protected branch is possible at all, and it's disabled by default. On GitLab the same setting exists as "Allow force push" in the protected branch configuration. If a developer tries a force-push anyway, the server rejects the push with a clear error message, the local Git client shows "remote rejected," and the push fails completely without losing any commits.

Branch deletion is protected via a separate setting: "Allow deletions" on GitHub, or the protected branch rule on GitLab, which prevents a branch from being deleted through the command line or the web interface as long as it's marked protected. This safeguard matters especially for release branches, which could otherwise be mistakenly marked "done" after a merge and get cleaned up automatically.


$ git push --force origin main
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 8 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 1.02 KiB | 1.02 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0)
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Cannot force-push to a protected branch
To github.com:mironsoft/shop.git
 ! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:mironsoft/shop.git'

# GitLab shows a comparable rejection:
$ git push --force origin main
remote: GitLab: You are not allowed to force push code to a protected branch on this project.
To gitlab.com:mironsoft/shop.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'gitlab.com:mironsoft/shop.git'

6. Setting up branch protection on GitHub

The fastest and most reproducible way to set up branch protection on GitHub is the GitHub CLI or the REST API, instead of clicking through the setting manually in the web interface. That has the advantage that the configuration can be versioned and rolled out identically across multiple repositories, for example via an internal setup script for new Magento projects.

The following example sets a complete baseline: two required approvals including a CODEOWNERS requirement, stale approvals dismissed on new commits, a required status check named build-and-test, disabled force-pushes and deletions, and optionally required linear history.


# Configure branch protection for main via the GitHub CLI
gh api \
  --method PUT \
  -H "Accept: application/vnd.github+json" \
  repos/mironsoft/shop/branches/main/protection \
  --input - <<'EOF'
{
  "required_status_checks": {
    "strict": true,
    "contexts": ["build-and-test"]
  },
  "enforce_admins": false,
  "required_pull_request_reviews": {
    "required_approving_review_count": 2,
    "require_code_owner_reviews": true,
    "dismiss_stale_reviews": true
  },
  "restrictions": null,
  "required_linear_history": true,
  "allow_force_pushes": false,
  "allow_deletions": false
}
EOF

7. Configuring protected branches on GitLab

GitLab models branch protection a bit differently than GitHub: instead of a single "protected" switch with several sub-options, the protected branches API defines separate access levels for push and merge. A push_access_level of 0 means "No one," meaning nobody can push directly, while a merge_access_level of 30 allows merges via a merge request starting at the Developer role.

The requirement for green pipelines isn't set through the protected branches API on GitLab, but through the project-wide merge request setting "Pipelines must succeed" in General Settings under Merge Requests. Code owner approval can be enabled both project-wide and per protected branch via code_owner_approval_required, which makes CODEOWNERS files on GitLab functionally equivalent to GitHub.


# Configure a protected branch for main via the GitLab API
curl --request POST \
  --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.com/api/v4/projects/$PROJECT_ID/protected_branches" \
  --data "name=main" \
  --data "push_access_level=0" \
  --data "merge_access_level=30" \
  --data "unprotect_access_level=40" \
  --data "allow_force_push=false" \
  --data "code_owner_approval_required=true"

# push_access_level=0 means "No one": merges only via an approved MR
# merge_access_level=30 restricts merges to the Developer role and above

8. Strictness vs. team friction: finding the right balance

Overly strict branch protection noticeably slows down small teams: if you work with two developers and require two approvals, you can end up blocking yourself when both happen to be on vacation or sick at the same time. Enabling enforce_admins on top of that means even a repository owner can't push through a critical hotfix in an emergency without temporarily changing the rule. Overly loose protection, on the other hand, defeats the point entirely: a required check that isn't actually required, because admins can bypass it, ends up protecting nobody.

As a rule of thumb, a tiered approach by team size has worked well in practice. Solo developers or two-person teams do fine with one required approval, active required checks, and enforce_admins disabled, so someone can still react in an emergency. Agency teams with five to ten developers benefit from two required approvals, a CODEOWNERS requirement for critical paths like db_schema.xml and deployment configuration, and a clearly documented exception process for genuine hotfixes, instead of loosening protection across the board for every edge case.

9. Unprotected vs. protected branch compared

The following overview compares an unprotected main branch against the recommended baseline for Magento and Hyva projects. The difference is rarely in exotic settings, it comes from consistently combining a handful of basic rules.

Dimension Unprotected main Protected main (baseline)
Direct pushes Anyone with write access can push directly Only via pull request, no direct push
Force-push Allowed, overwrites history and teammates' work Blocked, allow_force_pushes: false
Branch deletion Anyone can delete the branch Protected, allow_deletions: false
Required approvals None, merges without review possible 1-2 approvals including CODEOWNERS
Required CI checks Merges possible even with a red build Merge blocked until build-and-test is green

For main or production in an agency environment, the following baseline has proven itself: pull request required with no exceptions, one to two approvals depending on team size, required status checks for build and tests, disabled force-pushes, restricted direct push rights via restrictions or push_access_level, and optionally required linear history for a clean, bisect-friendly log. This combination covers the relevant damage scenarios without slowing the team down with excessive bureaucracy.

Mironsoft

Git workflows, branch protection, and CI/CD pipelines for Magento teams

Want branch protection set up right, without slowing the team down?

We set up required status checks, approval rules, and CODEOWNERS for main and production, matched to your team's size and your Magento or Hyva deployment workflow.

Branch protection audit

Review your existing main configuration on GitHub or GitLab against best practices

CI/CD integration

Set up and document required status checks matched to your pipeline

CODEOWNERS setup

Reviewer assignment by module and team for Magento and Hyva code

10. Summary

Protected branches solve one concrete problem: main or production must not depend on a single careless command. Direct pushes, force-pushes, and branch deletions get blocked server-side, required status checks make sure only tested code gets merged, and required reviews with CODEOWNERS ensure the right people see every change before it goes live.

The decisive point is finding the right balance: overly strict rules slow small teams down, overly loose rules defeat the purpose. A baseline of one to two approvals, mandatory CI checks, disabled force-pushes, and a documented exception process for hotfixes covers most Magento and Hyva projects in a practical way, without unnecessarily slowing developers down.

Configuring Protected Branches Correctly - The Essentials at a Glance

What protection prevents

Direct pushes, force-pushes, and branch deletion on main are blocked server-side, regardless of the local Git client.

Required checks

CI must be green before merging. The check name must exactly match the job from the workflow file.

Approvals & CODEOWNERS

1-2 required reviewers, automatic assignment via CODEOWNERS, dismiss stale approvals on new commits.

Baseline for agencies

PR required, 1-2 approvals, CI required, no force-push, no direct push, documented hotfix process.

11. FAQ: Configuring Protected Branches Correctly

1What does branch protection actually prevent?
Direct pushes without a pull request, force-pushes that overwrite history, and accidental branch deletion. All three operate server-side, independent of the local Git client.
2How do I require CI checks before a merge?
GitHub: required status checks with the exact job name. GitLab: Pipelines must succeed in merge request settings. Without a green check, the merge stays blocked.
3What is CODEOWNERS and how do reviews get auto-requested?
A file that maps path patterns to teams. For matched paths, the owner is auto-requested as a required reviewer, enabled via require_code_owner_reviews.
4Should approvals be dismissed on new commits?
For main or production, yes. Without this setting, an approval stays valid even after unreviewed code gets pushed afterward.
5How many required approvals is reasonable for a small team?
One approval is usually enough for two- to three-person teams. Two approvals fit better once a team has around five or more developers.
6Can admins bypass branch protection (enforce_admins)?
By default, yes. With enforce_admins active, the rules apply without exception, including to repository admins.
7How do I allow emergency hotfixes without fully disabling protection?
Via a documented exception process with a second pair of eyes, instead of permanently disabling the rule. Restore the baseline immediately after.
8GitHub branch protection rules vs. repository rulesets?
Rulesets are more granular, with layering, bypass lists, and application to multiple branches via a pattern. Branch protection rules remain functional.
9Does branch protection replace server-side hooks?
No, the two complement each other. Hooks allow additional project-specific validation and are covered in a separate article.
10What is required linear history and when is it useful?
Forbids merge commits, forces rebase or squash merges. Useful for a clean log and simpler git bisect runs.