code_ownership 2.0.0.pre.1-x86_64-linux-musl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +201 -0
- data/bin/codeownership +5 -0
- data/lib/code_ownership/3.2/code_ownership.so +0 -0
- data/lib/code_ownership/3.4/code_ownership.so +0 -0
- data/lib/code_ownership/cli.rb +150 -0
- data/lib/code_ownership/private/file_path_finder.rb +63 -0
- data/lib/code_ownership/private/file_path_team_cache.rb +41 -0
- data/lib/code_ownership/private/permit_pack_owner_top_level_key.rb +23 -0
- data/lib/code_ownership/private/team_finder.rb +75 -0
- data/lib/code_ownership/version.rb +5 -0
- data/lib/code_ownership.rb +107 -0
- metadata +218 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: fd5684f7a4168bb6c13168e3a79f09b3c522cad9be2c0cb2065ba6141653d089
|
4
|
+
data.tar.gz: 02f5e746bb2aaa4e660e3616728f41dc50423b46a5340dd12ff95338cf129760
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2b74fe184efa530e06287bf1de94c0078ee98feacce538eebd64ed8b953abeca6440f818542142b8aa4051b0faacee89b0f84d923563ec2c420fa5c2354989a2
|
7
|
+
data.tar.gz: 6e4d43aa1643ca969251f8b3133006650b1baa2ad6befd3efc87ba1121222654b130c755412ba092c331c0cb5ce069a1ce3d35ed2c1e0a82716f51c46b3ce547
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2022 Gusto
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# CodeOwnership
|
2
|
+
|
3
|
+
This gem helps engineering teams declare ownership of code. This gem works best in large, usually monolithic code bases where many teams work together.
|
4
|
+
|
5
|
+
Check out [`lib/code_ownership.rb`](https://github.com/rubyatscale/code_ownership/blob/main/lib/code_ownership.rb) to see the public API.
|
6
|
+
|
7
|
+
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.
|
8
|
+
|
9
|
+
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.
|
10
|
+
|
11
|
+
## Getting started
|
12
|
+
|
13
|
+
To get started there's a few things you should do.
|
14
|
+
|
15
|
+
1) Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:
|
16
|
+
```yml
|
17
|
+
owned_globs:
|
18
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
19
|
+
js_package_paths: []
|
20
|
+
unowned_globs:
|
21
|
+
- db/**/*
|
22
|
+
- app/services/some_file1.rb
|
23
|
+
- app/services/some_file2.rb
|
24
|
+
- frontend/javascripts/**/__generated__/**/*
|
25
|
+
```
|
26
|
+
2) Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
|
27
|
+
```yml
|
28
|
+
name: Operations
|
29
|
+
github:
|
30
|
+
team: '@my-org/operations-team'
|
31
|
+
```
|
32
|
+
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.
|
33
|
+
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.
|
34
|
+
|
35
|
+
## Usage: Declaring Ownership
|
36
|
+
|
37
|
+
There are five ways to declare code ownership using this gem:
|
38
|
+
|
39
|
+
### Directory-Based Ownership
|
40
|
+
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.
|
41
|
+
```
|
42
|
+
Team
|
43
|
+
```
|
44
|
+
|
45
|
+
### File-Annotation Based Ownership
|
46
|
+
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:
|
47
|
+
```ruby
|
48
|
+
# @team MyTeam
|
49
|
+
```
|
50
|
+
|
51
|
+
### Package-Based Ownership
|
52
|
+
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:
|
53
|
+
```yml
|
54
|
+
enforce_dependency: true
|
55
|
+
enforce_privacy: true
|
56
|
+
metadata:
|
57
|
+
owner: Team
|
58
|
+
```
|
59
|
+
|
60
|
+
You can also define `owner` as a top-level key, e.g.
|
61
|
+
```yml
|
62
|
+
enforce_dependency: true
|
63
|
+
enforce_privacy: true
|
64
|
+
owner: Team
|
65
|
+
```
|
66
|
+
|
67
|
+
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.
|
68
|
+
|
69
|
+
### Glob-Based Ownership
|
70
|
+
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`:
|
71
|
+
```yml
|
72
|
+
name: My Team
|
73
|
+
owned_globs:
|
74
|
+
- app/services/stuff_belonging_to_my_team/**/**
|
75
|
+
- app/controllers/other_stuff_belonging_to_my_team/**/**
|
76
|
+
unowned_globs:
|
77
|
+
- app/controllers/other_stuff_belonging_to_my_team/that_one_weird_dir_we_dont_own/*
|
78
|
+
```
|
79
|
+
|
80
|
+
### Javascript Package Ownership
|
81
|
+
Javascript package based ownership allows you to specify an ownership key in a `package.json`. To use this, configure your `package.json` like this:
|
82
|
+
|
83
|
+
```json
|
84
|
+
{
|
85
|
+
// other keys
|
86
|
+
"metadata": {
|
87
|
+
"owner": "My Team"
|
88
|
+
}
|
89
|
+
// other keys
|
90
|
+
}
|
91
|
+
```
|
92
|
+
|
93
|
+
You can also tell `code_ownership` where to find JS packages in the configuration, like this:
|
94
|
+
```yml
|
95
|
+
js_package_paths:
|
96
|
+
- frontend/javascripts/packages/*
|
97
|
+
- frontend/other_location_for_packages/*
|
98
|
+
```
|
99
|
+
|
100
|
+
This defaults `**/`, which makes it look for `package.json` files across your application.
|
101
|
+
|
102
|
+
> [!NOTE]
|
103
|
+
> 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.
|
104
|
+
```yml
|
105
|
+
js_package_paths: []
|
106
|
+
```
|
107
|
+
|
108
|
+
## Usage: Reading CodeOwnership
|
109
|
+
### `for_file`
|
110
|
+
`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.
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
CodeOwnership.for_file('path/to/file/relative/to/application/root.rb')
|
114
|
+
```
|
115
|
+
|
116
|
+
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).
|
117
|
+
|
118
|
+
See `code_ownership_spec.rb` for examples.
|
119
|
+
|
120
|
+
### `for_backtrace`
|
121
|
+
`CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeTeams::Team`.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
CodeOwnership.for_backtrace(exception.backtrace)
|
125
|
+
```
|
126
|
+
|
127
|
+
This will go through the backtrace, and return the first found owner of the files associated with frames within the backtrace.
|
128
|
+
|
129
|
+
See `code_ownership_spec.rb` for an example.
|
130
|
+
|
131
|
+
### `for_class`
|
132
|
+
|
133
|
+
`CodeOwnership.for_class` can be given a class and will either return `nil`, or a `CodeTeams::Team`.
|
134
|
+
|
135
|
+
```ruby
|
136
|
+
CodeOwnership.for_class(MyClass)
|
137
|
+
```
|
138
|
+
|
139
|
+
Under the hood, this finds the file where the class is defined and returns the owner of that file.
|
140
|
+
|
141
|
+
See `code_ownership_spec.rb` for an example.
|
142
|
+
|
143
|
+
### `for_team`
|
144
|
+
`CodeOwnership.for_team` can be used to generate an ownership report for a team.
|
145
|
+
```ruby
|
146
|
+
CodeOwnership.for_team('My Team')
|
147
|
+
```
|
148
|
+
|
149
|
+
You can shovel this into a markdown file for easy viewing using the CLI:
|
150
|
+
```
|
151
|
+
bin/codeownership for_team 'My Team' > tmp/ownership_report.md
|
152
|
+
```
|
153
|
+
|
154
|
+
## Usage: Generating a `CODEOWNERS` file
|
155
|
+
|
156
|
+
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.
|
157
|
+
|
158
|
+
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`.
|
159
|
+
|
160
|
+
## Proper Configuration & Validation
|
161
|
+
|
162
|
+
CodeOwnership comes with a validation function to ensure the following things are true:
|
163
|
+
|
164
|
+
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.
|
165
|
+
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`).
|
166
|
+
3) All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
|
167
|
+
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`.
|
168
|
+
|
169
|
+
CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
|
170
|
+
|
171
|
+
Here is an example `config/code_ownership.yml`.
|
172
|
+
```yml
|
173
|
+
owned_globs:
|
174
|
+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
175
|
+
unowned_globs:
|
176
|
+
- db/**/*
|
177
|
+
- app/services/some_file1.rb
|
178
|
+
- app/services/some_file2.rb
|
179
|
+
- frontend/javascripts/**/__generated__/**/*
|
180
|
+
```
|
181
|
+
You can call the validation function with the Ruby API
|
182
|
+
```ruby
|
183
|
+
CodeOwnership.validate!
|
184
|
+
```
|
185
|
+
or the CLI
|
186
|
+
```
|
187
|
+
bin/codeownership validate
|
188
|
+
```
|
189
|
+
|
190
|
+
## Development
|
191
|
+
|
192
|
+
Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
|
193
|
+
|
194
|
+
## Running specs
|
195
|
+
```sh
|
196
|
+
bundle install
|
197
|
+
bundle exec rake
|
198
|
+
```
|
199
|
+
|
200
|
+
## Creating a new release
|
201
|
+
Simply [create a new release](https://github.com/rubyatscale/code_ownership/releases/new) with github. The release tag must match the gem version
|
data/bin/codeownership
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# typed: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'pathname'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module CodeOwnership
|
8
|
+
class Cli
|
9
|
+
EXECUTABLE = 'bin/codeownership'
|
10
|
+
|
11
|
+
def self.run!(argv)
|
12
|
+
command = argv.shift
|
13
|
+
if command == 'validate'
|
14
|
+
validate!(argv)
|
15
|
+
elsif command == 'for_file'
|
16
|
+
for_file(argv)
|
17
|
+
elsif command == 'for_team'
|
18
|
+
for_team(argv)
|
19
|
+
elsif command == 'version'
|
20
|
+
version
|
21
|
+
elsif [nil, 'help'].include?(command)
|
22
|
+
puts <<~USAGE
|
23
|
+
Usage: #{EXECUTABLE} <subcommand>
|
24
|
+
|
25
|
+
Subcommands:
|
26
|
+
validate - run all validations
|
27
|
+
for_file - find code ownership for a single file
|
28
|
+
for_team - find code ownership information for a team
|
29
|
+
help - display help information about code_ownership
|
30
|
+
USAGE
|
31
|
+
else
|
32
|
+
puts "'#{command}' is not a code_ownership command. See `#{EXECUTABLE} help`."
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.validate!(argv)
|
37
|
+
options = {}
|
38
|
+
|
39
|
+
parser = OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: #{EXECUTABLE} validate [options]"
|
41
|
+
|
42
|
+
opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
|
43
|
+
options[:skip_autocorrect] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on('-d', '--diff', 'Only run validations with staged files') do
|
47
|
+
options[:diff] = true
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on('-s', '--skip-stage', 'Skips staging the CODEOWNERS file') do
|
51
|
+
options[:skip_stage] = true
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on('--help', 'Shows this prompt') do
|
55
|
+
puts opts
|
56
|
+
exit
|
57
|
+
end
|
58
|
+
end
|
59
|
+
args = parser.order!(argv)
|
60
|
+
parser.parse!(args)
|
61
|
+
|
62
|
+
files = if options[:diff]
|
63
|
+
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
64
|
+
File.exist?(file)
|
65
|
+
end
|
66
|
+
else
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
|
70
|
+
CodeOwnership.validate!(
|
71
|
+
files: files,
|
72
|
+
autocorrect: !options[:skip_autocorrect],
|
73
|
+
stage_changes: !options[:skip_stage]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.version
|
78
|
+
puts CodeOwnership.version.join("\n")
|
79
|
+
end
|
80
|
+
|
81
|
+
# For now, this just returns team ownership
|
82
|
+
# Later, this could also return code ownership errors about that file.
|
83
|
+
def self.for_file(argv)
|
84
|
+
options = {}
|
85
|
+
|
86
|
+
# Long-term, we probably want to use something like `thor` so we don't have to implement logic
|
87
|
+
# like this. In the short-term, this is a simple way for us to use the built-in OptionParser
|
88
|
+
# while having an ergonomic CLI.
|
89
|
+
files = argv.reject { |arg| arg.start_with?('--') }
|
90
|
+
|
91
|
+
parser = OptionParser.new do |opts|
|
92
|
+
opts.banner = "Usage: #{EXECUTABLE} for_file [options]"
|
93
|
+
|
94
|
+
opts.on('--json', 'Output as JSON') do
|
95
|
+
options[:json] = true
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on('--help', 'Shows this prompt') do
|
99
|
+
puts opts
|
100
|
+
exit
|
101
|
+
end
|
102
|
+
end
|
103
|
+
args = parser.order!(argv)
|
104
|
+
parser.parse!(args)
|
105
|
+
|
106
|
+
if files.count != 1
|
107
|
+
raise "Please pass in one file. Use `#{EXECUTABLE} for_file --help` for more info"
|
108
|
+
end
|
109
|
+
|
110
|
+
team = CodeOwnership.for_file(files.first)
|
111
|
+
|
112
|
+
team_name = team&.name || 'Unowned'
|
113
|
+
team_yml = team&.config_yml || 'Unowned'
|
114
|
+
|
115
|
+
if options[:json]
|
116
|
+
json = {
|
117
|
+
team_name: team_name,
|
118
|
+
team_yml: team_yml
|
119
|
+
}
|
120
|
+
|
121
|
+
puts json.to_json
|
122
|
+
else
|
123
|
+
puts <<~MSG
|
124
|
+
Team: #{team_name}
|
125
|
+
Team YML: #{team_yml}
|
126
|
+
MSG
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.for_team(argv)
|
131
|
+
parser = OptionParser.new do |opts|
|
132
|
+
opts.banner = "Usage: #{EXECUTABLE} for_team 'Team Name'"
|
133
|
+
|
134
|
+
opts.on('--help', 'Shows this prompt') do
|
135
|
+
puts opts
|
136
|
+
exit
|
137
|
+
end
|
138
|
+
end
|
139
|
+
teams = argv.reject { |arg| arg.start_with?('--') }
|
140
|
+
args = parser.order!(argv)
|
141
|
+
parser.parse!(args)
|
142
|
+
|
143
|
+
if teams.count != 1
|
144
|
+
raise "Please pass in one team. Use `#{EXECUTABLE} for_team --help` for more info"
|
145
|
+
end
|
146
|
+
|
147
|
+
puts CodeOwnership.for_team(teams.first).join("\n")
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module FilePathFinder
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
# Returns a string version of the relative path to a Rails constant,
|
14
|
+
# or nil if it can't find anything
|
15
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(String)) }
|
16
|
+
def path_from_klass(klass)
|
17
|
+
if klass
|
18
|
+
path = Object.const_source_location(klass.to_s)&.first
|
19
|
+
(path && Pathname.new(path).relative_path_from(Pathname.pwd).to_s) || nil
|
20
|
+
end
|
21
|
+
rescue NameError
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[String]) }
|
26
|
+
def from_backtrace(backtrace)
|
27
|
+
return [] unless backtrace
|
28
|
+
|
29
|
+
# The pattern for a backtrace hasn't changed in forever and is considered
|
30
|
+
# stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
|
31
|
+
#
|
32
|
+
# This pattern matches a line like the following:
|
33
|
+
#
|
34
|
+
# ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
|
35
|
+
#
|
36
|
+
backtrace_line = if RUBY_VERSION >= '3.4.0'
|
37
|
+
%r{\A(#{Pathname.pwd}/|\./)?
|
38
|
+
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
|
39
|
+
:
|
40
|
+
(?<line>\d+) # Matches '43'
|
41
|
+
:in\s
|
42
|
+
'(?<function>.*)' # Matches "`block (3 levels) in create'"
|
43
|
+
\z}x
|
44
|
+
else
|
45
|
+
%r{\A(#{Pathname.pwd}/|\./)?
|
46
|
+
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
|
47
|
+
:
|
48
|
+
(?<line>\d+) # Matches '43'
|
49
|
+
:in\s
|
50
|
+
`(?<function>.*)' # Matches "`block (3 levels) in create'"
|
51
|
+
\z}x
|
52
|
+
end
|
53
|
+
|
54
|
+
backtrace.lazy.filter_map do |line|
|
55
|
+
match = line.match(backtrace_line)
|
56
|
+
next unless match
|
57
|
+
|
58
|
+
T.must(match[:file])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module FilePathTeamCache
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
|
14
|
+
def get(file_path)
|
15
|
+
cache[file_path]
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(file_path: String, team: T.nilable(CodeTeams::Team)).void }
|
19
|
+
def set(file_path, team)
|
20
|
+
cache[file_path] = team
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(file_path: String).returns(T::Boolean) }
|
24
|
+
def cached?(file_path)
|
25
|
+
cache.key?(file_path)
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { void }
|
29
|
+
def bust_cache!
|
30
|
+
@cache = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
|
34
|
+
def cache
|
35
|
+
@cache ||= T.let(@cache,
|
36
|
+
T.nilable(T::Hash[String, T.nilable(CodeTeams::Team)]))
|
37
|
+
@cache ||= {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'packwerk'
|
5
|
+
|
6
|
+
module CodeOwnership
|
7
|
+
module Private
|
8
|
+
class PackOwnershipValidator
|
9
|
+
extend T::Sig
|
10
|
+
include Packwerk::Validator
|
11
|
+
|
12
|
+
sig { override.params(package_set: Packwerk::PackageSet, configuration: Packwerk::Configuration).returns(Result) }
|
13
|
+
def call(package_set, configuration)
|
14
|
+
Result.new(ok: true)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { override.returns(T::Array[String]) }
|
18
|
+
def permitted_keys
|
19
|
+
%w[owner]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
module CodeOwnership
|
6
|
+
module Private
|
7
|
+
module TeamFinder
|
8
|
+
module_function
|
9
|
+
|
10
|
+
extend T::Sig
|
11
|
+
extend T::Helpers
|
12
|
+
|
13
|
+
requires_ancestor { Kernel }
|
14
|
+
|
15
|
+
sig { params(file_path: String).returns(T.nilable(CodeTeams::Team)) }
|
16
|
+
def for_file(file_path)
|
17
|
+
return nil if file_path.start_with?('./')
|
18
|
+
|
19
|
+
return FilePathTeamCache.get(file_path) if FilePathTeamCache.cached?(file_path)
|
20
|
+
|
21
|
+
result = T.let(RustCodeOwners.for_file(file_path), T.nilable(T::Hash[Symbol, String]))
|
22
|
+
return if result.nil?
|
23
|
+
|
24
|
+
if result[:team_name].nil?
|
25
|
+
FilePathTeamCache.set(file_path, nil)
|
26
|
+
else
|
27
|
+
FilePathTeamCache.set(file_path, T.let(find_team!(T.must(result[:team_name])), T.nilable(CodeTeams::Team)))
|
28
|
+
end
|
29
|
+
|
30
|
+
FilePathTeamCache.get(file_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
|
34
|
+
def for_class(klass)
|
35
|
+
file_path = FilePathFinder.path_from_klass(klass)
|
36
|
+
return nil if file_path.nil?
|
37
|
+
|
38
|
+
for_file(file_path)
|
39
|
+
end
|
40
|
+
|
41
|
+
sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
|
42
|
+
def for_package(package)
|
43
|
+
owner_name = package.raw_hash['owner'] || package.metadata['owner']
|
44
|
+
return nil if owner_name.nil?
|
45
|
+
|
46
|
+
find_team!(owner_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
|
50
|
+
def for_backtrace(backtrace, excluded_teams: [])
|
51
|
+
first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)&.first
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
|
55
|
+
def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
|
56
|
+
FilePathFinder.from_backtrace(backtrace).each do |file|
|
57
|
+
team = for_file(file)
|
58
|
+
if team && !excluded_teams.include?(team)
|
59
|
+
return [team, file]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
sig { params(team_name: String).returns(CodeTeams::Team) }
|
67
|
+
def find_team!(team_name)
|
68
|
+
CodeTeams.find(team_name) ||
|
69
|
+
raise(StandardError, "Could not find team with name: `#{team_name}`. Make sure the team is one of `#{CodeTeams.all.map(&:name).sort}`")
|
70
|
+
end
|
71
|
+
|
72
|
+
private_class_method(:find_team!)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
require 'code_teams'
|
7
|
+
require 'sorbet-runtime'
|
8
|
+
require 'json'
|
9
|
+
require 'packs-specification'
|
10
|
+
require 'code_ownership/version'
|
11
|
+
require 'code_ownership/private/file_path_finder'
|
12
|
+
require 'code_ownership/private/file_path_team_cache'
|
13
|
+
require 'code_ownership/private/team_finder'
|
14
|
+
require 'code_ownership/cli'
|
15
|
+
|
16
|
+
begin
|
17
|
+
RUBY_VERSION =~ /(\d+\.\d+)/
|
18
|
+
require "code_ownership/#{Regexp.last_match(1)}/code_ownership"
|
19
|
+
rescue LoadError
|
20
|
+
require 'code_ownership/code_ownership'
|
21
|
+
end
|
22
|
+
|
23
|
+
if defined?(Packwerk)
|
24
|
+
require 'code_ownership/private/permit_pack_owner_top_level_key'
|
25
|
+
end
|
26
|
+
|
27
|
+
module CodeOwnership
|
28
|
+
module_function
|
29
|
+
|
30
|
+
extend T::Sig
|
31
|
+
extend T::Helpers
|
32
|
+
|
33
|
+
requires_ancestor { Kernel }
|
34
|
+
GlobsToOwningTeamMap = T.type_alias { T::Hash[String, CodeTeams::Team] }
|
35
|
+
|
36
|
+
sig { returns(T::Array[String]) }
|
37
|
+
def version
|
38
|
+
["code_ownership version: #{VERSION}",
|
39
|
+
"codeowners-rs version: #{::RustCodeOwners.version}"]
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
|
43
|
+
def for_file(file)
|
44
|
+
Private::TeamFinder.for_file(file)
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { params(team: T.any(CodeTeams::Team, String)).returns(T::Array[String]) }
|
48
|
+
def for_team(team)
|
49
|
+
team = T.must(CodeTeams.find(team)) if team.is_a?(String)
|
50
|
+
::RustCodeOwners.for_team(team.name)
|
51
|
+
end
|
52
|
+
|
53
|
+
class InvalidCodeOwnershipConfigurationError < StandardError
|
54
|
+
end
|
55
|
+
|
56
|
+
sig do
|
57
|
+
params(
|
58
|
+
autocorrect: T::Boolean,
|
59
|
+
stage_changes: T::Boolean,
|
60
|
+
files: T.nilable(T::Array[String])
|
61
|
+
).void
|
62
|
+
end
|
63
|
+
def validate!(
|
64
|
+
autocorrect: true,
|
65
|
+
stage_changes: true,
|
66
|
+
files: nil
|
67
|
+
)
|
68
|
+
if autocorrect
|
69
|
+
::RustCodeOwners.generate_and_validate(!stage_changes)
|
70
|
+
else
|
71
|
+
::RustCodeOwners.validate
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
76
|
+
# first line that corresponds to a file with assigned ownership
|
77
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
|
78
|
+
def for_backtrace(backtrace, excluded_teams: [])
|
79
|
+
Private::TeamFinder.for_backtrace(backtrace, excluded_teams: excluded_teams)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
83
|
+
# first owned file in it, useful for figuring out which file is being blamed.
|
84
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable([::CodeTeams::Team, String])) }
|
85
|
+
def first_owned_file_for_backtrace(backtrace, excluded_teams: [])
|
86
|
+
Private::TeamFinder.first_owned_file_for_backtrace(backtrace, excluded_teams: excluded_teams)
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(::CodeTeams::Team)) }
|
90
|
+
def for_class(klass)
|
91
|
+
Private::TeamFinder.for_class(klass)
|
92
|
+
end
|
93
|
+
|
94
|
+
sig { params(package: Packs::Pack).returns(T.nilable(::CodeTeams::Team)) }
|
95
|
+
def for_package(package)
|
96
|
+
Private::TeamFinder.for_package(package)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
|
100
|
+
# Namely, the set of files, packages, and directories which are tracked for ownership should not change.
|
101
|
+
# The primary reason this is helpful is for clients of CodeOwnership who want to test their code, and each test context
|
102
|
+
# has different ownership and tracked files.
|
103
|
+
sig { void }
|
104
|
+
def self.bust_caches!
|
105
|
+
Private::FilePathTeamCache.bust_cache!
|
106
|
+
end
|
107
|
+
end
|
metadata
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: code_ownership
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 2.0.0.pre.1
|
5
|
+
platform: x86_64-linux-musl
|
6
|
+
authors:
|
7
|
+
- Gusto Engineers
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: code_teams
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: packs-specification
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorbet-runtime
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.5.11249
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.5.11249
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: debug
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: packwerk
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: railties
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rake
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '3.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '3.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sorbet
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: tapioca
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: A gem to help engineering teams declare ownership of code
|
168
|
+
email:
|
169
|
+
- dev@gusto.com
|
170
|
+
executables:
|
171
|
+
- codeownership
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- LICENSE
|
176
|
+
- README.md
|
177
|
+
- bin/codeownership
|
178
|
+
- lib/code_ownership.rb
|
179
|
+
- lib/code_ownership/3.2/code_ownership.so
|
180
|
+
- lib/code_ownership/3.4/code_ownership.so
|
181
|
+
- lib/code_ownership/cli.rb
|
182
|
+
- lib/code_ownership/private/file_path_finder.rb
|
183
|
+
- lib/code_ownership/private/file_path_team_cache.rb
|
184
|
+
- lib/code_ownership/private/permit_pack_owner_top_level_key.rb
|
185
|
+
- lib/code_ownership/private/team_finder.rb
|
186
|
+
- lib/code_ownership/version.rb
|
187
|
+
homepage: https://github.com/rubyatscale/code_ownership
|
188
|
+
licenses:
|
189
|
+
- MIT
|
190
|
+
metadata:
|
191
|
+
homepage_uri: https://github.com/rubyatscale/code_ownership
|
192
|
+
source_code_uri: https://github.com/rubyatscale/code_ownership
|
193
|
+
changelog_uri: https://github.com/rubyatscale/code_ownership/releases
|
194
|
+
allowed_push_host: https://rubygems.org
|
195
|
+
cargo_crate_name: code_ownership
|
196
|
+
post_install_message:
|
197
|
+
rdoc_options: []
|
198
|
+
require_paths:
|
199
|
+
- lib
|
200
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '3.2'
|
205
|
+
- - "<"
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: 3.5.dev
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: 3.3.22
|
213
|
+
requirements: []
|
214
|
+
rubygems_version: 3.5.23
|
215
|
+
signing_key:
|
216
|
+
specification_version: 4
|
217
|
+
summary: A gem to help engineering teams declare ownership of code
|
218
|
+
test_files: []
|