gitlab-dangerfiles 4.5.1 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.5.1"
3
+ VERSION = "4.7.0"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-dangerfiles
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.5.1
4
+ version: 4.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitLab
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-23 00:00:00.000000000 Z
11
+ date: 2024-03-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -140,14 +140,14 @@ dependencies:
140
140
  name: rubocop-rails
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - "!="
143
+ - - "<"
144
144
  - !ruby/object:Gem::Version
145
145
  version: 2.21.2
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - "!="
150
+ - - "<"
151
151
  - !ruby/object:Gem::Version
152
152
  version: 2.21.2
153
153
  - !ruby/object:Gem::Dependency
@@ -213,6 +213,7 @@ files:
213
213
  - CONTRIBUTING.md
214
214
  - Dangerfile
215
215
  - Gemfile
216
+ - Gemfile.lock
216
217
  - Guardfile
217
218
  - LICENSE.txt
218
219
  - README.md
@@ -238,14 +239,17 @@ files:
238
239
  - lib/gitlab-dangerfiles.rb
239
240
  - lib/gitlab/Dangerfile
240
241
  - lib/gitlab/dangerfiles.rb
242
+ - lib/gitlab/dangerfiles/approval.rb
241
243
  - lib/gitlab/dangerfiles/base_linter.rb
242
- - lib/gitlab/dangerfiles/category.rb
244
+ - lib/gitlab/dangerfiles/capability.rb
243
245
  - lib/gitlab/dangerfiles/changes.rb
244
246
  - lib/gitlab/dangerfiles/commit_linter.rb
245
247
  - lib/gitlab/dangerfiles/config.rb
246
248
  - lib/gitlab/dangerfiles/emoji_checker.rb
247
249
  - lib/gitlab/dangerfiles/merge_request_linter.rb
248
250
  - lib/gitlab/dangerfiles/spec_helper.rb
251
+ - lib/gitlab/dangerfiles/spin.rb
252
+ - lib/gitlab/dangerfiles/spinner.rb
249
253
  - lib/gitlab/dangerfiles/task_loader.rb
250
254
  - lib/gitlab/dangerfiles/tasks/main.rake
251
255
  - lib/gitlab/dangerfiles/teammate.rb