Code Ownership & Review Assignment Tool - GitHub CODEOWNERS but better
This tool is meant to be an alternative to GitHub CODEOWNERS, overcoming limitations with the CODEOWNERS spec and GitHub code owners feature.
We hope that GitHub integrates some of these ideas into the code owners feature itself. Unfortunately, because code owners does not involve AI, GitHub will likely not prioritize it.
Because this tool is not built into GitHub itself, it does not get first-party integration like the owners badge next to the reviewer's name. Instead, this tool uses comments to communicate who the required reviews are, and uses status check as the default way to enforce required reviews.
These are features missing from GitHub code owners that are supported by Codeowners Plus:
- Smart dismissal of stale reviews (only dismiss review when owned files change, rather than any reviewable push)
- Admin bypass functionality for emergency overrides by repository administrators
- Supports multiple owners of files (
ANDownership rules)- GitHub CODEOWNERS supports only
ORownership rules, in contrast
- GitHub CODEOWNERS supports only
- Directory-level code ownership files to assign fine-grained code ownership
- Supports optional reviewers (cc users/teams for non-blocking reviews)
- Hunk filters: external tooling can decide which post-approval changes do not need re-review (see Hunk Filters)
- Advanced global configuration (see Advanced Configuration)
Create a .codeowners file in the root of your repository with the following contents:
# fallback owner for any file
* @your-usernameThis rule will set up your repo so @your-username will become a required approval for any file changed. When you are ready to add more rules, see .codeowners File Spec.
Create a GitHub Actions workflow:
name: 'Code Owners'
concurrency:
group: codeowners-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
permissions:
contents: read # required for @actions/checkout
pull-requests: write # required to request reviewers and leave comments
jobs:
codeowners:
name: 'Run Codeowners Plus'
runs-on: ubuntu-latest
steps:
- name: 'Checkout Code Repository'
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: 'Codeowners Plus'
uses: multimediallc/codeowners-plus@v1.11.0
with:
github-token: '${{ secrets.GITHUB_TOKEN }}'
pr: '${{ github.event.pull_request.number }}'
verbose: true
quiet: ${{ github.event.pull_request.draft }}The Codeowners Plus GitHub Action should be set up as a required status check in a GitHub Workflow.
It is recommended to also set up a rerun workflow on pull_request_review to rerun the check (see .github/workflows/rerun_codeowners.yml for an example).
For advanced features to work, such as only re-requesting review when owned files are changed, you must disable this rule in branch protections:
Dismiss stale pull request approvals when new commits are pushed
If you plan to have organization teams as code owners, you will need to use a PAT that has organization read access for Members and Administration as the token. If you do not have organization teams as owners, GITHUB_TOKEN should be sufficient.
Codeowners Plus talks to the API of the instance running the workflow, so it works on GitHub Enterprise without any extra configuration. To point it at a different API, set the github-api-url input:
github-api-url: 'https://ghe.example.com/api/v3'The value should be the instance's exact API URL — the same value as the github.api_url context (GITHUB_API_URL). For GitHub Enterprise Server this includes the /api/v3 suffix; for GitHub Enterprise Cloud with data residency use https://api.<tenant>.ghe.com. If the input is not set, it defaults to the API URL of the instance running the workflow.
Note
When the action is referenced by a release tag (or the SHA of a release commit), it downloads its prebuilt binary from this repository on public github.com (see scripts/install-action.sh) — this is also true if you mirror the action onto your own instance, so mirrored copies should not use release refs. Referencing a branch or a non-release commit builds the binary from source instead, which requires egress to the Go toolchain and module proxy (actions/setup-go downloads, proxy.golang.org, sum.golang.org). On an egress-restricted Enterprise instance, allow whichever set of origins fits your setup.
It is recommended to have a .codeowners file in each directory. However, if there is no .codeowners file in a directory, Codeowners Plus will still recursively check rules from higher in the directory tree.
To set fallback (default) owners for any file in the directory, use *:
* @your-org/dir-owner-team
To set owners for a specific file in the given directory, use the file name:
# matches only one file in this directory with the name `file_name.md`
file_name.md @your-org/file-owner-team
To set wildcard rules, use globstar pattern matching:
# matches all files in this directory starts with `test` and ends in `.py` - for example `test_one.py`
test*.py @your-org/tests-owner-team
# matches all files in this directory that include `config` in the filename
*config*
# matches all files in this directory or any subdirectory ending in `.js`
**/*.js
# matches all files in any directory called models
**/models/**
# matches any file in a subdirectory that starts with `prefix_`
prefix_*/**
# Note - `**` will be treated like `*` if it is not surrounded by path separators - for example
test**.txt
# will be treated the same as
test*.txt
For more details, see doublestar patterns.
By default, each file will resolve to a single reviewer. See priority section for details about how the owner is assigned.
To instead require an owner as an additional reviewer (AND rule), put an & at the start of the line:
# this rule will add `@task-auditor` as a required review in addition to the file owner's required review
& **/task.go @task-auditor
You can also add non-owner (view-only / optional) reviewers by putting a ? at the start of the line:
# this rule will cause `@your-org/data-team` to be alerted about the pull request without requiring them to submit a review
? **/migrations/** @your-org/data-team
Non-owner reviewers will be pinged in a comment, but not added as reviewers
Any rule can also be an OR rule by putting 2 or more teams on the same line:
& **/models.py @models-owner-1 @models-owner-2
In the above rule, either @models-owner-1 OR models-owner-2 can dismiss this additional reviewer rule for models.py files
Comments are also suppored:
# This is a comment
.codeowners rules are always relative to the directory the file lives in, while CODEOWNERS are not relative.
For example - *.js in CODEOWNERS matches any file in the repo that ends in .js but in .codeowners it will only match files in its directory that end in .js.
To match any file that ends in .js, use **/*.js
Because all rules in a .codeowners file are relative to its directory, leading / are ignored. By contrast, CODEOWNERS uses leading / to target rules from the root of the repository.
.codeowners rules do not directly support the CODEOWNERS trailing / to match any file in a directory (example apps/ to match any file in an apps directory).
However, the parser will replace trailing / with trailing /** to indirectly support it.
This is the priority of file owners in order from highest priority to lowest priority:
- specific file owner
- wildcard file owner
- globstar file owner
- globstar file owner from parent dirs (recursive)
- fallback owner for dir
- fallback owner from parent dir (recursive)
This means if there is overlap in rules, the last declared will be the owner. This is similar to GitHub CODEOWNERS, except with type-of-rule priority.
You can include a codeowners.toml in the root of your project to add some advanced configuration options.
codeowners.toml:
# `min_reviews` (default nil) allows you to specify the minimum number of reviews required
min_reviews = 1
# `max_reviews` (default nil) allows you to skip some reviewers if the number of reviewers
# is greater than the max_reviewers
max_reviews = 2
# `unskippable_reviewers` (default empty) allows you to specify reviewers that cannot be
# skipped via the max_reviews setting
unskippable_reviewers = ["@BakerNet"]
# `ignore` (default empty) allows you to specify directories that should be ignored by the
# codeowners check
ignore = ["test_project"]
# `high_priority_labels (default empty) will cause "High Prio" label to be added to comments
# when one of these PR labels is present
high_priority_labels = ["high-priority", "urgent"]
# `detailed_reviewers` (default false) means the codeowners will include a collapsible list
# of files and owners in its review comment
detailed_reviewers = true
# `disable_smart_dismissal` (default false) means the codeowners will not dismiss stale reviews
disable_smart_dismissal = true
# `require_both_branch_reviewers` (default false) requires approval from codeowners defined
# in BOTH the base branch AND the PR branch
# This is useful for ownership handoffs where both the outgoing and incoming teams must review
require_both_branch_reviewers = false
# `suppress_unowned_warning` (default false) suppresses the warning messages about unowned files
# Useful when you intentionally have files without codeowners
suppress_unowned_warning = true
# `allow_self_approval` (default false) treats the PR author as satisfying any OR ownership group they are listed in
# If the PR author is included in an OR group (e.g. `*.py @alice @bob`), the group is auto-satisfied without requiring another member to approve
# Note: this matches the author against individually listed logins only; see `self_approval_via_teams` for team membership
allow_self_approval = false
# `self_approval_via_teams` (default false) extends `allow_self_approval` to team membership:
# OR groups containing a team the PR author is a member of (e.g. `*.py @org/backend`) are also auto-satisfied
# Requires a token that can read org team members (same requirement as GitHub Teams support)
self_approval_via_teams = false
# `disable_review_status_comments` (default false) suppresses review status comments (required/unapproved reviewers).
# Optional reviewers are still invited with a CC comment.
disable_review_status_comments = false
# `enforcement` allows you to specify how the Codeowners Plus check should be enforced
[enforcement]
# see "Enforcement Options" below for more details
# `admin_bypass` allows repository administrators to bypass codeowner requirements
[admin_bypass]
# see "Admin Bypass" below for more details
# `hooks` names external programs the check may call
[hooks]
# see "Hunk Filters" below for more detailsWhen a PR has any of the high_priority_labels, the comment will look like this:
❗High Prio❗
Codeowners approval required for this PR:
- @user1
- @user2
In some cases, it may be undesirable to fail the status check - for example if you want to keep the green check reflecting the health of PR code.
To prevent Codeowners Plus check from failing, but instead add an approval to the PR, you can use enforcement.approval setting.
Notably, this can be used in conjunction with CODEOWNERS if the token owner is a user.
codeowners.toml:
[enforcement]
# `approval` (default false) means the github token owner will approve the PR.
# This can be used alongside GitHub CODEOWNERS to enforce Codeowners Plus checks by making
# the github token owner the only CODEOWNER
approval = true
# `fail_check` (default true) means the codeowners GHA check will fail if the codeowners check fails
fail_check = false.github/CODEOWNERS:
# NOTE: This should be the only rule
# github token owner (GitHub user set up to act as a bot)
* @token-owner
With this setup, @token-owner will be a GitHub code owner on every PR, and Codeowners Plus will approve on behalf of @token-owner when all required reviews are satisfied, unblocking merge.
Unfortunately, CODEOWNERS does not support apps/bots as owners despite there being an active discussion requesting the feature since 2020.
Hopefully GitHub adds support for apps/bots as codeowners so this option can become viable for non-org repos.
Repository administrators can bypass all codeowner requirements in emergency situations by creating a special approval review containing "Codeowners Bypass" text. This feature:
- Requires authorization: Only repository admins or users listed in
admin_bypass.allowed_userscan create valid bypass approvals - Guarantee success: When detected, the PR passes all codeowner checks regardless of missing approvals
- Audit trail: Creates a clear record of who bypassed requirements and when
codeowners.toml:
[admin_bypass]
# `enabled` (default false) enables checking reviews for a bypass from repository admins
# and allowed_users
enabled = true
# `allowed_users` (default empty) includes a list of users who can trigger Admin Bypass
allowed_users = ["emergency-contact", "release-manager"] # Optional specific usersTo trigger the admin bypass feature, Create an approval review containing "Codeowners Bypass" text. This can be done by:
- Repository administrators manually approving the PR with "Codeowners Bypass" in their review comment
- Users listed in
allowed_usersmanually approving with the bypass text - Automated workflows that create approval reviews with the bypass text on behalf of authorized users
Codeowners Plus automatically detects and validates the bypass approval, immediately marking the PR as passing all codeowner requirements.
The bypass text is case-insensitive, so "codeowners bypass", "Codeowners Bypass", or "CODEOWNERS BYPASS" all work.
The require_both_branch_reviewers feature enables self-service ownership transfers by requiring approval from codeowners defined in BOTH the base branch and the PR branch. This creates an AND relationship between ownership rules from both branches.
Use Case: When Team A wants to transfer ownership of files to Team B, they can submit a PR that modifies the .codeowners file. With require_both_branch_reviewers enabled, the PR will require approval from both Team A (base branch owners) and Team B (PR branch owners), ensuring both parties agree to the handoff.
codeowners.toml:
# `require_both_branch_reviewers` (default false) requires approval from BOTH base and PR branch codeowners
require_both_branch_reviewers = trueExample Ownership Handoff:
Base branch .codeowners:
*.py @backend-team
PR branch .codeowners (modified in the PR):
*.py @data-team
Result with require_both_branch_reviewers = true:
- Any
.pyfile changes require approval from both@backend-teamAND@data-team - This ensures the outgoing team (
@backend-team) and incoming team (@data-team) both approve the ownership transfer
Note: The require_both_branch_reviewers setting is read from the base branch's codeowners.toml for security. PR authors cannot enable this feature for their own PRs.
Using the quiet input on the action will change the behavior in a couple ways:
- No Comments: The action will not post the review status comment (listing required/unapproved reviewers) or the optional reviewer "cc" comment to the Pull Request.
- No Review Requests: The action will not automatically request reviews from required owners who have not yet approved via the GitHub API.
- Draft Pull Requests: This is a common use case. You might want the Codeowners Plus logic to run and report a status (e.g., pending or failed) on draft PRs, but without notifying reviewers prematurely by adding comments or requesting reviews until the PR is marked "Ready for review".
- Custom Notification Workflows: You might prefer to handle notifications or review requests through a different mechanism and only use Codeowners Plus for the status check enforcement.
When an approval is checked, the diff is taken twice from the same merge base, once to the head and once to the approved commit, and the hunks appearing in both are subtracted. What is left is what the reviewer has not seen. That comparison is textual, so a hunk which changed for a reason a particular codebase does not care about still counts as unreviewed. Hunk filters let external tooling make that judgement and feed the answer back to Codeowners Plus.
Name an executable in the [hooks] table of codeowners.toml:
[hooks]
hunk_filter = "/opt/review-tools/already-seen"Codeowners Plus reads codeowners.toml from the base ref, so a pull request cannot point the check at a program of its own.
For each approved commit being checked, the filter receives one JSON object on stdin:
{
"version": 1,
"base": "<merge base sha>",
"head": "<head sha>",
"ref": "<the approved commit>",
"files": [
{
"name": "service/handler.go",
"head_hunks": [{ "body": "@@ -1,3 +1,3 @@\n-old\n+new\n" }],
"approval_hunks": [{ "body": "@@ -9,0 +10,1 @@\n+approved\n" }]
}
]
}head_hunks: the hunks still outstanding against this approvalapproval_hunks: the hunks the approved commit itself introduced, for comparisonref: the approved commit the head is being compared againstversion: the request format, currently1
It writes one JSON object on stdout, naming the head_hunks the reviewer has effectively already reviewed by index into the same file:
{ "reviewed": [{ "name": "service/handler.go", "indexes": [0] }] }Both sides of the diff are sent because they answer different questions: a hunk rewritten since approval looks like new code on its own, and only looks like reviewed code next to the text the reviewer saw.
Notes:
- A filter can only narrow what an approval answers for. It cannot add hunks, name a file it was not sent, alter ownership, or grant an approval; ownership resolution and the staleness check run unchanged on whatever survives.
- Every failure leaves the diff as Codeowners Plus computed it and logs a warning: the program is missing or not executable, it exits non-zero, it does not finish inside 60s, its output is not a single JSON object of the known shape, or it names a file or an index that was not sent.
- Unlike a configuration option, a filter is code in the review path, and it acts in the direction of dismissing less. It cannot weaken ownership, but it can decide that a change nobody looked at did not need looking at.
- The path must be absolute, and it is executed directly rather than through a shell. A relative path would resolve inside
GITHUB_WORKSPACEand a bare name would go throughPATH, both of which put the choice of program within reach of the pull request. - A path under
GITHUB_WORKSPACEis refused for the same reason: the checkout is writable by the PR author. Symlinks are followed before the check, so a link out of the checkout does not get around it. Install the program in the runner image, fetch it by digest, or build it from a pinned ref. - Both of those refusals fail the run outright, unlike the failures above, which leave the diff alone and log a warning. A path that will not be run is a misconfiguration to fix, not a line in a log.
- The action's own inputs are removed from the filter's environment,
INPUT_GITHUB-TOKENamong them, so a filter never sees the token the action runs with. The rest of the environment is inherited. - Bounded per call: 60s, 8MB of stdout, 256KB of stderr into the run log. It runs once per distinct approved commit.
- A path in the same repository as the workflow is still a program someone can change. Treat it the way you would treat any other program in the review path.
A CLI tool is available which provides some utilities for working with .codeowners files.
You can download the built tools via releases or build from source:
go build -o codeowners-cli ./tools/cliAvailable subcommands are:
unownedto check for unowned filesownerto check who owns a specific file or list of filesvalidateto check for typos in a.codeownersfile
See CONTRIBUTING.md
- Inline ownership comments for having owners for specific functions, classes, etc.
