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.
Table of Contents
- 1. Protected branches: more than bureaucracy
- 2. What protected branches actually prevent
- 3. Required status checks: CI must be green
- 4. Required reviews: approvals and CODEOWNERS
- 5. Blocking force-push and branch deletion at the server
- 6. Setting up branch protection on GitHub
- 7. Configuring protected branches on GitLab
- 8. Strictness vs. team friction: finding the right balance
- 9. Unprotected vs. protected branch compared
- 10. Summary
- 11. FAQ
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.