cleo_codeowners 0.3.0 → 0.5.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 +13 -5
- data/lib/cleo_codeowners/version.rb +1 -1
- data/lib/codeowners/generator.rb +46 -30
- 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: 3da6d91b323d359c5906252cef6cf83727342be78baca1dcf576f5d43ac01be7
|
|
4
|
+
data.tar.gz: d4092679171b3762a76ac338546cf31c02a0b158fb50eecb7d1c8d818ef9b239
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2c847488207f368367a95244a5cd025449961b1df3f8454658144898d552761463bf5d97c07018c1813ccb89e088a1360fa7d9c3614ac9368aa5aa3decedcf93
|
|
7
|
+
data.tar.gz: e2b9f252b9054559deb915e2f68ed6d526b4953438d5457ae6249cdd189daae688cc329ac3c37b8b52994efc81bffb06a889239f99b94e2981c0310847f556cf
|
data/README.md
CHANGED
|
@@ -60,27 +60,35 @@ Create YAML files under `.cleo/codeowners/`.
|
|
|
60
60
|
# .cleo/codeowners/features.yml
|
|
61
61
|
features:
|
|
62
62
|
session management:
|
|
63
|
+
session administration:
|
|
63
64
|
session expiry:
|
|
64
65
|
billing:
|
|
65
66
|
```
|
|
66
67
|
|
|
68
|
+
Child features inherit their parent's owners, or can override with their own.
|
|
69
|
+
An owner can be one or more teams.
|
|
70
|
+
|
|
67
71
|
```yaml
|
|
68
72
|
# .cleo/codeowners/owners.yml
|
|
69
73
|
owners:
|
|
74
|
+
session administration:
|
|
75
|
+
- operations
|
|
70
76
|
session management: identity-platform
|
|
71
|
-
billing:
|
|
77
|
+
billing:
|
|
78
|
+
- payments
|
|
79
|
+
- finance
|
|
72
80
|
```
|
|
73
81
|
|
|
74
|
-
Child features inherit their parent owner unless they define one.
|
|
75
|
-
|
|
76
82
|
```yaml
|
|
77
83
|
# .cleo/codeowners/files/session_management.yml
|
|
78
84
|
files:
|
|
85
|
+
session administration:
|
|
86
|
+
- /app/controllers/admin/sessions/
|
|
87
|
+
session expiry:
|
|
88
|
+
- /app/services/session_expiry/
|
|
79
89
|
session management:
|
|
80
90
|
- /app/controllers/sessions/
|
|
81
91
|
- /app/models/session.rb
|
|
82
|
-
session expiry:
|
|
83
|
-
- /app/services/session_expiry/
|
|
84
92
|
|
|
85
93
|
# .cleo/codeowners/files/billing.yml
|
|
86
94
|
files:
|
data/lib/codeowners/generator.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# typed: false
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require 'active_support/core_ext/array/conversions'
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
4
7
|
module Codeowners
|
|
5
8
|
class Generator
|
|
6
9
|
DEFAULT_OUTPUT_PATH = '.github/CODEOWNERS'
|
|
@@ -57,15 +60,18 @@ module Codeowners
|
|
|
57
60
|
@files ||= definitions_file.file_config
|
|
58
61
|
end
|
|
59
62
|
|
|
60
|
-
def calculate_owners_and_output_in_human_format(features:,
|
|
63
|
+
def calculate_owners_and_output_in_human_format(features:, current_owners: [], depth: 0)
|
|
61
64
|
return unless present?(features)
|
|
62
65
|
|
|
63
66
|
features.sort_by(&:first).each do |feature, children|
|
|
64
67
|
output(feature.upcase, depth:)
|
|
65
|
-
|
|
68
|
+
feature_owners = Array(
|
|
69
|
+
owners.fetch(feature, current_owners)
|
|
70
|
+
).reject { |owner| blank?(owner) }
|
|
71
|
+
|
|
66
72
|
feature_files = files[feature]
|
|
67
73
|
|
|
68
|
-
if
|
|
74
|
+
if feature_owners.empty?
|
|
69
75
|
raise "Please assign an owner for #{feature} in owners.yaml!"
|
|
70
76
|
elsif blank?(feature_files) && blank?(children)
|
|
71
77
|
raise "Please assign files or child features to #{feature}, or remove it!"
|
|
@@ -73,20 +79,22 @@ module Codeowners
|
|
|
73
79
|
all_features << feature
|
|
74
80
|
end
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
long_names = feature_owners.map { |owner| Owner.new(owner).long_name }.uniq
|
|
83
|
+
|
|
84
|
+
if feature_owners == ['unowned']
|
|
77
85
|
output('Currently unowned', depth:)
|
|
78
86
|
else
|
|
79
|
-
output("Currently owned by
|
|
87
|
+
output("Currently owned by #{long_names.to_sentence}", depth:)
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
if present?(feature_files)
|
|
83
91
|
feature_files.sort.uniq.each do |file|
|
|
84
|
-
record_calculated_owner(file,
|
|
85
|
-
output("#{file} #{
|
|
92
|
+
long_names.each { |long_name| record_calculated_owner(file, long_name) }
|
|
93
|
+
output("#{file} #{long_names.join(' ')}", depth:, comment: false)
|
|
86
94
|
end
|
|
87
95
|
end
|
|
88
96
|
|
|
89
|
-
calculate_owners_and_output_in_human_format(features: children,
|
|
97
|
+
calculate_owners_and_output_in_human_format(features: children, current_owners: feature_owners, depth: depth + 1)
|
|
90
98
|
end
|
|
91
99
|
end
|
|
92
100
|
|
|
@@ -105,32 +113,40 @@ module Codeowners
|
|
|
105
113
|
calculated_owners.push({ path:, owner: })
|
|
106
114
|
end
|
|
107
115
|
|
|
116
|
+
# GitHub applies the *last* matching CODEOWNERS rule, so rules are listed
|
|
117
|
+
# least-specific first: by number of path segments (`/app/` is broader than
|
|
118
|
+
# `/app/models/user.rb`), then by path so a wildcard such as `/foo/*` sorts
|
|
119
|
+
# ahead of the `/foo/bar/` it would otherwise shadow.
|
|
108
120
|
def output_in_machine_format
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
merge_owners_by_path(calculated_owners)
|
|
122
|
+
.sort_by { |path, _owners| [path_depth(path), path] }
|
|
123
|
+
.each { |path, rule_owners| output_file << "#{path} #{rule_owners.join(' ')}\n" }
|
|
112
124
|
end
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
end
|
|
126
|
+
# Combines the owners of rules that share a path onto one line.
|
|
127
|
+
def merge_owners_by_path(rules)
|
|
128
|
+
rules_by_path = rules.group_by { |owner| sanitised_path(owner[:path]) }
|
|
129
|
+
reject_trailing_slash_conflicts(rules_by_path.keys)
|
|
130
|
+
rules_by_path.map do |path, owners_for_path|
|
|
131
|
+
[path, owners_for_path.map { |owner| owner[:owner] }.uniq.sort]
|
|
121
132
|
end
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# A path written both with and without a trailing slash is rejected rather
|
|
136
|
+
# than guessed at: `/foo` and `/foo/` are effectively same rule, but on disk
|
|
137
|
+
# they differ (a directory needs its slash to match recursively, a file must
|
|
138
|
+
# not have one), so the definitions must pick one.
|
|
139
|
+
def reject_trailing_slash_conflicts(paths)
|
|
140
|
+
paths.group_by { |path| path.chomp('/') }.each_value do |variants|
|
|
141
|
+
next if variants.uniq.size == 1
|
|
142
|
+
|
|
143
|
+
raise "#{variants.min} is declared both with and without a trailing slash " \
|
|
144
|
+
"(#{variants.uniq.sort.join(', ')}); pick one so it is clear whether it is a file or a directory."
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def path_depth(path)
|
|
149
|
+
Pathname.new(path).each_filename.count
|
|
134
150
|
end
|
|
135
151
|
|
|
136
152
|
def blurb
|