Codeowners is a fast, Rust-based CLI for generating and validating GitHub CODEOWNERS files in large repositories.
Note: For Ruby applications, it's usually easier to use codeowners-rs via the code_ownership gem.
The most common workflow is to generate and validate in one step:
codeowners gv- Generates a fresh
CODEOWNERSfile (default:.github/CODEOWNERS) - Validates ownership and that the file is up to date
- Exits non-zero and prints detailed errors if validation fails
- Quick Start: Generate & Validate
- Installation
- Getting Started
- Declaring Ownership
- CLI Reference
- Configuration
- Cache
- Validation
- Library Usage
- Development
You can run codeowners without installing a platform-specific binary by using DotSlash, or install from source with Cargo.
- Install DotSlash: see https://dotslash-cli.com/docs/installation/
- Download the latest DotSlash text file from a release, for example https://github.com/rubyatscale/codeowners-rs/releases.
- Execute the downloaded file with DotSlash; it will fetch and run the correct binary.
Release archives after v0.5.0 also hold a licenses directory next to the codeowners binary. It has the license notices that the dependencies listed in dev/license-notices require redistributions of the binary to include.
Requires Rust toolchain.
cargo install --git https://github.com/rubyatscale/codeowners-rs codeowners-
Configure Ownership
Create aconfig/code_ownership.ymlfile. Example:owned_globs: - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}' js_package_paths: [] unowned_globs: - db/**/* - frontend/javascripts/**/__generated__/**/* optionally_owned_globs: - app/services/some_file1.rb
-
Declare Teams
Example:config/teams/operations.ymlname: Operations github: team: '@my-org/operations-team'
-
Run the Main Workflow
codeowners gv
You can declare code ownership in several ways:
Add a .codeowner file to a directory with the team name as its contents:
TeamName
Add an annotation at the top of a file:
# @team MyTeam// @team MyTeam<!-- @team MyTeam --><%# @team: Foo %>In package.yml (for Ruby Packwerk):
owner: TeamNameIn your team's YML:
owned_globs:
- app/services/my_team/**/*
unowned_globs:
- app/services/my_team/legacy/*unowned_globs "subtracts" from owned_globs
In package.json:
{
"metadata": {
"owner": "My Team"
}
}Configure search paths in code_ownership.yml:
js_package_paths:
- frontend/javascripts/packages/*--codeowners-file-path <path>: Path for the CODEOWNERS file. Default:./.github/CODEOWNERS--config-path <path>: Path tocode_ownership.yml. Default:./config/code_ownership.yml--project-root <path>: Project root. Default:.--no-cache: Disable on-disk caching (useful in CI)-V, --version,-h, --help
generate(g): Generate the CODEOWNERS file and write it to--codeowners-file-path.- Flags:
--skip-stage, -sto avoidgit addafter writing
- Flags:
validate(v): Validate the CODEOWNERS file and configuration.generate-and-validate(gv): Rungeneratethenvalidate.- Flags:
--skip-stage, -s
- Flags:
for-file <path>(f): Print the owner of a file.- Flags:
--from-codeownersto resolve using only the CODEOWNERS rules
- Flags:
for-team <name>(t): Print ownership report for a team.delete-cache(d): Delete the persisted cache.
codeowners for-file path/to/file.rbcodeowners for-team Payrollcodeowners generate --skip-stagecodeowners gv --no-cacheconfig/code_ownership.yml keys and defaults:
owned_globs(required): Glob patterns that must be owned.ruby_package_paths(default:['packs/**/*', 'components/**'])js_package_paths/javascript_package_paths(default:['frontend/**/*'])team_file_glob(default:['config/teams/**/*.yml'])unowned_globs(default:['frontend/**/node_modules/**/*', 'frontend/**/__generated__/**/*']): Files matched here are left out of the project: they don't need an owner, validation doesn't check them, and file annotations in them are ignored. A.codeowner,package.ymlorpackage.jsonamong them is still read, though, so its owner still goes into CODEOWNERS and an invalid team in it is still reported. A team file among them is still read too, so it still defines its team and keeps its own line in CODEOWNERS. Use this for third-party or generated code.optionally_owned_globs(default:[], added in 0.6.0): Files inowned_globsthat match these globs don't need an owner. It doesn't add toowned_globs, so a file must match both. Unlikeunowned_globs, file annotations in them still count, and a fullvalidateorgvstill runs every other check, so a file there can't have more than one owner or name an invalid team. Given a list of files, as in a git hook,validateandgvlook each one up in CODEOWNERS and don't check for more than one owner. An optionally owned file with no line passes only if nothing names an owner for it, so one whose annotation,.codeowneror package names a team that doesn't exist, or whose owner CODEOWNERS doesn't list yet, is reported under "Files matching optionally_owned_globs name an owner that CODEOWNERS doesn't list". As in a fullvalidate, an annotation,.codeowneror package file doesn't count if the file is untracked or in a top-level directory named inignore_dirs, so a new file's own annotation counts once the file is staged. Use['**/*']if only some of your code has owners, or scope it, e.g.['**/deprecated/**/*'].unowned_globstakes precedence, so a file that matches both is left out. If you useunowned_globs: ['**/*']only so that files can go unowned, replace it withoptionally_owned_globs: ['**/*']to make annotations work, and runcodeowners gvto add a CODEOWNERS line for each annotated file. The files thatunowned_globsleft out are then read and checked like any other owned file, which takes longer and may turn up problems that were hidden before. Keep third-party and generated code inunowned_globs. Versions before 0.6.0 ignore the key without a warning, so they still report unowned files in these globs.vendored_gems_path(default:'vendored/')cache_directory(default:'tmp/cache/codeowners')ignore_dirs(default includes:.git,node_modules,tmp, etc.)executable_name(default:'codeowners generate'): The command that validation errors tell you to run to update the CODEOWNERS file. Useful when usingcodeowners-rsvia wrappers like the code_ownership Ruby gem.yaml_max_nodes(default:20000): The most YAML nodes (mappings, sequences and scalars) allowed in one team file orpackage.yml. A file over it, or over the limits on total scalar bytes and anchor copies that grow with it, fails with abudget breachederror and a hint to raise this key in the config file in use. Raise it if a large team file hits the limit; real files have at most a few thousand nodes. It does not apply to the config file itself, which is always held to the default of 20,000 nodes, about one per list entry. Raising it also raises how much memory a single file can take: a file crafted to waste memory can makecodeownersuse about 280 MB more at the default and about 700 MB more at 50,000, measured on macOS, while ordinary files add almost nothing. Separately, any of these files, or the config, fails if it nests more than 16 levels deep, counting the top level. That limit can't be raised.
Example configuration with custom executable name:
owned_globs:
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
executable_name: 'bin/codeownership validate' # For Ruby gem wrapperSee examples in tests/fixtures/**/config/ for reference setups.
By default, cache is stored under tmp/cache/codeowners relative to the project root. This speeds up repeated runs.
- Disable cache for a run: add the global flag
--no-cache - Clear all cache:
codeowners delete-cache
codeowners validate (or codeowners gv) ensures:
- Only one mechanism defines ownership for any file.
- All referenced teams are valid.
- All files in
owned_globsare owned, unless matched byunowned_globsoroptionally_owned_globs. - The generated
CODEOWNERSfile is up to date.
Exit status is non-zero on errors.
Import public APIs from codeowners::runner::*.
use codeowners::runner::{RunConfig, for_file, teams_for_files_from_codeowners};
fn main() {
let run_config = RunConfig {
project_root: std::path::PathBuf::from("."),
codeowners_file_path: Some(std::path::PathBuf::from(".github/CODEOWNERS")), // optional, if None provided, will be resolved from config/env
config_path: std::path::PathBuf::from("config/code_ownership.yml"),
no_cache: true, // set false to enable on-disk caching
executable_name: None,
};
// Find owner for a single file using the optimized path (not just CODEOWNERS)
let result = for_file(&run_config, "app/models/user.rb", false);
for msg in result.info_messages { println!("{}", msg); }
for err in result.io_errors { eprintln!("io: {}", err); }
for err in result.validation_errors { eprintln!("validation: {}", err); }
// Map multiple files to teams using CODEOWNERS rules only
let files = vec![
"app/models/user.rb".to_string(),
"config/teams/payroll.yml".to_string(),
];
match teams_for_files_from_codeowners(&run_config, &files) {
Ok(map) => println!("{:?}", map),
Err(e) => eprintln!("error: {}", e),
}
}-
Written in Rust for speed and reliability.
-
To build locally, install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
Please update
CHANGELOG.mdand thisREADME.mdwhen making changes.
src/runner.rs: public façade re-exporting the API and types.src/runner/api.rs: externally available functions used by the CLI and other crates.src/runner/types.rs:RunConfig,RunResult, and runnerError.src/ownership/: all ownership logic (parsing, mapping, validation, generation).src/ownership/codeowners_query.rs: CODEOWNERS-only queries consumed by the façade.