Skip to content

About

No description, website, or topics provided.

Resources

Security policy

Stars

9 stars

Watchers

2 watching

Forks

Repository files navigation

Codeowners

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.

🚀 Quick Start: Generate & Validate

The most common workflow is to generate and validate in one step:

codeowners gv
  • Generates a fresh CODEOWNERS file (default: .github/CODEOWNERS)
  • Validates ownership and that the file is up to date
  • Exits non-zero and prints detailed errors if validation fails

Table of Contents

Installation

You can run codeowners without installing a platform-specific binary by using DotSlash, or install from source with Cargo.

Option A: DotSlash (recommended)

  1. Install DotSlash: see https://dotslash-cli.com/docs/installation/. DotSlash files from releases after v0.6.0 fetch the binary by running gh release download, so the GitHub CLI must be on your PATH as well.
  2. Download the codeowners DotSlash text file from the release you want to pin to: https://github.com/rubyatscale/codeowners-rs/releases
  3. Commit that file into your repository (for example at bin/codeowners) and mark it executable with chmod +x.
  4. Run it like any other executable — ./bin/codeowners --help. DotSlash fetches, verifies, and caches the correct binary for the current platform on first use.

Upgrading is a deliberate act: download the DotSlash file from the newer release, replace the committed one, and review the diff. The digests change, so the change is visible in code review.

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.

Why commit the file rather than re-download it

A DotSlash file records the expected size and digest of each platform's release archive, and DotSlash refuses to unpack or execute an artifact that does not match. That check is only worth something if the digest itself is trusted — and the digest is trusted because the file lives in your repository and changes to it go through review. If you re-download the DotSlash file alongside the binary every time, you are trusting whatever the release currently claims, and the digest verifies nothing you did not just fetch from the same place.

This is also DotSlash's own guidance: "the provenance of a DotSlash file is paramount", and DotSlash files "are meant to live in source control" (motivation).

Verifying a release independently (optional)

DotSlash verifies digests but does not check signatures. Release binaries are built by GitHub Actions and carry SLSA build provenance attestations, which you can verify with the GitHub CLI:

gh attestation verify codeowners-mac.tar.gz --repo rubyatscale/codeowners-rs

To additionally require that the attestation came from this repository's release workflow rather than any workflow in it:

gh attestation verify codeowners-mac.tar.gz \
  --repo rubyatscale/codeowners-rs \
  --signer-workflow rubyatscale/codeowners-rs/.github/workflows/ci.yml

For releases after v0.6.0, the digest a DotSlash file records for each platform is the SHA-256 of that platform's archive, which is also the digest the attestation covers. So gh attestation verify on an archive checks the same bytes your committed DotSlash file pins. Earlier DotSlash files record BLAKE3 digests instead.

Releases up to and including v0.6.0 were published before attestation was added, so gh attestation verify reports "no matching attestations found" for them. That is expected for those versions and is not evidence of tampering.

Note that macOS binaries are ad-hoc signed only. They are not signed with an Apple Developer ID and are not notarized. Ad-hoc signing satisfies Apple silicon's requirement that all code be signed, but it does not satisfy Gatekeeper.

Whether that matters depends on how you fetch the binary. DotSlash and curl do not set the com.apple.quarantine attribute, so Gatekeeper never evaluates the binary and it runs normally. If you download the tarball from the releases page in a browser, quarantine is set and Gatekeeper will refuse to run it until you clear the attribute:

xattr -d com.apple.quarantine codeowners

Option B: From source with Cargo

Requires Rust toolchain.

cargo install --git https://github.com/rubyatscale/codeowners-rs codeowners

Getting Started

  1. Configure Ownership
    Create a config/code_ownership.yml file. 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
  2. Declare Teams
    Example: config/teams/operations.yml

    name: Operations
    github:
      team: '@my-org/operations-team'
  3. Run the Main Workflow

    codeowners gv

Declaring Ownership

You can declare code ownership in several ways:

1. Directory-Based Ownership

Add a .codeowner file to a directory with the team name as its contents:

TeamName

2. File Annotation

Add an annotation at the top of a file:

# @team MyTeam
// @team MyTeam
<!-- @team MyTeam -->
<%# @team: Foo %>

3. Package-Based Ownership

In package.yml (for Ruby Packwerk):

owner: TeamName

4. Glob-Based Ownership

In your team's YML:

owned_globs:
  - app/services/my_team/**/*
unowned_globs:
  - app/services/my_team/legacy/*

unowned_globs "subtracts" from owned_globs

5. JavaScript Package Ownership

In package.json:

{
  "metadata": {
    "owner": "My Team"
  }
}

Configure search paths in code_ownership.yml:

js_package_paths:
  - frontend/javascripts/packages/*

CLI Reference

Global Flags

  • --codeowners-file-path <path>: Path for the CODEOWNERS file. Default: ./.github/CODEOWNERS
  • --config-path <path>: Path to code_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

Commands

  • generate (g): Generate the CODEOWNERS file and write it to --codeowners-file-path.
    • Flags: --skip-stage, -s to avoid git add after writing
  • validate [files...] (v): Validate the CODEOWNERS file and configuration. Given files, a fast mode for git hooks, it runs the same checks on just those files and the packages and .codeowner files whose directories contain them, and checks that CODEOWNERS gives each file its owner rather than comparing the whole file.
  • generate-and-validate [files...] (gv): Run generate then validate.
    • Flags: --skip-stage, -s
  • for-file <path> (f): Print the owner of a file.
    • Flags: --from-codeowners to resolve using only the CODEOWNERS rules
  • for-team <name> (t): Print ownership report for a team.
  • delete-cache (d): Delete the persisted cache.

A path given to validate, gv or for-file may be relative to the project root or absolute.

Examples

Find the owner of a file

codeowners for-file path/to/file.rb

Ownership report for a team

codeowners for-team Payroll

Generate but do not stage the file

codeowners generate --skip-stage

Run without using the cache

codeowners gv --no-cache

Configuration

config/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.yml or package.json among 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 in owned_globs that match these globs don't need an owner. It doesn't add to owned_globs, so a file must match both. Unlike unowned_globs, file annotations in them still count, and validate and gv still run every other check, with or without a list of files, so a file there can't have more than one owner or name an invalid team, and given a list of files, validate reports one whose owner CODEOWNERS doesn't list yet. An annotation, .codeowner or package file doesn't count if the file is untracked or in a top-level directory named in ignore_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_globs takes precedence, so a file that matches both is left out. If you use unowned_globs: ['**/*'] only so that files can go unowned, replace it with optionally_owned_globs: ['**/*'] to make annotations work, and run codeowners gv to add a CODEOWNERS line for each annotated file. The files that unowned_globs left 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 in unowned_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 using codeowners-rs via 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 or package.yml. A file over it, or over the limits on total scalar bytes and anchor copies that grow with it, fails with a budget breached error 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 make codeowners use 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 wrapper

See examples in tests/fixtures/**/config/ for reference setups.

Cache

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

Validation

codeowners validate (or codeowners gv) ensures:

  1. Only one mechanism defines ownership for any file.
  2. All referenced teams are valid.
  3. All files in owned_globs are owned, unless matched by unowned_globs or optionally_owned_globs.
  4. The generated CODEOWNERS file is up to date.

Exit status is non-zero on errors.

Library Usage

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),
    }
}

Development

  • 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.md and this README.md when making changes.

Module layout

  • 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 runner Error.
  • src/ownership/: all ownership logic (parsing, mapping, validation, generation).
  • src/ownership/codeowners_query.rs: CODEOWNERS-only queries consumed by the façade.

About

No description, website, or topics provided.

Resources

Security policy

Stars

9 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages