Mandatory reviewers, not an honor code
Approval rules make sure code review does not depend on individual good will but is technically enforced, with CODEOWNERS integration, minimum approval counts, and clear rules for exceptions.
Table of Contents
- 1. Why a four-eyes principle alone is not enough
- 2. CODEOWNERS as the basis for area-specific review obligations
- 3. Setting a minimum number of approvals per rule group
- 4. Typical bypass paths and why they exist
- 5. Deliberately closing the loopholes: the relevant switches
- 6. Emergency hotfixes: documenting exceptions instead of loosening rules
- 7. Tracing approval events: who approved what and when
- 8. Gradual rollout instead of a big-bang switch
- 9. Common configuration mistakes and how to avoid them
- 10. Summary
- 11. FAQ
1. Why a four-eyes principle alone is not enough
Many teams introduce code review as an organizational rule: every merge request needs an approval before it can be merged. That sounds like a solid foundation at first, but in practice it regularly fails at the enforcement stage. Without technical backing, any team member with sufficient rights can simply ignore the rule, whether due to time pressure, lack of awareness, or because a hotfix needs to go out fast. By the end of the quarter it often turns out that a quarter of all merges happened without a real review.
GitLab solves this with approval rules, defined at the project or merge request level, which in combination with branch protection technically prevent a merge request from reaching a protected branch without the required approvals. The key difference from a plain team rule: enforcement no longer rests with the developers but with the platform itself. This significantly reduces debates about exceptions because the rule applies equally to everyone and cannot be interpreted individually.
2. CODEOWNERS as the basis for area-specific review obligations
The CODEOWNERS file in the project root (or under .gitlab/ or docs/) defines which people or groups are responsible for specific paths in the repository. GitLab reads this file and can automatically derive approval rules from it, so that changes to the payment module must be approved by the payments group, while frontend changes are reviewed by a different team. This prevents a reviewer from approving code they have no domain context for.
The order of the rules matters: later, more specific entries override earlier, more general ones. A wildcard entry * at the top of the file covers the entire repository, while targeted path patterns further down define additional, stricter requirements for critical areas such as app/code/Vendor/Payment/. This makes it possible to model a tiered protection level without maintaining every single file by hand.
# .gitlab/CODEOWNERS
# Default rule: at least one review from the core team
* @mironsoft/core-reviewers
# Payment and checkout code: mandatory payments team review
app/code/Vendor/Payment/** @mironsoft/payments-team
app/code/Vendor/Checkout/** @mironsoft/payments-team @security-lead
# Frontend/theme: frontend team is responsible
app/design/frontend/** @mironsoft/frontend-team
# CI/CD configuration: only the platform team may change pipelines
.gitlab-ci.yml @mironsoft/platform-team
/ci/** @mironsoft/platform-team
3. Setting a minimum number of approvals per rule group
Besides CODEOWNERS mapping, GitLab lets you define a minimum number of approvals for every approval group. A single approval is enough for most changes, but security-critical areas like authentication or payment processing benefit from two independent approvals from different teams. This granularity is the real value over a single global one-approval rule, because it puts effort and risk into a sensible relationship instead of treating a typo fix the same as a change to payment logic.
In the project configuration under Settings > Merge requests > Merge request approvals, you can define several rules in parallel, each with its own target group, its own path filter, and its own minimum count. GitLab evaluates all matching rules for a merge request and requires every single one to be satisfied before the merge button becomes active. This allows for fine-grained escalation logic without needing a separate external tool.
# Example approval rule definition via the GitLab API
# (POST /projects/:id/approval_rules)
{
"name": "Payments Security Review",
"approvals_required": 2,
"user_ids": [],
"group_ids": [4821, 4903],
"protected_branch_ids": [1],
"applies_to_all_protected_branches": false
}
# Second, more general rule for the rest of the code
{
"name": "Standard Code Review",
"approvals_required": 1,
"group_ids": [4711],
"applies_to_all_protected_branches": true
}
4. Typical bypass paths and why they exist
Even well-configured approval rules have loopholes if other settings do not keep up. The most common case: a project maintainer has enough rights to temporarily change branch protection rules or bypass the merge request entirely with a direct push to the protected branch. Without explicit restrictions, users with the Maintainer role can also edit approval rules on a per-merge-request basis and remove reviewers afterwards, which undermines the entire mechanism if nobody notices the change.
A second, more subtle problem is self-approval: by default the author of a merge request cannot approve their own change, but without the "Prevent approval by author" option enabled, this does not reliably hold in every GitLab configuration, especially in older projects created before this setting existed. Allowing committers to act as reviewers also erodes the four-eyes principle if two developers wave each other's changes through without real review.
5. Deliberately closing the loopholes: the relevant switches
Under Settings > Merge requests, GitLab offers several switches that specifically counter the loopholes described above. Prevent approval by author stops the MR creator's own approval from counting. Prevent approvals by users who add commits closes the gap where someone pushes additional code unnoticed after the initial approval. Prevent editing approval rules in merge requests forbids maintainers from loosening rules on a per-MR basis, so only the central project settings apply.
For truly critical repositories, it is also worth restricting the permission to change branch protection rules to a very small group of people and enabling Code owner approval required for the protected branch. This means even a maintainer can no longer bypass the CODEOWNERS requirement with a click, but would have to explicitly and traceably change the branch protection configuration, which stays visible in the audit log.
# Setting the relevant options via the GitLab API
curl --request PUT \
--header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/123/approval_rules/settings" \
--data "prevent_approval_by_author=true" \
--data "prevent_approval_by_commit_author=true" \
--data "disable_overriding_approvers_per_merge_request=true"
6. Emergency hotfixes: documenting exceptions instead of loosening rules
The classic objection to strict approval rules is: "What if a critical bug needs to be fixed live at night and no reviewer is reachable?" The wrong answer is to permanently loosen the rule. The right answer is a documented exception process: a small, named group of people with the Owner role may temporarily change branch protection rules in an emergency, but must justify it in an incident ticket and revert the change immediately after the issue is resolved.
In practice, a fixed checklist proves useful for such cases: announce the exception in the team chat, link the ticket reference in the merge request, request a retroactive review from a second team member after merging, and revert the temporary permission change within 24 hours. This process preserves the safety of the rule without becoming an obstacle in a real emergency, and it makes every exception visible in the audit log instead of allowing it silently.
7. Tracing approval events: who approved what and when
GitLab logs every approval, every removal of an approval, and every change to approval rules in the project-level audit log (group-wide as well on Premium/Ultimate licenses). This history is more than a formality: it lets you clarify after the fact why a particular merge request only had one approval instead of two despite critical changes, or whether a rule was briefly disabled. For regulated industries or ISO 27001 audits, this log is often a required proof of a working change management process.
The approval status of every merge request can also be evaluated automatically via the API, for example to generate a monthly report of how many merge requests were merged without the required minimum number of approvals (which should be zero with correctly configured rules), or how often the emergency exception was actually used. A rising number is usually a signal that either the reviewer pool is too thin or the rules are too strict in the wrong place.
# Query a merge request's approval status via the API
curl --header "PRIVATE-TOKEN: <token>" \
"https://gitlab.example.com/api/v4/projects/123/merge_requests/456/approvals" \
| jq '{approved: .approved, approved_by: [.approved_by[].user.username]}'
8. Gradual rollout instead of a big-bang switch
Introducing approval rules with maximum strictness overnight usually leads to frustration and creative workarounds in established teams. A three-stage approach has proven effective: first, only a single, general rule with a minimum of one approval is enabled for the entire repository, without CODEOWNERS differentiation. After two to three weeks of getting used to it, the split into functional areas via CODEOWNERS follows, so teams become responsible for their own modules specifically.
Only in the third stage are the stricter switches, such as Prevent approval by author and higher minimum counts for security-critical paths, enabled, accompanied by brief team communication explaining why the change makes sense. This staged approach builds acceptance because developers experience the rules as support rather than a bureaucratic hurdle, and it noticeably reduces the number of exception requests during the rollout phase.
9. Common configuration mistakes and how to avoid them
A recurring mistake is defining approval rules without actually marking the target branch as a protected branch. Without that protection, changes can still bypass the rules entirely via a direct push, regardless of how strictly the merge request rules are configured. Just as common is a CODEOWNERS file with overlapping, contradictory path patterns, where in the end nobody is quite sure which group is actually responsible for which folder.
The table below summarizes the most important levers and shows what behavior each one protects against. It works well as a checklist for a configuration review before approval rules go live for a critical repository.
| Setting | Protects against | Recommended for | Location in GitLab |
|---|---|---|---|
| CODEOWNERS file | Reviews by people outside the domain | All projects with multiple teams | .gitlab/CODEOWNERS |
| Approvals required >= 2 | A single opinion as the only control | Payment, auth, and security code | Settings > Merge request approvals |
| Prevent approval by author | Self-approval by the MR creator | All protected branches | Settings > Merge request approvals |
| Prevent approvals by commit authors | Unreviewed follow-up commits | All protected branches | Settings > Merge request approvals |
| Disable overriding rules per MR | Maintainers loosening rules case by case | Critical and regulated repositories | Settings > Merge request approvals |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
Approval Rules: The Essentials at a Glance
CODEOWNERS
Area-specific mandatory reviewers instead of one global rule for everything.
Minimum count
One or two independent approvals per rule group depending on criticality.
Bypass protection
Enable the prevent switches for self-approval and follow-up commits.
Emergency process
A documented, time-limited exception instead of a permanently loosened rule.