Skip to content

rubyatscale/code_ownership

Repository files navigation

CodeOwnership

This gem helps engineering teams declare ownership of code. This gem works best in large, usually monolithic code bases where many teams work together.

Check out lib/code_ownership.rb to see the public API.

Check out code_ownership_spec.rb to see examples of how code ownership is used.

There is also a companion VSCode Extension for this gem. Just search Gusto.code-ownership-vscode in the VSCode Extension Marketplace.

Getting started

To get started there's a few things you should do.

  1. Create a config/code_ownership.yml file and declare where your files live. Here's a sample to start with:
owned_globs:
  - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
js_package_paths: []
unowned_globs:
  - db/**/*
  - app/services/some_file1.rb
  - app/services/some_file2.rb
  - frontend/javascripts/**/__generated__/**/*
  1. Declare some teams. Here's an example, that would live at config/teams/operations.yml:
name: Operations
github:
  team: '@my-org/operations-team'
  1. Declare ownership. You can do this at a directory level or at a file level. All of the files within the owned_globs you declared in step 1 will need to have an owner assigned (or be opted out via unowned_globs). See the next section for more detail.
  2. Run validations when you commit, and/or in CI. If you run validations in CI, ensure that if your .github/CODEOWNERS file gets changed, that gets pushed to the PR.

Usage: Declaring Ownership

There are three ways to declare code ownership using this gem.

Directory-Based Ownership

Directory based ownership allows for all files in that directory and all its sub-directories to be owned by one team. To define this, add a .codeowner file inside that directory with the name of the team as the contents of that file.

Team

File-Annotation Based Ownership

File annotations are a last resort if there is no clear home for your code. File annotations go at the top of your file, and look like this:

# @team MyTeam

Package-Based Ownership

Package based ownership integrates packwerk and has ownership defined per package. To define that all files within a package are owned by one team, configure your package.yml like this:

enforce_dependency: true
enforce_privacy: true
metadata:
  owner: Team

You can also define owner as a top-level key, e.g.

enforce_dependency: true
enforce_privacy: true
owner: Team

To do this, add code_ownership to the require key of your packwerk.yml. See https://github.com/Shopify/packwerk/blob/main/USAGE.md#loading-extensions for more information.

Glob-Based Ownership

In your team's configured YML (see code_teams), you can set owned_globs to be a glob of files your team owns. For example, in my_team.yml:

name: My Team
owned_globs:
  - app/services/stuff_belonging_to_my_team/**/**
  - app/controllers/other_stuff_belonging_to_my_team/**/**

Javascript Package Ownership

Javascript package based ownership allows you to specify an ownership key in a package.json. To use this, configure your package.json like this:

{
  // other keys
  "metadata": {
    "owner": "My Team"
  }
  // other keys
}

You can also tell code_ownership where to find JS packages in the configuration, like this:

js_package_paths:
  - frontend/javascripts/packages/*
  - frontend/other_location_for_packages/*

This defaults **/, which makes it look for package.json files across your application.

Note

Javscript package ownership does not respect unowned_globs. If you wish to disable usage of this feature you can set js_package_paths to an empty list.

js_package_paths: []

Custom Ownership

To enable custom ownership, you can inject your own custom classes into code_ownership. To do this, first create a class that adheres to the CodeOwnership::Mapper and/or CodeOwnership::Validator interface. Then, in config/code_ownership.yml, you can require that file:

require:
  - ./lib/my_extension.rb

Now, bin/codeownership validate will automatically include your new mapper and/or validator. See `spec/lib/code_ownership/private/extension_loader_spec.rb for an example of what this looks like.

Usage: Reading CodeOwnership

for_file

CodeOwnership.for_file, given a relative path to a file returns a CodeTeams::Team if there is a team that owns the file, nil otherwise.

CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')

Contributor note: If you are making updates to this method or the methods getting used here, please benchmark the performance of the new implementation against the current for both for_files and for_file (with 1, 100, 1000 files).

See code_ownership_spec.rb for examples.

for_backtrace

CodeOwnership.for_backtrace can be given a backtrace and will either return nil, or a CodeTeams::Team.

CodeOwnership.for_backtrace(exception.backtrace)

This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.

See code_ownership_spec.rb for an example.

for_class

CodeOwnership.for_class can be given a class and will either return nil, or a CodeTeams::Team.

CodeOwnership.for_class(MyClass)

Under the hood, this finds the file where the class is defined and returns the owner of that file.

See code_ownership_spec.rb for an example.

for_team

CodeOwnership.for_team can be used to generate an ownership report for a team.

CodeOwnership.for_team('My Team')

You can shovel this into a markdown file for easy viewing using the CLI:

bin/codeownership for_team 'My Team' > tmp/ownership_report.md

Usage: Generating a CODEOWNERS file

A CODEOWNERS file defines who owns specific files or paths in a repository. When you run bin/codeownership validate, a .github/CODEOWNERS file will automatically be generated and updated.

Proper Configuration & Validation

CodeOwnership comes with a validation function to ensure the following things are true:

  1. Only one mechanism is defining file ownership. That is -- you can't have a file annotation on a file owned via package-based or glob-based ownership. This helps make ownership behavior more clear by avoiding concerns about precedence.
  2. All teams referenced as an owner for any file or package is a valid team (i.e. it's in the list of CodeTeams.all).
  3. All files have ownership. You can specify in unowned_globs to represent a TODO list of files to add ownership to.
  4. The .github/CODEOWNERS file is up to date. This is automatically corrected and staged unless specified otherwise with bin/codeownership validate --skip-autocorrect --skip-stage. You can turn this validation off by setting skip_codeowners_validation: true in config/code_ownership.yml.

CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.

Here is an example config/code_ownership.yml.

owned_globs:
  - '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
unowned_globs:
  - db/**/*
  - app/services/some_file1.rb
  - app/services/some_file2.rb
  - frontend/javascripts/**/__generated__/**/*

You can call the validation function with the Ruby API

CodeOwnership.validate!

or the CLI

bin/codeownership validate

Development

Please add to CHANGELOG.md and this README.md when you make make changes.