Code Review

How CODEOWNERS Should Decide Who Must Approve

A CODEOWNERS file is a plain text list of path patterns mapped to GitHub teams or usernames. GitHub reads it, and when a pull request touches a matching path, the listed owners get requested for review automatically. The file has no other job. It does not enforce anything by itself, it does not block a merge, and it cannot veto a change. What it does is name people, and the interesting question is what happens after the naming: which of those people actually has to click approve before the merge button goes green.

Most repositories answer that question by accident. A pattern gets added, a team gets listed, branch protection gets switched on with "require approvals" checked, and the default assumption settles in: every listed owner must approve. That assumption is wrong often enough to cause real friction, and it is worth pulling apart.

The file names reviewers, not gates

Read the GitHub documentation on CODEOWNERS and one detail stands out: the file controls who is requested for review, and branch protection decides whether those requests are binding. The two systems are separate. A CODEOWNERS entry that lists four people on a shared infrastructure path does not mean four approvals are needed. It means four people get a notification and a little grey checkmark request in their review queue.

This separation trips up teams constantly. Someone adds a database migrations folder to CODEOWNERS, lists the platform team, and assumes the platform team now holds a hard gate. Six weeks later a migration merges with a single approval from a teammate who happened to be online. The file did its job. It requested the platform team. Nobody had configured the rule that would have made the request mandatory.

The first decision, then, is not about CODEOWNERS at all. It is about whether a given path deserves a hard gate. That decision belongs in branch protection rules, and it should be made path by path, deliberately.

"Require review from Code Owners" is a single switch

GitHub offers a branch protection option labelled "Require review from Code Owners." When enabled, a pull request that touches a path with owners cannot merge until at least one owner approves. Note the wording: at least one. The switch does not multiply approvals by the number of names in the file. It does not require every owner, or even a majority. One owner approval satisfies it.

That single-approval behavior is the source of most confusion in this area. A team lists eight people on a critical authentication module, turns on the switch, and believes the module now requires eight approvals. It requires one. The other seven get requested and can be ignored entirely, and the merge proceeds the moment any single owner signs off.

If the intent was consensus, the configuration did not deliver it. If the intent was "someone competent must look at this," the configuration delivered exactly that and nothing more. Those are different intents, and teams owe it to themselves to know which one they are buying.

When one owner approval is the right rule

For most paths, one owner approval is correct. A shared utilities folder, a styling layer, a documentation directory: these benefit from a knowledgeable reviewer, and one is enough. Requiring two or three approvals on a CSS refactor slows the queue without improving the outcome. The reviewer either understands the cascade or does not, and a second signature from someone equally unfamiliar adds ceremony, not safety.

The single-approval rule also behaves well when owners are listed as a team rather than individuals. GitHub resolves team membership at review time, so a small team of four can absorb vacations and time zones while still guaranteeing that one of them looked at the change. That flexibility is the point. A rigid list of named individuals creates a queue that stalls the moment one person is offline.

Where the rule starts to strain is on paths where a single wrong approval is expensive and hard to reverse. Migrations, payment logic, permission checks, and anything that touches production credentials fall into that category. One reviewer is a coin flip dressed as a process, and the code does not care how many people were notified.

Paths that need more than a single signature

GitHub's built-in CODEOWNERS enforcement cannot express "two owners must approve." The switch is binary, and the approval count it enforces is one. Teams that want a higher bar on specific paths have to build it themselves, and there are a few ways that actually work.

The most common approach is a small automation: a workflow that reads the CODEOWNERS file, checks which owners have approved the pull request, and fails the check if the count is below the required threshold for that path. GitHub Actions can query the review list through the API and compare it against the owners resolved for the changed files. It is perhaps forty lines of YAML and a short script, and it converts a vague social expectation into a red X that blocks the merge.

A lighter approach is to split the ownership. Instead of listing one team of eight on the migrations folder, list two distinct teams, each with a narrow scope: one for schema correctness, one for operational safety. With the "require review from Code Owners" switch on and a separate branch protection setting requiring two approvals overall, the practical effect is that both teams must sign off, because each team's approval is needed to satisfy the general count. It is a hack, but it is a hack that uses GitHub's own machinery rather than external scripts.

The third approach, and the one that ages best, is to stop treating the file as the source of truth for approval counts. Keep CODEOWNERS lean, listing the people who genuinely own a path, and encode the approval threshold in the automation or branch protection layer where it can be reviewed and changed independently. The file answers who. The rules answer how many.

Last-match-wins makes the file a puzzle

CODEOWNERS resolves patterns in a specific order: the last matching pattern wins. A broad entry for the whole repository followed by a narrow entry for one subdirectory means the narrow entry takes precedence for files inside it. This is straightforward in a file with three lines and a nightmare in a file with ninety.

When a path is matched by multiple patterns, only the final match applies. A team that lists the platform group on infra/ and then adds a more specific line for infra/secrets/ pointed at the security group has silently removed the platform group from secrets changes. The platform team still believes it owns that folder. The file disagrees.

This ordering behavior matters for the approval question because the resolved owner list determines who can satisfy the gate. If the last matching pattern names a single individual who is on leave, the gate is effectively closed, no matter how many other competent people are listed earlier in the file. Auditing the file for accidental last-match overrides is part of deciding who must approve, because the resolution order decides it for you.

The admin bypass nobody documents

Branch protection settings include an option to allow administrators to bypass the rules. When enabled, repository admins can merge a pull request that has not satisfied the CODEOWNERS requirement. This is often turned on during initial setup and then forgotten. The gate exists on paper and not in practice, and the people with the bypass are usually the ones most likely to be in a hurry.

A team that has decided a path requires owner approval should know whether that decision can be overridden and by whom. If the answer is "any admin, silently, with no audit trail beyond the merge event," the policy is decorative. Some organizations disable bypass entirely on critical branches. Others leave it on but require a follow-up review after the fact. Either is defensible. Not knowing is not.

Stale ownership rots quietly

Ownership drifts. People change teams, teams get renamed, someone deletes their account, and the CODEOWNERS file keeps pointing at a team that no longer exists. GitHub handles a missing user or team by ignoring the entry, which means the matching path silently loses its reviewer request. The gate, if one was configured, becomes a gate that nobody can satisfy, or worse, a gate that resolves to the remaining stale members who are no longer paying attention.

There is no built-in expiration on ownership. The file is static text, and the organization around it moves. A quarterly review of the file against the actual team structure catches most of this. So does a check in CI that verifies every team and username in the file resolves to a real, active entity. Neither is glamorous. Both prevent the specific failure where a critical path has owners on paper and no one with a pulse behind the approval button.

A practical shape for the file

Given all of that, the file that survives contact with a real team tends to look a certain way. It is short. It lists teams rather than individuals wherever possible, so membership can change without editing the file. It puts the broadest patterns first and the narrowest last, and it avoids overlapping patterns that create last-match surprises. Each entry has a reason someone can articulate out loud: this path is owned because these people are the ones who get paged when it breaks.

Above the file sits a branch protection rule with the Code Owners requirement enabled and the approval count set deliberately, not left at the default. Beside it sits a small automation for the one or two paths where a single approval is not enough. Below it sits a calendar reminder to audit the whole arrangement once a quarter, because the organization will drift and the file will not drift with it.

The file itself is a few dozen lines of text. The decisions around it are the actual work, and they are decisions a team can make in an afternoon if it stops assuming the file enforces anything on its own. The merge button is green or it is not. That outcome is determined by the rules, and the rules are worth getting right before the next migration lands at 5 p.m. on a Friday.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button