code_ownership 2.0.0 → 2.1.0
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 +4 -4
- data/README.md +50 -14
- data/lib/code_ownership/cli.rb +13 -2
- data/lib/code_ownership/private/for_file_output_builder.rb +1 -0
- data/lib/code_ownership/version.rb +1 -1
- data/lib/code_ownership.rb +51 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 523b1156430e253b991257fdada359ab8f0dddec015984c7994c925205412b33
|
|
4
|
+
data.tar.gz: 7033d3b132d70101d78d4ff10602953e0c17bbfa14e830cabe68ce81ea49405d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8dae151b7c6b4e9ef31a632c9e77fae434ec6620df11f15b65f5e5e88b200f95740972cf6b3ccf87d941564dbe16a04cd3a65f7c31d2cfb5778eda211db4d00c
|
|
7
|
+
data.tar.gz: 8cf1f5ae34760260a9ff460684cafad0e8810911bda5cf211fd423582c02fe421bdf0ec33425eebe39e18963361a8150f61ad4e32a78d4af7a323ee052a8f7f2
|
data/README.md
CHANGED
|
@@ -6,13 +6,14 @@ Check out [`lib/code_ownership.rb`](https://github.com/rubyatscale/code_ownershi
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
-
There is also a [companion VSCode Extension](
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
11
|
## Getting started
|
|
12
12
|
|
|
13
13
|
To get started there's a few things you should do.
|
|
14
14
|
|
|
15
|
-
1
|
|
15
|
+
1. Create a `config/code_ownership.yml` file and declare where your files live. Here's a sample to start with:
|
|
16
|
+
|
|
16
17
|
```yml
|
|
17
18
|
owned_globs:
|
|
18
19
|
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
|
@@ -23,33 +24,42 @@ unowned_globs:
|
|
|
23
24
|
- app/services/some_file2.rb
|
|
24
25
|
- frontend/javascripts/**/__generated__/**/*
|
|
25
26
|
```
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
2. Declare some teams. Here's an example, that would live at `config/teams/operations.yml`:
|
|
29
|
+
|
|
27
30
|
```yml
|
|
28
31
|
name: Operations
|
|
29
32
|
github:
|
|
30
33
|
team: '@my-org/operations-team'
|
|
31
34
|
```
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
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
38
|
|
|
35
39
|
## Usage: Declaring Ownership
|
|
36
40
|
|
|
37
41
|
There are five ways to declare code ownership using this gem:
|
|
38
42
|
|
|
39
43
|
### Directory-Based Ownership
|
|
44
|
+
|
|
40
45
|
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.
|
|
46
|
+
|
|
41
47
|
```
|
|
42
48
|
Team
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
### File-Annotation Based Ownership
|
|
52
|
+
|
|
46
53
|
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:
|
|
54
|
+
|
|
47
55
|
```ruby
|
|
48
56
|
# @team MyTeam
|
|
49
57
|
```
|
|
50
58
|
|
|
51
59
|
### Package-Based Ownership
|
|
60
|
+
|
|
52
61
|
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:
|
|
62
|
+
|
|
53
63
|
```yml
|
|
54
64
|
enforce_dependency: true
|
|
55
65
|
enforce_privacy: true
|
|
@@ -58,16 +68,19 @@ metadata:
|
|
|
58
68
|
```
|
|
59
69
|
|
|
60
70
|
You can also define `owner` as a top-level key, e.g.
|
|
71
|
+
|
|
61
72
|
```yml
|
|
62
73
|
enforce_dependency: true
|
|
63
74
|
enforce_privacy: true
|
|
64
75
|
owner: Team
|
|
65
76
|
```
|
|
66
77
|
|
|
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.
|
|
78
|
+
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
79
|
|
|
69
80
|
### Glob-Based Ownership
|
|
81
|
+
|
|
70
82
|
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`:
|
|
83
|
+
|
|
71
84
|
```yml
|
|
72
85
|
name: My Team
|
|
73
86
|
owned_globs:
|
|
@@ -78,6 +91,7 @@ unowned_globs:
|
|
|
78
91
|
```
|
|
79
92
|
|
|
80
93
|
### Javascript Package Ownership
|
|
94
|
+
|
|
81
95
|
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
96
|
|
|
83
97
|
```json
|
|
@@ -91,6 +105,7 @@ Javascript package based ownership allows you to specify an ownership key in a `
|
|
|
91
105
|
```
|
|
92
106
|
|
|
93
107
|
You can also tell `code_ownership` where to find JS packages in the configuration, like this:
|
|
108
|
+
|
|
94
109
|
```yml
|
|
95
110
|
js_package_paths:
|
|
96
111
|
- frontend/javascripts/packages/*
|
|
@@ -101,12 +116,15 @@ This defaults `**/`, which makes it look for `package.json` files across your ap
|
|
|
101
116
|
|
|
102
117
|
> [!NOTE]
|
|
103
118
|
> 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.
|
|
119
|
+
|
|
104
120
|
```yml
|
|
105
121
|
js_package_paths: []
|
|
106
122
|
```
|
|
107
123
|
|
|
108
124
|
## Usage: Reading CodeOwnership
|
|
125
|
+
|
|
109
126
|
### `for_file`
|
|
127
|
+
|
|
110
128
|
`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
129
|
|
|
112
130
|
```ruby
|
|
@@ -118,6 +136,7 @@ Contributor note: If you are making updates to this method or the methods gettin
|
|
|
118
136
|
See `code_ownership_spec.rb` for examples.
|
|
119
137
|
|
|
120
138
|
### `for_backtrace`
|
|
139
|
+
|
|
121
140
|
`CodeOwnership.for_backtrace` can be given a backtrace and will either return `nil`, or a `CodeTeams::Team`.
|
|
122
141
|
|
|
123
142
|
```ruby
|
|
@@ -141,19 +160,22 @@ Under the hood, this finds the file where the class is defined and returns the o
|
|
|
141
160
|
See `code_ownership_spec.rb` for an example.
|
|
142
161
|
|
|
143
162
|
### `for_team`
|
|
163
|
+
|
|
144
164
|
`CodeOwnership.for_team` can be used to generate an ownership report for a team.
|
|
165
|
+
|
|
145
166
|
```ruby
|
|
146
167
|
CodeOwnership.for_team('My Team')
|
|
147
168
|
```
|
|
148
169
|
|
|
149
170
|
You can shovel this into a markdown file for easy viewing using the CLI:
|
|
171
|
+
|
|
150
172
|
```
|
|
151
|
-
|
|
173
|
+
codeownership for_team 'My Team' > tmp/ownership_report.md
|
|
152
174
|
```
|
|
153
175
|
|
|
154
176
|
## Usage: Generating a `CODEOWNERS` file
|
|
155
177
|
|
|
156
|
-
A `CODEOWNERS` file defines who owns specific files or paths in a repository. When you run `
|
|
178
|
+
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.
|
|
157
179
|
|
|
158
180
|
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
181
|
|
|
@@ -161,14 +183,15 @@ If `codeowners_path` is set in `code_ownership.yml` codeowners will use that pat
|
|
|
161
183
|
|
|
162
184
|
CodeOwnership comes with a validation function to ensure the following things are true:
|
|
163
185
|
|
|
164
|
-
1
|
|
165
|
-
2
|
|
166
|
-
3
|
|
167
|
-
|
|
186
|
+
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.
|
|
187
|
+
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`).
|
|
188
|
+
3. All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
|
|
189
|
+
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`.
|
|
168
190
|
|
|
169
191
|
CodeOwnership also allows you to specify which globs and file extensions should be considered ownable.
|
|
170
192
|
|
|
171
193
|
Here is an example `config/code_ownership.yml`.
|
|
194
|
+
|
|
172
195
|
```yml
|
|
173
196
|
owned_globs:
|
|
174
197
|
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
|
|
@@ -178,13 +201,24 @@ unowned_globs:
|
|
|
178
201
|
- app/services/some_file2.rb
|
|
179
202
|
- frontend/javascripts/**/__generated__/**/*
|
|
180
203
|
```
|
|
204
|
+
|
|
181
205
|
You can call the validation function with the Ruby API
|
|
206
|
+
|
|
182
207
|
```ruby
|
|
183
208
|
CodeOwnership.validate!
|
|
184
209
|
```
|
|
210
|
+
|
|
185
211
|
or the CLI
|
|
186
|
-
|
|
187
|
-
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Validate all files
|
|
215
|
+
codeownership validate
|
|
216
|
+
|
|
217
|
+
# Validate specific files
|
|
218
|
+
codeownership validate path/to/file1.rb path/to/file2.rb
|
|
219
|
+
|
|
220
|
+
# Validate only staged files
|
|
221
|
+
codeownership validate --diff
|
|
188
222
|
```
|
|
189
223
|
|
|
190
224
|
## Development
|
|
@@ -192,10 +226,12 @@ bin/codeownership validate
|
|
|
192
226
|
Please add to `CHANGELOG.md` and this `README.md` when you make make changes.
|
|
193
227
|
|
|
194
228
|
## Running specs
|
|
229
|
+
|
|
195
230
|
```sh
|
|
196
231
|
bundle install
|
|
197
232
|
bundle exec rake
|
|
198
233
|
```
|
|
199
234
|
|
|
200
235
|
## Creating a new release
|
|
236
|
+
|
|
201
237
|
Simply [create a new release](https://github.com/rubyatscale/code_ownership/releases/new) with github. The release tag must match the gem version
|
data/lib/code_ownership/cli.rb
CHANGED
|
@@ -37,7 +37,7 @@ module CodeOwnership
|
|
|
37
37
|
options = {}
|
|
38
38
|
|
|
39
39
|
parser = OptionParser.new do |opts|
|
|
40
|
-
opts.banner = "Usage: #{EXECUTABLE} validate [options]"
|
|
40
|
+
opts.banner = "Usage: #{EXECUTABLE} validate [options] [files...]"
|
|
41
41
|
|
|
42
42
|
opts.on('--skip-autocorrect', 'Skip automatically correcting any errors, such as the .github/CODEOWNERS file') do
|
|
43
43
|
options[:skip_autocorrect] = true
|
|
@@ -59,11 +59,22 @@ module CodeOwnership
|
|
|
59
59
|
args = parser.order!(argv)
|
|
60
60
|
parser.parse!(args)
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
# Collect any remaining arguments as file paths
|
|
63
|
+
specified_files = argv.reject { |arg| arg.start_with?('--') }
|
|
64
|
+
|
|
65
|
+
files = if !specified_files.empty?
|
|
66
|
+
# Files explicitly provided on command line
|
|
67
|
+
if options[:diff]
|
|
68
|
+
warn 'Warning: Ignoring --diff flag because explicit files were provided'
|
|
69
|
+
end
|
|
70
|
+
specified_files.select { |file| File.exist?(file) }
|
|
71
|
+
elsif options[:diff]
|
|
72
|
+
# Staged files from git
|
|
63
73
|
ENV.fetch('CODEOWNERS_GIT_STAGED_FILES') { `git diff --staged --name-only` }.split("\n").select do |file|
|
|
64
74
|
File.exist?(file)
|
|
65
75
|
end
|
|
66
76
|
else
|
|
77
|
+
# No files specified, validate all
|
|
67
78
|
nil
|
|
68
79
|
end
|
|
69
80
|
|
data/lib/code_ownership.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
# typed: strict
|
|
4
4
|
|
|
5
|
-
require 'set'
|
|
6
5
|
require 'code_teams'
|
|
7
6
|
require 'sorbet-runtime'
|
|
8
7
|
require 'json'
|
|
@@ -239,6 +238,57 @@ module CodeOwnership
|
|
|
239
238
|
end
|
|
240
239
|
end
|
|
241
240
|
|
|
241
|
+
# Removes the file annotation (e.g., "# @team TeamName") from a file.
|
|
242
|
+
#
|
|
243
|
+
# This method removes the ownership annotation from the first line of a file,
|
|
244
|
+
# which is typically used to declare team ownership at the file level.
|
|
245
|
+
# The annotation can be in the form of:
|
|
246
|
+
# - Ruby comments: # @team TeamName
|
|
247
|
+
# - JavaScript/TypeScript comments: // @team TeamName
|
|
248
|
+
# - YAML comments: -# @team TeamName
|
|
249
|
+
#
|
|
250
|
+
# If the file does not have an annotation or the annotation doesn't match a valid team,
|
|
251
|
+
# this method does nothing.
|
|
252
|
+
#
|
|
253
|
+
# @param filename [String] The path to the file from which to remove the annotation.
|
|
254
|
+
# Can be relative or absolute.
|
|
255
|
+
#
|
|
256
|
+
# @return [void]
|
|
257
|
+
#
|
|
258
|
+
# @example Remove annotation from a Ruby file
|
|
259
|
+
# # Before: File contains "# @team Platform\nclass User; end"
|
|
260
|
+
# CodeOwnership.remove_file_annotation!('app/models/user.rb')
|
|
261
|
+
# # After: File contains "class User; end"
|
|
262
|
+
#
|
|
263
|
+
# @example Remove annotation from a JavaScript file
|
|
264
|
+
# # Before: File contains "// @team Frontend\nexport default function() {}"
|
|
265
|
+
# CodeOwnership.remove_file_annotation!('app/javascript/component.js')
|
|
266
|
+
# # After: File contains "export default function() {}"
|
|
267
|
+
#
|
|
268
|
+
# @note This method modifies the file in place.
|
|
269
|
+
# @note Leading newlines after the annotation are also removed to maintain clean formatting.
|
|
270
|
+
#
|
|
271
|
+
sig { params(filename: String).void }
|
|
272
|
+
def remove_file_annotation!(filename)
|
|
273
|
+
filepath = Pathname.new(filename)
|
|
274
|
+
|
|
275
|
+
begin
|
|
276
|
+
content = filepath.read
|
|
277
|
+
rescue Errno::EISDIR, Errno::ENOENT
|
|
278
|
+
# Ignore files that fail to read (directories, missing files, etc.)
|
|
279
|
+
return
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Remove the team annotation and any trailing newlines after it
|
|
283
|
+
team_pattern = %r{\A(?:#|//|-#) @team .*\n+}
|
|
284
|
+
new_content = content.sub(team_pattern, '')
|
|
285
|
+
|
|
286
|
+
filepath.write(new_content) if new_content != content
|
|
287
|
+
rescue ArgumentError => e
|
|
288
|
+
# Handle invalid byte sequences gracefully
|
|
289
|
+
raise unless e.message.include?('invalid byte sequence')
|
|
290
|
+
end
|
|
291
|
+
|
|
242
292
|
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
|
243
293
|
# first line that corresponds to a file with assigned ownership
|
|
244
294
|
sig { params(backtrace: T.nilable(T::Array[String]), excluded_teams: T::Array[::CodeTeams::Team]).returns(T.nilable(::CodeTeams::Team)) }
|