diff --git a/Gemfile b/Gemfile index a3e75c4..56582cf 100644 --- a/Gemfile +++ b/Gemfile @@ -2,4 +2,4 @@ source 'https://rubygems.org' gemspec -gem 'rake-compiler', '~> 1.3.0' \ No newline at end of file +gem 'rake-compiler', '~> 1.3.0' diff --git a/README.md b/README.md index 14b8145..1f62851 100644 --- a/README.md +++ b/README.md @@ -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}' @@ -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 @@ -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 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: @@ -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 @@ -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/* @@ -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 @@ -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 @@ -141,19 +160,22 @@ 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`. @@ -161,14 +183,15 @@ If `codeowners_path` is set in `code_ownership.yml` codeowners will use that pat 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}' @@ -178,13 +201,24 @@ 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 @@ -192,10 +226,12 @@ bin/codeownership validate 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 diff --git a/lib/code_ownership.rb b/lib/code_ownership.rb index 3116fad..4df4bb0 100644 --- a/lib/code_ownership.rb +++ b/lib/code_ownership.rb @@ -2,7 +2,6 @@ # typed: strict -require 'set' require 'code_teams' require 'sorbet-runtime' require 'json' diff --git a/lib/code_ownership/cli.rb b/lib/code_ownership/cli.rb index 79d776e..605682c 100644 --- a/lib/code_ownership/cli.rb +++ b/lib/code_ownership/cli.rb @@ -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 @@ -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 diff --git a/lib/code_ownership/code_ownership.bundle b/lib/code_ownership/code_ownership.bundle index f8d94bb..1ea578d 100755 Binary files a/lib/code_ownership/code_ownership.bundle and b/lib/code_ownership/code_ownership.bundle differ diff --git a/lib/code_ownership/private/for_file_output_builder.rb b/lib/code_ownership/private/for_file_output_builder.rb index 7fa75d1..ab7fac7 100644 --- a/lib/code_ownership/private/for_file_output_builder.rb +++ b/lib/code_ownership/private/for_file_output_builder.rb @@ -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 } diff --git a/lib/code_ownership/version.rb b/lib/code_ownership/version.rb index a0c7708..edd7d2c 100644 --- a/lib/code_ownership/version.rb +++ b/lib/code_ownership/version.rb @@ -2,5 +2,5 @@ # frozen_string_literal: true module CodeOwnership - VERSION = '2.0.0' + VERSION = '2.1.0' end diff --git a/spec/lib/code_ownership/cli_spec.rb b/spec/lib/code_ownership/cli_spec.rb index 9950574..e0c5b78 100644 --- a/spec/lib/code_ownership/cli_spec.rb +++ b/spec/lib/code_ownership/cli_spec.rb @@ -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'] }