gitlab-dangerfiles 4.6.0 → 4.7.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.
@@ -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
- require_relative "category"
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
- Category.for(category, project: project, kind: kind, labels: labels).has_capability?(self)
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)
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Dangerfiles
3
- VERSION = "4.6.0"
3
+ VERSION = "4.7.0"
4
4
  end
5
5
  end