Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ source 'https://rubygems.org'

gemspec

gem 'rake-compiler', '~> 1.3.0'
gem 'rake-compiler', '~> 1.3.0'
64 changes: 50 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ Check out [`lib/code_ownership.rb`](https://github.com/rubyatscale/code_ownershi

Check out [`code_ownership_spec.rb`](https://github.com/rubyatscale/code_ownership/blob/main/spec/lib/code_ownership_spec.rb) to see examples of how code ownership is used.

There is also a [companion VSCode Extension]([url](https://github.com/rubyatscale/code-ownership-vscode)) for this gem. Just search `Gusto.code-ownership-vscode` in the VSCode Extension Marketplace.
There is also a [companion VSCode Extension](https://github.com/rubyatscale/code-ownership-vscode) 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:
1. Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:

```yml
owned_globs:
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
Expand All @@ -23,33 +24,42 @@ unowned_globs:
- app/services/some_file2.rb
- frontend/javascripts/**/__generated__/**/*
```
2) Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:

2. Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:

```yml
name: Operations
github:
team: '@my-org/operations-team'
```
3) 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.
4) 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.

3. 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.
4. 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 five 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:

```ruby
# @team MyTeam
```

### Package-Based Ownership

Package based ownership integrates [`packwerk`](https://github.com/Shopify/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:

```yml
enforce_dependency: true
enforce_privacy: true
Expand All @@ -58,16 +68,19 @@ metadata:
```

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

```yml
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.
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`](https://github.com/rubyatscale/code_teams)), you can set `owned_globs` to be a glob of files your team owns. For example, in `my_team.yml`:

```yml
name: My Team
owned_globs:
Expand All @@ -78,6 +91,7 @@ unowned_globs:
```

### 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:

```json
Expand All @@ -91,6 +105,7 @@ Javascript package based ownership allows you to specify an ownership key in a `
```

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

```yml
js_package_paths:
- frontend/javascripts/packages/*
Expand All @@ -101,12 +116,15 @@ This defaults `**/`, which makes it look for `package.json` files across your ap

> [!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.

```yml
js_package_paths: []
```

## 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.

```ruby
Expand All @@ -118,6 +136,7 @@ Contributor note: If you are making updates to this method or the methods gettin
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`.

```ruby
Expand All @@ -141,34 +160,38 @@ Under the hood, this finds the file where the class is defined and returns the o
See `code_ownership_spec.rb` for an example.

### `for_team`

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

```ruby
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
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.
A `CODEOWNERS` file defines who owns specific files or paths in a repository. When you run `codeownership validate`, a `.github/CODEOWNERS` file will automatically be generated and updated.

If `codeowners_path` is set in `code_ownership.yml` codeowners will use that path to generate the `CODEOWNERS` file. For example, `codeowners_path: docs` will generate `docs/CODEOWNERS`.

## 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.
3) 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`.
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`.

```yml
owned_globs:
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
Expand All @@ -178,24 +201,37 @@ unowned_globs:
- app/services/some_file2.rb
- frontend/javascripts/**/__generated__/**/*
```

You can call the validation function with the Ruby API

```ruby
CodeOwnership.validate!
```

or the CLI
```
bin/codeownership validate

```bash
# Validate all files
codeownership validate

# Validate specific files
codeownership validate path/to/file1.rb path/to/file2.rb

# Validate only staged files
codeownership validate --diff
```

## Development

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

## Running specs

```sh
bundle install
bundle exec rake
```

## Creating a new release

Simply [create a new release](https://github.com/rubyatscale/code_ownership/releases/new) with github. The release tag must match the gem version
1 change: 0 additions & 1 deletion lib/code_ownership.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

# typed: strict

require 'set'
require 'code_teams'
require 'sorbet-runtime'
require 'json'
Expand Down
15 changes: 13 additions & 2 deletions lib/code_ownership/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def self.validate!(argv)
options = {}

parser = OptionParser.new do |opts|
opts.banner = "Usage: #{EXECUTABLE} validate [options]"
opts.banner = "Usage: #{EXECUTABLE} validate [options] [files...]"

opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
options[:skip_autocorrect] = true
Expand All @@ -59,11 +59,22 @@ def self.validate!(argv)
args = parser.order!(argv)
parser.parse!(args)

files = if options[:diff]
# Collect any remaining arguments as file paths
specified_files = argv.reject { |arg| arg.start_with?('--') }

files = if !specified_files.empty?
# Files explicitly provided on command line
if options[:diff]
warn 'Warning: Ignoring --diff flag because explicit files were provided'
end
specified_files.select { |file| File.exist?(file) }
elsif options[:diff]
# Staged files from git
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
File.exist?(file)
end
else
# No files specified, validate all
nil
end

Expand Down
Binary file modified lib/code_ownership/code_ownership.bundle
Binary file not shown.
1 change: 1 addition & 0 deletions lib/code_ownership/private/for_file_output_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module CodeOwnership
module Private
class ForFileOutputBuilder
extend T::Sig

private_class_method :new

sig { params(file_path: String, json: T::Boolean, verbose: T::Boolean).void }
Expand Down
2 changes: 1 addition & 1 deletion lib/code_ownership/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# frozen_string_literal: true

module CodeOwnership
VERSION = '2.0.0'
VERSION = '2.1.0'
end
42 changes: 42 additions & 0 deletions spec/lib/code_ownership/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,48 @@
end
end

context 'with explicit file arguments' do
let(:argv) { ['validate', 'app/services/my_file.rb', 'frontend/javascripts/my_file.jsx'] }

it 'validates only the specified files' do
expect(CodeOwnership).to receive(:validate!) do |args|
expect(args[:files]).to match_array(['app/services/my_file.rb', 'frontend/javascripts/my_file.jsx'])
expect(args[:autocorrect]).to eq true
expect(args[:stage_changes]).to eq true
end
subject
end

context 'with options' do
let(:argv) { ['validate', '--skip-autocorrect', '--skip-stage', 'app/services/my_file.rb'] }

it 'passes the options correctly' do
expect(CodeOwnership).to receive(:validate!) do |args|
expect(args[:files]).to eq(['app/services/my_file.rb'])
expect(args[:autocorrect]).to eq false
expect(args[:stage_changes]).to eq false
end
subject
end
end

context 'when combined with --diff flag' do
let(:argv) { ['validate', '--diff', 'app/services/my_file.rb'] }

before do
allow(ENV).to receive(:fetch).and_call_original
allow(ENV).to receive(:fetch).with('CODEOWNERS_GIT_STAGED_FILES').and_return('other_file.rb')
end

it 'prioritizes explicit files over git diff' do
expect(CodeOwnership).to receive(:validate!) do |args|
expect(args[:files]).to eq(['app/services/my_file.rb'])
end
subject
end
end
end

context 'with --diff argument' do
let(:argv) { ['validate', '--diff'] }

Expand Down