gitlab-dangerfiles 4.6.0 → 4.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -1
- data/.gitlab-ci.yml +9 -16
- data/.rubocop.yml +3 -0
- data/Gemfile.lock +231 -0
- data/lib/danger/plugins/internal/helper.rb +2 -1
- data/lib/danger/plugins/roulette.rb +34 -267
- data/lib/danger/rules/commit_messages/Dangerfile +1 -1
- data/lib/gitlab/dangerfiles/approval.rb +22 -0
- data/lib/gitlab/dangerfiles/capability.rb +84 -0
- data/lib/gitlab/dangerfiles/spec_helper.rb +230 -0
- data/lib/gitlab/dangerfiles/spin.rb +15 -0
- data/lib/gitlab/dangerfiles/spinner.rb +190 -0
- data/lib/gitlab/dangerfiles/teammate.rb +88 -2
- data/lib/gitlab/dangerfiles/version.rb +1 -1
- metadata +7 -3
- data/lib/gitlab/dangerfiles/category.rb +0 -111
@@ -121,7 +121,7 @@ def warn_or_fail_commits(failed_linters, default_to_fail: true)
|
|
121
121
|
when :subject_too_short, :subject_above_warning, :details_too_many_changes, :details_line_too_long
|
122
122
|
warn_commit(linter.commit, problem_desc)
|
123
123
|
else
|
124
|
-
self.__send__("#{level}_commit", linter.commit, problem_desc)
|
124
|
+
self.__send__(:"#{level}_commit", linter.commit, problem_desc)
|
125
125
|
end
|
126
126
|
end
|
127
127
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spin"
|
4
|
+
|
5
|
+
module Gitlab
|
6
|
+
module Dangerfiles
|
7
|
+
Approval = Struct.new(:category, :spin) do
|
8
|
+
def self.from_approval_rule(rule, maintainer)
|
9
|
+
category =
|
10
|
+
if rule["section"] == "codeowners"
|
11
|
+
"`#{rule['name']}`"
|
12
|
+
else
|
13
|
+
rule["section"]
|
14
|
+
end.to_sym
|
15
|
+
|
16
|
+
spin = Spin.new(category, nil, maintainer, :reviewer)
|
17
|
+
|
18
|
+
new(category, spin)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Dangerfiles
|
5
|
+
CapabilityStruct = Struct.new(:category, :project, :kind, :labels, keyword_init: true)
|
6
|
+
|
7
|
+
class Capability < CapabilityStruct
|
8
|
+
def self.for(category, **arguments)
|
9
|
+
(category_to_class[category] || self)
|
10
|
+
.new(category: category, **arguments)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.category_to_class
|
14
|
+
@category_to_class ||= {
|
15
|
+
none: None,
|
16
|
+
test: Test,
|
17
|
+
tooling: Tooling,
|
18
|
+
import_integrate_be: ImportIntegrateBE,
|
19
|
+
import_integrate_fe: ImportIntegrateFE,
|
20
|
+
ux: UX,
|
21
|
+
}.freeze
|
22
|
+
end
|
23
|
+
private_class_method :category_to_class
|
24
|
+
|
25
|
+
def has_capability?(teammate)
|
26
|
+
teammate.capabilities(project).include?(capability)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def capability
|
32
|
+
@capability ||= "#{kind} #{category}"
|
33
|
+
end
|
34
|
+
|
35
|
+
class None < Capability
|
36
|
+
def capability
|
37
|
+
@capability ||= kind.to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Test < Capability
|
42
|
+
def has_capability?(teammate)
|
43
|
+
return false if kind != :reviewer
|
44
|
+
|
45
|
+
area = teammate.role[/Software Engineer in Test(?:.*?, (\w+))/, 1]
|
46
|
+
|
47
|
+
!!area && labels.any?("devops::#{area.downcase}")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class Tooling < Capability
|
52
|
+
def has_capability?(teammate)
|
53
|
+
if super
|
54
|
+
true
|
55
|
+
elsif %i[trainee_maintainer maintainer].include?(kind)
|
56
|
+
false
|
57
|
+
else # fallback to backend reviewer
|
58
|
+
teammate.capabilities(project).include?("#{kind} backend")
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class ImportIntegrateBE < Capability
|
64
|
+
def has_capability?(teammate)
|
65
|
+
kind == :reviewer &&
|
66
|
+
teammate.role.match?(/Backend Engineer.+Manage:Import and Integrate/)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class ImportIntegrateFE < Capability
|
71
|
+
def has_capability?(teammate)
|
72
|
+
kind == :reviewer &&
|
73
|
+
teammate.role.match?(/Frontend Engineer.+Manage:Import and Integrate/)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class UX < Capability
|
78
|
+
def has_capability?(teammate)
|
79
|
+
super && teammate.member_of_the_group?(labels)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -45,6 +45,19 @@ module DangerSpecHelper
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
+
RSpec::Matchers.define :match_teammates do |expected|
|
49
|
+
match do |actual|
|
50
|
+
expected.each do |expected_person|
|
51
|
+
matched_person = actual.find { |actual_person| actual_person.name == expected_person.username }
|
52
|
+
|
53
|
+
matched_person &&
|
54
|
+
matched_person.name == expected_person.name &&
|
55
|
+
matched_person.role == expected_person.role &&
|
56
|
+
matched_person.projects == expected_person.projects
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
48
61
|
RSpec.shared_context "with dangerfile" do
|
49
62
|
let(:dangerfile) { DangerSpecHelper.testing_dangerfile }
|
50
63
|
let(:added_files) { %w[added-from-git] }
|
@@ -68,3 +81,220 @@ RSpec.shared_context "with dangerfile" do
|
|
68
81
|
allow(dangerfile.helper).to receive(:changes).and_return(changes) if dangerfile.respond_to?(:helper)
|
69
82
|
end
|
70
83
|
end
|
84
|
+
|
85
|
+
RSpec.shared_context "with teammates" do
|
86
|
+
let(:backend_available) { true }
|
87
|
+
let(:backend_tz_offset_hours) { 2.0 }
|
88
|
+
let(:backend_maintainer_project) { { "gitlab" => "maintainer backend" } }
|
89
|
+
let(:backend_maintainer) do
|
90
|
+
Gitlab::Dangerfiles::Teammate.new(
|
91
|
+
"username" => "backend-maintainer",
|
92
|
+
"name" => "Backend maintainer",
|
93
|
+
"role" => "Backend engineer",
|
94
|
+
"projects" => backend_maintainer_project,
|
95
|
+
"available" => backend_available,
|
96
|
+
"tz_offset_hours" => backend_tz_offset_hours,
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
let(:another_backend_maintainer) do
|
101
|
+
Gitlab::Dangerfiles::Teammate.new(
|
102
|
+
"username" => "another-backend-maintainer",
|
103
|
+
"name" => "Another Backend Maintainer",
|
104
|
+
"role" => "Backend engineer",
|
105
|
+
"projects" => backend_maintainer_project,
|
106
|
+
"available" => backend_available,
|
107
|
+
"tz_offset_hours" => backend_tz_offset_hours,
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
let(:backend_reviewer_available) { true }
|
112
|
+
let(:backend_reviewer) do
|
113
|
+
Gitlab::Dangerfiles::Teammate.new(
|
114
|
+
"username" => "backend-reviewer",
|
115
|
+
"name" => "Backend reviewer",
|
116
|
+
"role" => "Backend engineer",
|
117
|
+
"projects" => { "gitlab" => "reviewer backend" },
|
118
|
+
"available" => backend_reviewer_available,
|
119
|
+
"tz_offset_hours" => 1.0,
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
let(:frontend_reviewer) do
|
124
|
+
Gitlab::Dangerfiles::Teammate.new(
|
125
|
+
"username" => "frontend-reviewer",
|
126
|
+
"name" => "Frontend reviewer",
|
127
|
+
"role" => "Frontend engineer",
|
128
|
+
"projects" => { "gitlab" => "reviewer frontend" },
|
129
|
+
"available" => true,
|
130
|
+
"tz_offset_hours" => 2.0,
|
131
|
+
)
|
132
|
+
end
|
133
|
+
|
134
|
+
let(:frontend_maintainer) do
|
135
|
+
Gitlab::Dangerfiles::Teammate.new(
|
136
|
+
"username" => "frontend-maintainer",
|
137
|
+
"name" => "Frontend maintainer",
|
138
|
+
"role" => "Frontend engineer",
|
139
|
+
"projects" => { "gitlab" => "maintainer frontend" },
|
140
|
+
"available" => true,
|
141
|
+
"tz_offset_hours" => 2.0,
|
142
|
+
)
|
143
|
+
end
|
144
|
+
|
145
|
+
let(:ux_reviewer) do
|
146
|
+
Gitlab::Dangerfiles::Teammate.new(
|
147
|
+
"username" => "ux-reviewer",
|
148
|
+
"name" => "UX reviewer",
|
149
|
+
"role" => "Product Designer",
|
150
|
+
"projects" => { "gitlab" => "reviewer ux" },
|
151
|
+
"specialty" => "Create: Source Code",
|
152
|
+
"available" => true,
|
153
|
+
"tz_offset_hours" => 2.0,
|
154
|
+
)
|
155
|
+
end
|
156
|
+
|
157
|
+
let(:software_engineer_in_test) do
|
158
|
+
Gitlab::Dangerfiles::Teammate.new(
|
159
|
+
"username" => "software-engineer-in-test",
|
160
|
+
"name" => "Software Engineer in Test",
|
161
|
+
"role" => "Software Engineer in Test, Create:Source Code",
|
162
|
+
"projects" => { "gitlab" => "maintainer qa", "gitlab-qa" => "maintainer" },
|
163
|
+
"available" => true,
|
164
|
+
"tz_offset_hours" => 2.0,
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
let(:software_engineer_in_import_integrate_fe) do
|
169
|
+
Gitlab::Dangerfiles::Teammate.new(
|
170
|
+
"username" => "software-engineer-in-import-and-integrate-fe",
|
171
|
+
"name" => "Software Engineer in Import and Integrate FE",
|
172
|
+
"role" => "Frontend Engineer, Manage:Import and Integrate",
|
173
|
+
"projects" => { "gitlab" => "reviewer frontend" },
|
174
|
+
"available" => true,
|
175
|
+
"tz_offset_hours" => 2.0,
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
let(:software_engineer_in_import_integrate_be) do
|
180
|
+
Gitlab::Dangerfiles::Teammate.new(
|
181
|
+
"username" => "software-engineer-in-import-and-integrate-be",
|
182
|
+
"name" => "Software Engineer in Import and Integrate BE",
|
183
|
+
"role" => "Backend Engineer, Manage:Import and Integrate",
|
184
|
+
"projects" => { "gitlab" => "reviewer backend" },
|
185
|
+
"available" => true,
|
186
|
+
"tz_offset_hours" => 2.0,
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
let(:tooling_reviewer) do
|
191
|
+
Gitlab::Dangerfiles::Teammate.new(
|
192
|
+
"username" => "eng-prod-reviewer",
|
193
|
+
"name" => "EP engineer",
|
194
|
+
"role" => "Engineering Productivity",
|
195
|
+
"projects" => { "gitlab" => "reviewer tooling" },
|
196
|
+
"available" => true,
|
197
|
+
"tz_offset_hours" => 2.0,
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
let(:ci_template_reviewer) do
|
202
|
+
Gitlab::Dangerfiles::Teammate.new(
|
203
|
+
"username" => "ci-template-maintainer",
|
204
|
+
"name" => "CI Template engineer",
|
205
|
+
"role" => '~"ci::templates"',
|
206
|
+
"projects" => { "gitlab" => "reviewer ci_template" },
|
207
|
+
"available" => true,
|
208
|
+
"tz_offset_hours" => 2.0,
|
209
|
+
)
|
210
|
+
end
|
211
|
+
|
212
|
+
let(:analytics_instrumentation_reviewer) do
|
213
|
+
Gitlab::Dangerfiles::Teammate.new(
|
214
|
+
"username" => "analytics-instrumentation-reviewer",
|
215
|
+
"name" => "PI engineer",
|
216
|
+
"role" => "Backend Engineer, Analytics: Analytics Instrumentation",
|
217
|
+
"projects" => { "gitlab" => "reviewer analytics_instrumentation" },
|
218
|
+
"available" => true,
|
219
|
+
"tz_offset_hours" => 2.0,
|
220
|
+
)
|
221
|
+
end
|
222
|
+
|
223
|
+
let(:import_and_integrate_backend_reviewer) do
|
224
|
+
Gitlab::Dangerfiles::Teammate.new(
|
225
|
+
"username" => "import-and-integrate-backend-reviewer",
|
226
|
+
"name" => "Import and Integrate BE engineer",
|
227
|
+
"role" => "Backend Engineer, Manage:Import and Integrate",
|
228
|
+
"projects" => { "gitlab" => "reviewer backend" },
|
229
|
+
"available" => backend_reviewer_available,
|
230
|
+
"tz_offset_hours" => 2.0,
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
let(:import_and_integrate_frontend_reviewer) do
|
235
|
+
Gitlab::Dangerfiles::Teammate.new(
|
236
|
+
"username" => "import-and-integrate-frontend-reviewer",
|
237
|
+
"name" => "Import and Integrate FE engineer",
|
238
|
+
"role" => "Frontend Engineer, Manage:Import and Integrate",
|
239
|
+
"projects" => { "gitlab" => "reviewer frontend" },
|
240
|
+
"available" => true,
|
241
|
+
"tz_offset_hours" => 2.0,
|
242
|
+
)
|
243
|
+
end
|
244
|
+
|
245
|
+
let(:workhorse_reviewer) do
|
246
|
+
Gitlab::Dangerfiles::Teammate.new(
|
247
|
+
"username" => "workhorse-reviewer",
|
248
|
+
"name" => "Workhorse reviewer",
|
249
|
+
"role" => "Backend engineer",
|
250
|
+
"projects" => { "gitlab-workhorse" => "reviewer" },
|
251
|
+
"available" => true,
|
252
|
+
"tz_offset_hours" => 2.0,
|
253
|
+
)
|
254
|
+
end
|
255
|
+
|
256
|
+
let(:workhorse_maintainer) do
|
257
|
+
Gitlab::Dangerfiles::Teammate.new(
|
258
|
+
"username" => "workhorse-maintainer",
|
259
|
+
"name" => "Workhorse maintainer",
|
260
|
+
"role" => "Backend engineer",
|
261
|
+
"projects" => { "gitlab-workhorse" => "maintainer" },
|
262
|
+
"available" => true,
|
263
|
+
"tz_offset_hours" => 2.0,
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
let(:teammates) do
|
268
|
+
[
|
269
|
+
backend_maintainer.to_h,
|
270
|
+
backend_reviewer.to_h,
|
271
|
+
frontend_maintainer.to_h,
|
272
|
+
frontend_reviewer.to_h,
|
273
|
+
ux_reviewer.to_h,
|
274
|
+
software_engineer_in_test.to_h,
|
275
|
+
tooling_reviewer.to_h,
|
276
|
+
ci_template_reviewer.to_h,
|
277
|
+
workhorse_reviewer.to_h,
|
278
|
+
workhorse_maintainer.to_h,
|
279
|
+
analytics_instrumentation_reviewer.to_h,
|
280
|
+
import_and_integrate_backend_reviewer.to_h,
|
281
|
+
import_and_integrate_frontend_reviewer.to_h,
|
282
|
+
software_engineer_in_import_integrate_fe.to_h,
|
283
|
+
software_engineer_in_import_integrate_be.to_h
|
284
|
+
]
|
285
|
+
end
|
286
|
+
|
287
|
+
let(:teammate_json) { teammates.to_json }
|
288
|
+
let(:teammate_pedroms) { instance_double(Gitlab::Dangerfiles::Teammate, name: "Pedro") }
|
289
|
+
let(:company_members) { Gitlab::Dangerfiles::Teammate.fetch_company_members }
|
290
|
+
|
291
|
+
before do
|
292
|
+
WebMock
|
293
|
+
.stub_request(:get, Gitlab::Dangerfiles::Teammate::ROULETTE_DATA_URL)
|
294
|
+
.to_return(body: teammate_json)
|
295
|
+
|
296
|
+
# This avoid changing the internal state of the class
|
297
|
+
allow(Gitlab::Dangerfiles::Teammate).to receive(:warnings).and_return([])
|
298
|
+
allow(Gitlab::Dangerfiles::Teammate).to receive(:company_members).and_return(company_members)
|
299
|
+
end
|
300
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gitlab
|
4
|
+
module Dangerfiles
|
5
|
+
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role) do
|
6
|
+
def no_reviewer?
|
7
|
+
reviewer.nil?
|
8
|
+
end
|
9
|
+
|
10
|
+
def no_maintainer?
|
11
|
+
maintainer.nil?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "spin"
|
4
|
+
require_relative "teammate"
|
5
|
+
require_relative "weightage/maintainers"
|
6
|
+
require_relative "weightage/reviewers"
|
7
|
+
|
8
|
+
module Gitlab
|
9
|
+
module Dangerfiles
|
10
|
+
class Spinner
|
11
|
+
attr_reader :project, :author, :team_author, :labels, :categories
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
project:, author:, team_author: nil, labels: [], categories: [],
|
15
|
+
random: Random.new, ux_fallback_wider_community_reviewer: nil)
|
16
|
+
@project = project
|
17
|
+
@author = author
|
18
|
+
@team_author = team_author
|
19
|
+
@labels = labels
|
20
|
+
@categories = categories.reject do |category|
|
21
|
+
import_and_integrate_reject_category?(category)
|
22
|
+
end
|
23
|
+
@random = random
|
24
|
+
@ux_fallback_wider_community_reviewer = ux_fallback_wider_community_reviewer
|
25
|
+
end
|
26
|
+
|
27
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
28
|
+
def spin
|
29
|
+
spins = categories.sort_by(&:to_s).map do |category|
|
30
|
+
spin_for_category(category)
|
31
|
+
end
|
32
|
+
|
33
|
+
backend_spin = spins.find { |spin| spin.category == :backend }
|
34
|
+
frontend_spin = spins.find { |spin| spin.category == :frontend }
|
35
|
+
|
36
|
+
spins.each do |spin|
|
37
|
+
case spin.category
|
38
|
+
when :qa
|
39
|
+
spin.optional_role = :maintainer if
|
40
|
+
categories.size > 1 && author_no_qa_capability?
|
41
|
+
when :test
|
42
|
+
spin.optional_role = :maintainer
|
43
|
+
|
44
|
+
if spin.no_reviewer?
|
45
|
+
# Fetch an already picked backend reviewer, or pick one otherwise
|
46
|
+
spin.reviewer = backend_spin&.reviewer || spin_for_category(:backend).reviewer
|
47
|
+
end
|
48
|
+
when :tooling
|
49
|
+
if spin.no_maintainer?
|
50
|
+
# Fetch an already picked backend maintainer, or pick one otherwise
|
51
|
+
spin.maintainer = backend_spin&.maintainer || spin_for_category(:backend).maintainer
|
52
|
+
end
|
53
|
+
when :ci_template # rubocop:disable Lint/DuplicateBranch -- bug?
|
54
|
+
if spin.no_maintainer?
|
55
|
+
# Fetch an already picked backend maintainer, or pick one otherwise
|
56
|
+
spin.maintainer = backend_spin&.maintainer || spin_for_category(:backend).maintainer
|
57
|
+
end
|
58
|
+
when :analytics_instrumentation
|
59
|
+
spin.optional_role = :maintainer
|
60
|
+
|
61
|
+
if spin.no_maintainer?
|
62
|
+
# Fetch an already picked maintainer, or pick one otherwise
|
63
|
+
spin.maintainer = backend_spin&.maintainer || frontend_spin&.maintainer || spin_for_category(:backend).maintainer
|
64
|
+
end
|
65
|
+
when :import_integrate_be, :import_integrate_fe
|
66
|
+
spin.optional_role = :maintainer
|
67
|
+
when :ux
|
68
|
+
spin.optional_role = :maintainer
|
69
|
+
|
70
|
+
# We want at least a UX reviewer who can review any wider community
|
71
|
+
# contribution even without a team designer. We assign this to Pedro.
|
72
|
+
spin.reviewer = ux_fallback_wider_community_reviewer if
|
73
|
+
labels.include?("Community contribution") &&
|
74
|
+
spin.no_reviewer? &&
|
75
|
+
spin.no_maintainer?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
spins
|
80
|
+
end
|
81
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
82
|
+
|
83
|
+
# Spin a reviewer for a particular approval rule
|
84
|
+
#
|
85
|
+
# @param [Hash] rule of approval
|
86
|
+
#
|
87
|
+
# @return [Gitlab::Dangerfiles::Teammate]
|
88
|
+
def spin_for_approver(rule)
|
89
|
+
approvers = rule["eligible_approvers"].filter_map do |approver|
|
90
|
+
Gitlab::Dangerfiles::Teammate.find_member(
|
91
|
+
approver["username"], project: project)
|
92
|
+
end
|
93
|
+
|
94
|
+
spin_for_person(approvers) || spin_for_approver_fallback(rule)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
attr_reader :random, :ux_fallback_wider_community_reviewer
|
100
|
+
|
101
|
+
# @param [String] category name
|
102
|
+
# @return [Boolean]
|
103
|
+
def import_and_integrate_reject_category?(category)
|
104
|
+
# Reject Import and Integrate categories if the MR author has reviewing abilities for the category.
|
105
|
+
team_author&.import_integrate_be?(project, category, labels) ||
|
106
|
+
team_author&.import_integrate_fe?(project, category, labels)
|
107
|
+
end
|
108
|
+
|
109
|
+
# MR includes QA changes, but also other changes, and author isn't an SET
|
110
|
+
def author_no_qa_capability?
|
111
|
+
!(team_author && team_author.capabilities(project).any? { |capability| capability.end_with?("qa") })
|
112
|
+
end
|
113
|
+
|
114
|
+
def spin_for_category(category)
|
115
|
+
reviewers, traintainers, maintainers =
|
116
|
+
%i[reviewer traintainer maintainer].map do |role|
|
117
|
+
spin_role_for_category(role, category)
|
118
|
+
end
|
119
|
+
|
120
|
+
weighted_reviewers = Weightage::Reviewers.new(reviewers, traintainers).execute
|
121
|
+
weighted_maintainers = Weightage::Maintainers.new(maintainers).execute
|
122
|
+
|
123
|
+
reviewer = spin_for_person(weighted_reviewers)
|
124
|
+
maintainer = spin_for_person(weighted_maintainers)
|
125
|
+
|
126
|
+
# allow projects with small number of reviewers to take from maintainers if possible
|
127
|
+
if reviewer.nil? && weighted_maintainers.uniq.size > 1
|
128
|
+
weighted_maintainers.delete(maintainer)
|
129
|
+
reviewer = spin_for_person(weighted_maintainers)
|
130
|
+
end
|
131
|
+
|
132
|
+
Spin.new(category, reviewer, maintainer, false)
|
133
|
+
end
|
134
|
+
|
135
|
+
def spin_role_for_category(role, category)
|
136
|
+
team.select do |member|
|
137
|
+
member.public_send(:"#{role}?", project, category, labels)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
|
142
|
+
# selection will change on next spin.
|
143
|
+
#
|
144
|
+
# @param [Array<Gitlab::Dangerfiles::Teammate>] people
|
145
|
+
#
|
146
|
+
# @return [Gitlab::Dangerfiles::Teammate]
|
147
|
+
def spin_for_person(people)
|
148
|
+
shuffled_people = people.shuffle(random: random)
|
149
|
+
|
150
|
+
shuffled_people.find { |person| valid_person?(person) }
|
151
|
+
end
|
152
|
+
|
153
|
+
# @param [Gitlab::Dangerfiles::Teammate] person
|
154
|
+
# @return [Boolean]
|
155
|
+
def valid_person?(person)
|
156
|
+
person.username != author && person.available
|
157
|
+
end
|
158
|
+
|
159
|
+
# It can be possible that we don't have a valid reviewer for approval.
|
160
|
+
# In this case, we sample again without considering:
|
161
|
+
#
|
162
|
+
# * If they're available
|
163
|
+
# * If they're an actual reviewer from roulette data
|
164
|
+
#
|
165
|
+
# We do this because we strictly require an approval from the approvers.
|
166
|
+
#
|
167
|
+
# @param [Hash] rule of approval
|
168
|
+
#
|
169
|
+
# @return [Gitlab::Dangerfiles::Teammate]
|
170
|
+
def spin_for_approver_fallback(rule)
|
171
|
+
fallback_approvers = rule["eligible_approvers"].map do |approver|
|
172
|
+
Teammate.find_member(approver["username"]) || Teammate.new(approver)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Intentionally not using `spin_for_person` to skip `valid_person?`.
|
176
|
+
# This should strictly return someone so we don't filter anything,
|
177
|
+
# and it's a fallback mechanism which should not happen often that
|
178
|
+
# deserves a complex algorithm.
|
179
|
+
fallback_approvers.sample(random: random)
|
180
|
+
end
|
181
|
+
|
182
|
+
# @return [Array<Gitlab::Dangerfiles::Teammate>]
|
183
|
+
def team
|
184
|
+
@team ||= Teammate.company_members.select do |member|
|
185
|
+
member.in_project?(project)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -1,10 +1,82 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
require_relative "capability"
|
4
7
|
|
5
8
|
module Gitlab
|
6
9
|
module Dangerfiles
|
7
10
|
class Teammate
|
11
|
+
ROULETTE_DATA_URL = "https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json"
|
12
|
+
|
13
|
+
def self.find_member(username, project: nil)
|
14
|
+
company_members.find do |member|
|
15
|
+
member.username == username &&
|
16
|
+
(project.nil? || member.in_project?(project))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.has_member_for_the_group?(category, labels:, **arguments)
|
21
|
+
capabilities = %i[reviewer maintainer].map do |kind|
|
22
|
+
# Use new to use the base class for original has_capability? method
|
23
|
+
Capability.new(category: category, kind: kind, labels: labels, **arguments)
|
24
|
+
end
|
25
|
+
|
26
|
+
company_members.any? do |teammate|
|
27
|
+
capabilities.any? do |capability|
|
28
|
+
capability.has_capability?(teammate) &&
|
29
|
+
teammate.member_of_the_group?(labels)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Looks up the current list of GitLab team members and parses it into a
|
35
|
+
# useful form.
|
36
|
+
#
|
37
|
+
# @return [Array<Gitlab::Dangerfiles::Teammate>]
|
38
|
+
def self.company_members
|
39
|
+
@company_members ||= fetch_company_members
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.fetch_company_members
|
43
|
+
data = http_get_json(ROULETTE_DATA_URL) || []
|
44
|
+
data.map { |hash| Teammate.new(hash) }
|
45
|
+
rescue JSON::ParserError
|
46
|
+
warnings << "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
|
47
|
+
[]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Fetches the given +url+ and parse its response as JSON.
|
51
|
+
#
|
52
|
+
# @param [String] url
|
53
|
+
#
|
54
|
+
# @return [Hash, Array, NilClass]
|
55
|
+
def self.http_get_json(url)
|
56
|
+
rsp = Net::HTTP.get_response(URI.parse(url))
|
57
|
+
|
58
|
+
if rsp.is_a?(Net::HTTPRedirection)
|
59
|
+
uri = URI.parse(rsp.header["location"])
|
60
|
+
|
61
|
+
uri.query = nil if uri
|
62
|
+
|
63
|
+
warnings << "Redirection detected: #{uri}."
|
64
|
+
return nil
|
65
|
+
end
|
66
|
+
|
67
|
+
unless rsp.is_a?(Net::HTTPOK)
|
68
|
+
message = rsp.message[0, 30]
|
69
|
+
warnings << "HTTPError: Failed to read #{url}: #{rsp.code} #{message}."
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
|
73
|
+
JSON.parse(rsp.body)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.warnings
|
77
|
+
@warnings ||= []
|
78
|
+
end
|
79
|
+
|
8
80
|
attr_reader :options, :username, :name, :role, :specialty, :projects, :available, :hungry, :reduced_capacity, :tz_offset_hours,
|
9
81
|
:only_maintainer_reviews
|
10
82
|
|
@@ -25,6 +97,20 @@ module Gitlab
|
|
25
97
|
@only_maintainer_reviews = options["only_maintainer_reviews"]
|
26
98
|
end
|
27
99
|
|
100
|
+
def member_of_the_group?(labels)
|
101
|
+
# Specialty can be:
|
102
|
+
# Source Code
|
103
|
+
# [Growth: Activation, Growth: Expansion]
|
104
|
+
# Runner
|
105
|
+
group_labels = Array(specialty).map do |field|
|
106
|
+
group = field.strip.sub(/^.+: ?/, "").downcase
|
107
|
+
|
108
|
+
"group::#{group}"
|
109
|
+
end
|
110
|
+
|
111
|
+
(group_labels & labels).any?
|
112
|
+
end
|
113
|
+
|
28
114
|
def to_h
|
29
115
|
options
|
30
116
|
end
|
@@ -128,7 +214,7 @@ module Gitlab
|
|
128
214
|
end
|
129
215
|
|
130
216
|
def has_capability?(project, category, kind, labels)
|
131
|
-
|
217
|
+
Capability.for(category, project: project, kind: kind, labels: labels).has_capability?(self)
|
132
218
|
end
|
133
219
|
|
134
220
|
def pluralize(count, singular, plural)
|