code_ownership 1.28.0 → 1.29.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d73cb8995d6430d05780252417863b38bd14a461bf8f3afe4b9724abb407efea
4
- data.tar.gz: 7cc3c3c4a2968b8e3a265f990c28395b0c4e6163b7ba48ad2b0c8be64123990c
3
+ metadata.gz: 8008aad63593c3be910dc1e7f45f2518150d0e76a92f8a021279902995499402
4
+ data.tar.gz: 551585001712db071af3e33607bfd121ff48524fa66776f8ee48a299da28ef1a
5
5
  SHA512:
6
- metadata.gz: 6f2a2c2331aa980b414343e54429ba3ab367fa6a38cdad44811b84193afd8a6d3846baf77ae8b7cd78a2754b65865b5799644382ff5182ba0439bb3cb3884cf6
7
- data.tar.gz: d0ecc33dd542a3dc606caceb52fb38ed32743a12076f5253b96a2c2c6b7fa24a0b1445df6d1a41878ef350a87fe5075cbb17ebc5bb3964211cbac18ee005bfd5
6
+ metadata.gz: a2b94a65f309e158505b6dd4aa740cf3cfedc7076d9ccb0e7fbc1fe04ce147c8355cbe9cfd687a6b0e05e5ad755bb703f8fc1b530ad40f9b24fb9a4d8326a18f
7
+ data.tar.gz: 895352504690505cce06da848c5a400b3e48f1a9e10d49baa5a1e0093bd29c70c6042eaaa9c21dea14cbe8b0e7e44d15a8bf124150528f33280abee5eb101d97
data/README.md CHANGED
@@ -1,14 +1,17 @@
1
1
  # CodeOwnership
2
- This gem helps engineering teams declare ownership of code.
3
2
 
4
- Check out `lib/code_ownership.rb` to see the public API.
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.
5
4
 
6
- Check out `code_ownership_spec.rb` to see examples of how code ownership is used.
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.
7
8
 
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.
9
10
 
10
11
  ## Usage: Declaring Ownership
12
+
11
13
  There are three ways to declare code ownership using this gem.
14
+
12
15
  ### Package-Based Ownership
13
16
  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:
14
17
  ```yml
@@ -31,6 +34,29 @@ File annotations are a last resort if there is no clear home for your code. File
31
34
  ```ruby
32
35
  # @team MyTeam
33
36
  ```
37
+
38
+ ### Javascript Package Ownership
39
+ Javascript package based ownership allows you to specify an owenrship key in a `package.json`. To use this, configure your `package.json` like this:
40
+
41
+ ```json
42
+ {
43
+ // other keys
44
+ "metadata": {
45
+ "owner": "My Team"
46
+ }
47
+ // other keys
48
+ }
49
+ ```
50
+
51
+ You can also tell `code_ownership` where to find JS packages in the configuration, like this:
52
+ ```yml
53
+ js_package_paths:
54
+ - frontend/javascripts/packages/*
55
+ - frontend/other_location_for_packages/*
56
+ ```
57
+
58
+ This defaults `**/`, which makes it look for `package.json` files across your application.
59
+
34
60
  ## Usage: Reading CodeOwnership
35
61
  ### `for_file`
36
62
  `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.
@@ -66,12 +92,25 @@ Under the hood, this finds the file where the class is defined and returns the o
66
92
 
67
93
  See `code_ownership_spec.rb` for an example.
68
94
 
95
+ ### `for_team`
96
+ `CodeOwnership.for_team` can be used to generate an ownership report for a team.
97
+ ```ruby
98
+ CodeOwnership.for_team('My Team')
99
+ ```
100
+
101
+ You can shovel this into a markdown file for easy viewing using the CLI:
102
+ ```
103
+ bin/codeownership for_team 'My Team' > tmp/ownership_report.md
104
+ ```
105
+
69
106
  ## Usage: Generating a `CODEOWNERS` file
70
107
 
71
108
  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.
72
109
 
73
110
  ## Proper Configuration & Validation
111
+
74
112
  CodeOwnership comes with a validation function to ensure the following things are true:
113
+
75
114
  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.
76
115
  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`).
77
116
  3) All files have ownership. You can specify in `unowned_globs` to represent a TODO list of files to add ownership to.
@@ -11,6 +11,8 @@ module CodeOwnership
11
11
  validate!(argv)
12
12
  elsif command == 'for_file'
13
13
  for_file(argv)
14
+ elsif command == 'for_team'
15
+ for_team(argv)
14
16
  elsif [nil, "help"].include?(command)
15
17
  puts <<~USAGE
16
18
  Usage: bin/codeownership <subcommand>
@@ -18,6 +20,7 @@ module CodeOwnership
18
20
  Subcommands:
19
21
  validate - run all validations
20
22
  for_file - find code ownership for a single file
23
+ for_team - find code ownership information for a team
21
24
  help - display help information about code_ownership
22
25
  USAGE
23
26
  else
@@ -116,6 +119,28 @@ module CodeOwnership
116
119
  end
117
120
  end
118
121
 
122
+ def self.for_team(argv)
123
+ options = {}
124
+
125
+ parser = OptionParser.new do |opts|
126
+ opts.banner = 'Usage: bin/codeownership for_team \'Team Name\''
127
+
128
+ opts.on('--help', 'Shows this prompt') do
129
+ puts opts
130
+ exit
131
+ end
132
+ end
133
+ teams = argv.select { |arg| !arg.start_with?('--') }
134
+ args = parser.order!(argv) {}
135
+ parser.parse!(args)
136
+
137
+ if teams.count != 1
138
+ raise "Please pass in one team. Use `bin/codeownership for_team --help` for more info"
139
+ end
140
+
141
+ puts CodeOwnership.for_team(teams.first)
142
+ end
143
+
119
144
  private_class_method :validate!
120
145
  end
121
146
  end
@@ -16,7 +16,7 @@ module CodeOwnership
16
16
  returns(T.nilable(::CodeTeams::Team))
17
17
  end
18
18
  def map_file_to_owner(file)
19
- package = map_file_to_relevant_package(file)
19
+ package = ParsePackwerk.package_from_path(file)
20
20
 
21
21
  return nil if package.nil?
22
22
 
@@ -80,41 +80,6 @@ module CodeOwnership
80
80
  def bust_caches!
81
81
  @@package_yml_cache = {} # rubocop:disable Style/ClassVars
82
82
  end
83
-
84
- private
85
-
86
- # takes a file and finds the relevant `package.yml` file by walking up the directory
87
- # structure. Example, given `packs/a/b/c.rb`, this looks for `packs/a/b/package.yml`, `packs/a/package.yml`,
88
- # `packs/package.yml`, and `package.yml` in that order, stopping at the first file to actually exist.
89
- # We do additional caching so that we don't have to check for file existence every time
90
- sig { params(file: String).returns(T.nilable(ParsePackwerk::Package)) }
91
- def map_file_to_relevant_package(file)
92
- file_path = Pathname.new(file)
93
- path_components = file_path.each_filename.to_a.map { |path| Pathname.new(path) }
94
-
95
- (path_components.length - 1).downto(0).each do |i|
96
- potential_relative_path_name = T.must(path_components[0...i]).reduce(Pathname.new('')) { |built_path, path| built_path.join(path) }
97
- potential_package_yml_path = potential_relative_path_name.
98
- join(ParsePackwerk::PACKAGE_YML_NAME)
99
-
100
- potential_package_yml_string = potential_package_yml_path.to_s
101
-
102
- package = nil
103
- if @@package_yml_cache.key?(potential_package_yml_string)
104
- package = @@package_yml_cache[potential_package_yml_string]
105
- elsif potential_package_yml_path.exist?
106
- package = ParsePackwerk::Package.from(potential_package_yml_path)
107
-
108
- @@package_yml_cache[potential_package_yml_string] = package
109
- else
110
- @@package_yml_cache[potential_package_yml_string] = nil
111
- end
112
-
113
- return package unless package.nil?
114
- end
115
-
116
- nil
117
- end
118
83
  end
119
84
  end
120
85
  end
@@ -32,6 +32,16 @@ module CodeOwnership
32
32
  name: package_name,
33
33
  metadata: package_loaded_json[METADATA] || {}
34
34
  )
35
+ rescue JSON::ParserError => e
36
+ error_message = <<~MESSAGE
37
+ #{e.inspect}
38
+
39
+ #{pathname} has invalid JSON, so code ownership cannot be determined.
40
+
41
+ Please either make the JSON in that file valid or specify `js_package_paths` in config/code_ownership.yml.
42
+ MESSAGE
43
+
44
+ raise InvalidCodeOwnershipConfigurationError.new(error_message)
35
45
  end
36
46
 
37
47
  sig { returns(Pathname) }
@@ -35,6 +35,36 @@ module CodeOwnership
35
35
  @for_file[file] = owner
36
36
  end
37
37
 
38
+ sig { params(team: T.any(CodeTeams::Team, String)).returns(String) }
39
+ def for_team(team)
40
+ team = T.must(CodeTeams.find(team)) if team.is_a?(String)
41
+ ownership_information = T.let([], T::Array[String])
42
+
43
+ ownership_information << "# Code Ownership Report for `#{team.name}` Team"
44
+ Private.mappers.each do |mapper|
45
+ ownership_information << "## #{mapper.description}"
46
+ codeowners_lines = mapper.codeowners_lines_to_owners
47
+ ownership_for_mapper = []
48
+ codeowners_lines.each do |line, team_for_line|
49
+ next if team_for_line.nil?
50
+ if team_for_line.name == team.name
51
+ ownership_for_mapper << "- #{line}"
52
+ end
53
+ end
54
+
55
+ if ownership_for_mapper.empty?
56
+ ownership_information << 'This team owns nothing in this category.'
57
+ else
58
+ ownership_information += ownership_for_mapper
59
+ end
60
+
61
+
62
+ ownership_information << ""
63
+ end
64
+
65
+ ownership_information.join("\n")
66
+ end
67
+
38
68
  class InvalidCodeOwnershipConfigurationError < StandardError
39
69
  end
40
70
 
data/sorbet/config CHANGED
@@ -1,4 +1,5 @@
1
1
  --dir
2
2
  .
3
3
  --ignore=/spec
4
+ --ignore=/vendor/bundle
4
5
  --enable-experimental-requires-ancestor
@@ -6,12 +6,18 @@
6
6
 
7
7
  module ParsePackwerk
8
8
  class << self
9
- sig { params(package_yml_pathnames: T.nilable(T::Array[::Pathname])).returns(T::Array[::ParsePackwerk::Package]) }
10
- def all(package_yml_pathnames: T.unsafe(nil)); end
9
+ sig { returns(T::Array[::ParsePackwerk::Package]) }
10
+ def all; end
11
+
12
+ sig { void }
13
+ def bust_cache!; end
11
14
 
12
15
  sig { params(name: ::String).returns(T.nilable(::ParsePackwerk::Package)) }
13
16
  def find(name); end
14
17
 
18
+ sig { params(file_path: T.any(::Pathname, ::String)).returns(T.nilable(::ParsePackwerk::Package)) }
19
+ def package_from_path(file_path); end
20
+
15
21
  sig { params(package: ::ParsePackwerk::Package).void }
16
22
  def write_package_yml!(package); end
17
23
 
@@ -27,12 +33,24 @@ end
27
33
 
28
34
  class ParsePackwerk::Configuration < ::T::Struct
29
35
  const :exclude, T::Array[::String]
36
+ const :package_paths, T::Array[::String]
30
37
 
31
38
  class << self
39
+ sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[::String]) }
40
+ def excludes(config_hash); end
41
+
42
+ sig { returns(::ParsePackwerk::Configuration) }
43
+ def fetch; end
44
+
32
45
  def inherited(s); end
46
+
47
+ sig { params(config_hash: T::Hash[T.untyped, T.untyped]).returns(T::Array[::String]) }
48
+ def package_paths(config_hash); end
33
49
  end
34
50
  end
35
51
 
52
+ ParsePackwerk::DEFAULT_EXCLUDE_GLOBS = T.let(T.unsafe(nil), Array)
53
+ ParsePackwerk::DEFAULT_PACKAGE_PATHS = T.let(T.unsafe(nil), Array)
36
54
  ParsePackwerk::DEPENDENCIES = T.let(T.unsafe(nil), String)
37
55
  ParsePackwerk::DEPRECATED_REFERENCES_YML_NAME = T.let(T.unsafe(nil), String)
38
56
 
@@ -91,6 +109,23 @@ class ParsePackwerk::Package < ::T::Struct
91
109
  end
92
110
  end
93
111
 
112
+ class ParsePackwerk::PackageSet
113
+ class << self
114
+ sig do
115
+ params(
116
+ package_pathspec: T::Array[::String],
117
+ exclude_pathspec: T::Array[::String]
118
+ ).returns(T::Array[::ParsePackwerk::Package])
119
+ end
120
+ def from(package_pathspec:, exclude_pathspec:); end
121
+
122
+ private
123
+
124
+ sig { params(globs: T::Array[::String], path: ::Pathname).returns(T::Boolean) }
125
+ def exclude_path?(globs, path); end
126
+ end
127
+ end
128
+
94
129
  ParsePackwerk::ROOT_PACKAGE_NAME = T.let(T.unsafe(nil), String)
95
130
 
96
131
  class ParsePackwerk::Violation < ::T::Struct
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: code_ownership
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.28.0
4
+ version: 1.29.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-14 00:00:00.000000000 Z
11
+ date: 2022-11-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: code_teams
@@ -150,7 +150,7 @@ files:
150
150
  - lib/code_ownership/private/validations/interface.rb
151
151
  - sorbet/config
152
152
  - sorbet/rbi/gems/code_teams@1.0.0.rbi
153
- - sorbet/rbi/gems/parse_packwerk@0.7.0.rbi
153
+ - sorbet/rbi/gems/parse_packwerk@0.12.0.rbi
154
154
  - sorbet/rbi/manual.rbi
155
155
  - sorbet/rbi/todo.rbi
156
156
  homepage: https://github.com/rubyatscale/code_ownership
@@ -176,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
176
176
  - !ruby/object:Gem::Version
177
177
  version: '0'
178
178
  requirements: []
179
- rubygems_version: 3.3.7
179
+ rubygems_version: 3.1.6
180
180
  signing_key:
181
181
  specification_version: 4
182
182
  summary: A gem to help engineering teams declare ownership of code