gitlab-dangerfiles 0.1.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 +7 -0
- data/.gitignore +14 -0
- data/.gitlab-ci.yml +43 -0
- data/.gitlab/merge_request_templates/Release.md +35 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +43 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fixtures/emojis/aliases.json +542 -0
- data/fixtures/emojis/digests.json +12553 -0
- data/gitlab-dangerfiles.gemspec +38 -0
- data/lib/danger/changelog.rb +39 -0
- data/lib/danger/helper.rb +260 -0
- data/lib/danger/roulette.rb +135 -0
- data/lib/danger/sidekiq_queues.rb +37 -0
- data/lib/gitlab-dangerfiles.rb +1 -0
- data/lib/gitlab/Dangerfile +1 -0
- data/lib/gitlab/dangerfiles.rb +40 -0
- data/lib/gitlab/dangerfiles/bundle_size/Dangerfile +38 -0
- data/lib/gitlab/dangerfiles/ce_ee_vue_templates/Dangerfile +56 -0
- data/lib/gitlab/dangerfiles/changelog/Dangerfile +90 -0
- data/lib/gitlab/dangerfiles/changes_size/Dangerfile +17 -0
- data/lib/gitlab/dangerfiles/commit_linter.rb +226 -0
- data/lib/gitlab/dangerfiles/commit_messages/Dangerfile +135 -0
- data/lib/gitlab/dangerfiles/database/Dangerfile +67 -0
- data/lib/gitlab/dangerfiles/documentation/Dangerfile +29 -0
- data/lib/gitlab/dangerfiles/duplicate_yarn_dependencies/Dangerfile +29 -0
- data/lib/gitlab/dangerfiles/emoji_checker.rb +45 -0
- data/lib/gitlab/dangerfiles/eslint/Dangerfile +31 -0
- data/lib/gitlab/dangerfiles/frozen_string/Dangerfile +28 -0
- data/lib/gitlab/dangerfiles/karma/Dangerfile +51 -0
- data/lib/gitlab/dangerfiles/metadata/Dangerfile +50 -0
- data/lib/gitlab/dangerfiles/popen.rb +55 -0
- data/lib/gitlab/dangerfiles/prettier/Dangerfile +41 -0
- data/lib/gitlab/dangerfiles/roulette/Dangerfile +97 -0
- data/lib/gitlab/dangerfiles/sidekiq_queues/Dangerfile +27 -0
- data/lib/gitlab/dangerfiles/specs/Dangerfile +42 -0
- data/lib/gitlab/dangerfiles/tasks.rb +19 -0
- data/lib/gitlab/dangerfiles/teammate.rb +106 -0
- data/lib/gitlab/dangerfiles/telemetry/Dangerfile +32 -0
- data/lib/gitlab/dangerfiles/utility_css/Dangerfile +51 -0
- data/lib/gitlab/dangerfiles/version.rb +5 -0
- metadata +191 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative "lib/gitlab/dangerfiles/version"
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "gitlab-dangerfiles"
|
5
|
+
spec.version = Gitlab::Dangerfiles::VERSION
|
6
|
+
spec.authors = ["Rémy Coutable"]
|
7
|
+
spec.email = ["remy@rymai.me"]
|
8
|
+
|
9
|
+
spec.summary = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
|
10
|
+
spec.description = %q{This gem provides common Dangerfile and plugins for GitLab projects.}
|
11
|
+
spec.homepage = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
14
|
+
|
15
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
16
|
+
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
18
|
+
spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
|
19
|
+
spec.metadata["changelog_uri"] = "https://gitlab.com/gitlab-org/gitlab-dangerfiles"
|
20
|
+
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
23
|
+
spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
25
|
+
end
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_dependency "danger"
|
31
|
+
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "rspec-parameterized"
|
34
|
+
spec.add_development_dependency "timecop"
|
35
|
+
spec.add_development_dependency "webmock"
|
36
|
+
spec.add_development_dependency "climate_control"
|
37
|
+
spec.add_development_dependency "rufo"
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Danger
|
4
|
+
# Common helper functions for our danger scripts. See Danger::Helper
|
5
|
+
# for more details
|
6
|
+
class Changelog < Danger::Plugin
|
7
|
+
NO_CHANGELOG_LABELS = [
|
8
|
+
"backstage", # To be removed by https://gitlab.com/gitlab-org/gitlab/-/issues/222360.
|
9
|
+
"tooling",
|
10
|
+
"tooling::pipelines",
|
11
|
+
"tooling::workflow",
|
12
|
+
"ci-build",
|
13
|
+
"meta",
|
14
|
+
].freeze
|
15
|
+
NO_CHANGELOG_CATEGORIES = %i[docs none].freeze
|
16
|
+
|
17
|
+
def needed?
|
18
|
+
categories_need_changelog? && (gitlab.mr_labels & NO_CHANGELOG_LABELS).empty?
|
19
|
+
end
|
20
|
+
|
21
|
+
def found
|
22
|
+
@found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
|
23
|
+
end
|
24
|
+
|
25
|
+
def sanitized_mr_title
|
26
|
+
gitlab.mr_json["title"].gsub(/^WIP: */, "").gsub(/`/, '\\\`')
|
27
|
+
end
|
28
|
+
|
29
|
+
def ee_changelog?
|
30
|
+
found.start_with?("ee/")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def categories_need_changelog?
|
36
|
+
(helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any?
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
require_relative "../gitlab/dangerfiles/teammate"
|
6
|
+
|
7
|
+
module Danger
|
8
|
+
# Common helper functions for our danger scripts.
|
9
|
+
class Helper < Danger::Plugin
|
10
|
+
RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
|
11
|
+
CATEGORY_LABELS = {
|
12
|
+
docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
|
13
|
+
none: "",
|
14
|
+
qa: "~QA",
|
15
|
+
test: "~test ~Quality for `spec/features/*`",
|
16
|
+
engineering_productivity: '~"Engineering Productivity" for CI, Danger',
|
17
|
+
}.freeze
|
18
|
+
# First-match win, so be sure to put more specific regex at the top...
|
19
|
+
CATEGORIES = {
|
20
|
+
%r{\Adoc/} => :docs,
|
21
|
+
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
|
22
|
+
|
23
|
+
%r{\A(ee/)?app/(assets|views)/} => :frontend,
|
24
|
+
%r{\A(ee/)?public/} => :frontend,
|
25
|
+
%r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend,
|
26
|
+
%r{\A(ee/)?vendor/assets/} => :frontend,
|
27
|
+
%r{\A(ee/)?scripts/frontend/} => :frontend,
|
28
|
+
%r{(\A|/)(
|
29
|
+
\.babelrc |
|
30
|
+
\.eslintignore |
|
31
|
+
\.eslintrc(\.yml)? |
|
32
|
+
\.nvmrc |
|
33
|
+
\.prettierignore |
|
34
|
+
\.prettierrc |
|
35
|
+
\.scss-lint.yml |
|
36
|
+
\.stylelintrc |
|
37
|
+
\.haml-lint.yml |
|
38
|
+
\.haml-lint_todo.yml |
|
39
|
+
babel\.config\.js |
|
40
|
+
jest\.config\.js |
|
41
|
+
package\.json |
|
42
|
+
yarn\.lock |
|
43
|
+
config/.+\.js
|
44
|
+
)\z}x => :frontend,
|
45
|
+
|
46
|
+
%r{(\A|/)(
|
47
|
+
\.gitlab/ci/frontend\.gitlab-ci\.yml
|
48
|
+
)\z}x => %i[frontend engineering_productivity],
|
49
|
+
|
50
|
+
%r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
|
51
|
+
%r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database,
|
52
|
+
%r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
|
53
|
+
%r{\A(ee/)?app/finders/} => :database,
|
54
|
+
%r{\Arubocop/cop/migration(/|\.rb)} => :database,
|
55
|
+
|
56
|
+
%r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
|
57
|
+
%r{\A\.codeclimate\.yml\z} => :engineering_productivity,
|
58
|
+
%r{\A\.overcommit\.yml\.example\z} => :engineering_productivity,
|
59
|
+
%r{\A\.editorconfig\z} => :engineering_productivity,
|
60
|
+
%r{Dangerfile\z} => :engineering_productivity,
|
61
|
+
%r{\A(ee/)?(danger/|lib/gitlab/danger/)} => :engineering_productivity,
|
62
|
+
%r{\A(ee/)?scripts/} => :engineering_productivity,
|
63
|
+
%r{\Atooling/} => :engineering_productivity,
|
64
|
+
|
65
|
+
%r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
|
66
|
+
%r{\A(ee/)?(bin|config|generator_templates|lib|rubocop)/} => :backend,
|
67
|
+
%r{\A(ee/)?spec/features/} => :test,
|
68
|
+
%r{\A(ee/)?spec/} => :backend,
|
69
|
+
%r{\A(ee/)?vendor/} => :backend,
|
70
|
+
%r{\A(Gemfile|Gemfile.lock|Rakefile)\z} => :backend,
|
71
|
+
%r{\A[A-Z_]+_VERSION\z} => :backend,
|
72
|
+
%r{\A\.rubocop(_todo)?\.yml\z} => :backend,
|
73
|
+
%r{\Afile_hooks/} => :backend,
|
74
|
+
|
75
|
+
%r{\A(ee/)?qa/} => :qa,
|
76
|
+
|
77
|
+
# Files that don't fit into any category are marked with :none
|
78
|
+
%r{\A(ee/)?changelogs/} => :none,
|
79
|
+
%r{\Alocale/gitlab\.pot\z} => :none,
|
80
|
+
|
81
|
+
# Fallbacks in case the above patterns miss anything
|
82
|
+
%r{\.rb\z} => :backend,
|
83
|
+
%r{(
|
84
|
+
\.(md|txt)\z |
|
85
|
+
\.markdownlint\.json
|
86
|
+
)}x => :none, # To reinstate roulette for documentation, set to `:docs`.
|
87
|
+
%r{\.js\z} => :frontend,
|
88
|
+
}.freeze
|
89
|
+
|
90
|
+
HTTPError = Class.new(StandardError)
|
91
|
+
|
92
|
+
def gitlab_helper
|
93
|
+
# Unfortunately the following does not work:
|
94
|
+
# - respond_to?(:gitlab)
|
95
|
+
# - respond_to?(:gitlab, true)
|
96
|
+
gitlab
|
97
|
+
rescue NoMethodError
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def rule_names
|
102
|
+
ci? ? Gitlab::Dangerfiles::LOCAL_RULES | Gitlab::Dangerfiles::CI_ONLY_RULES : Gitlab::Dangerfiles::LOCAL_RULES
|
103
|
+
end
|
104
|
+
|
105
|
+
def html_link(str)
|
106
|
+
ci? ? gitlab_helper.html_link(str) : str
|
107
|
+
end
|
108
|
+
|
109
|
+
def ci?
|
110
|
+
!gitlab_helper.nil?
|
111
|
+
end
|
112
|
+
|
113
|
+
# @param [String] url
|
114
|
+
def http_get_json(url)
|
115
|
+
rsp = Net::HTTP.get_response(URI.parse(url))
|
116
|
+
|
117
|
+
unless rsp.is_a?(Net::HTTPOK)
|
118
|
+
raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
|
119
|
+
end
|
120
|
+
|
121
|
+
JSON.parse(rsp.body)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Returns a list of all files that have been added, modified or renamed.
|
125
|
+
# `git.modified_files` might contain paths that already have been renamed,
|
126
|
+
# so we need to remove them from the list.
|
127
|
+
#
|
128
|
+
# Considering these changes:
|
129
|
+
#
|
130
|
+
# - A new_file.rb
|
131
|
+
# - D deleted_file.rb
|
132
|
+
# - M modified_file.rb
|
133
|
+
# - R renamed_file_before.rb -> renamed_file_after.rb
|
134
|
+
#
|
135
|
+
# it will return
|
136
|
+
# ```
|
137
|
+
# [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ]
|
138
|
+
# ```
|
139
|
+
#
|
140
|
+
# @return [Array<String>]
|
141
|
+
def all_changed_files
|
142
|
+
Set.new
|
143
|
+
.merge(git.added_files.to_a)
|
144
|
+
.merge(git.modified_files.to_a)
|
145
|
+
.merge(git.renamed_files.map { |x| x[:after] })
|
146
|
+
.subtract(git.renamed_files.map { |x| x[:before] })
|
147
|
+
.to_a
|
148
|
+
.sort
|
149
|
+
end
|
150
|
+
|
151
|
+
def all_ee_changes
|
152
|
+
all_changed_files.grep(%r{\Aee/})
|
153
|
+
end
|
154
|
+
|
155
|
+
def ee?
|
156
|
+
# Support former project name for `dev` and support local Danger run
|
157
|
+
%w[gitlab gitlab-ee].include?(ENV["CI_PROJECT_NAME"]) || Dir.exist?("../../ee")
|
158
|
+
end
|
159
|
+
|
160
|
+
def release_automation?
|
161
|
+
gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
|
162
|
+
end
|
163
|
+
|
164
|
+
def project_name
|
165
|
+
ee? ? "gitlab" : "gitlab-foss"
|
166
|
+
end
|
167
|
+
|
168
|
+
def markdown_list(items)
|
169
|
+
list = items.map { |item| "* `#{item}`" }.join("\n")
|
170
|
+
|
171
|
+
if items.size > 10
|
172
|
+
"\n<details>\n\n#{list}\n\n</details>\n"
|
173
|
+
else
|
174
|
+
list
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# @return [Hash<String,Array<String>>]
|
179
|
+
def changes_by_category
|
180
|
+
all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
|
181
|
+
categories_for_file(file).each { |category| hash[category] << file }
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`.
|
186
|
+
# @return Array<Symbol>
|
187
|
+
def categories_for_file(file)
|
188
|
+
_, categories = CATEGORIES.find { |regexp, _| regexp.match?(file) }
|
189
|
+
|
190
|
+
Array(categories || :unknown)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns the GFM for a category label, making its best guess if it's not
|
194
|
+
# a category we know about.
|
195
|
+
#
|
196
|
+
# @return[String]
|
197
|
+
def label_for_category(category)
|
198
|
+
CATEGORY_LABELS.fetch(category, "~#{category}")
|
199
|
+
end
|
200
|
+
|
201
|
+
def new_teammates(usernames)
|
202
|
+
usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
|
203
|
+
end
|
204
|
+
|
205
|
+
def missing_database_labels(current_mr_labels)
|
206
|
+
labels = if has_database_scoped_labels?(current_mr_labels)
|
207
|
+
["database"]
|
208
|
+
else
|
209
|
+
["database", "database::review pending"]
|
210
|
+
end
|
211
|
+
|
212
|
+
labels - current_mr_labels
|
213
|
+
end
|
214
|
+
|
215
|
+
def sanitize_mr_title(title)
|
216
|
+
title.gsub(/^WIP: */, "").gsub(/`/, '\\\`')
|
217
|
+
end
|
218
|
+
|
219
|
+
def security_mr?
|
220
|
+
return false unless gitlab_helper
|
221
|
+
|
222
|
+
gitlab_helper.mr_json["web_url"].include?("/gitlab-org/security/")
|
223
|
+
end
|
224
|
+
|
225
|
+
def cherry_pick_mr?
|
226
|
+
return false unless gitlab_helper
|
227
|
+
|
228
|
+
/cherry[\s-]*pick/i.match?(gitlab_helper.mr_json["title"])
|
229
|
+
end
|
230
|
+
|
231
|
+
def stable_branch?
|
232
|
+
return false unless gitlab_helper
|
233
|
+
|
234
|
+
/\A\d+-\d+-stable-ee/i.match?(gitlab_helper.mr_json["target_branch"])
|
235
|
+
end
|
236
|
+
|
237
|
+
def mr_has_labels?(*labels)
|
238
|
+
return false unless gitlab_helper
|
239
|
+
|
240
|
+
labels = labels.flatten.uniq
|
241
|
+
(labels & gitlab_helper.mr_labels) == labels
|
242
|
+
end
|
243
|
+
|
244
|
+
def labels_list(labels, sep: ", ")
|
245
|
+
labels.map { |label| %Q{~"#{label}"} }.join(sep)
|
246
|
+
end
|
247
|
+
|
248
|
+
def prepare_labels_for_mr(labels)
|
249
|
+
return "" unless labels.any?
|
250
|
+
|
251
|
+
"/label #{labels_list(labels, sep: " ")}"
|
252
|
+
end
|
253
|
+
|
254
|
+
private
|
255
|
+
|
256
|
+
def has_database_scoped_labels?(current_mr_labels)
|
257
|
+
current_mr_labels.any? { |label| label.start_with?("database::") }
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../gitlab/dangerfiles/teammate"
|
4
|
+
|
5
|
+
module Danger
|
6
|
+
# Common helper functions for our danger scripts. See Danger::Helper
|
7
|
+
# for more details
|
8
|
+
class Roulette < Danger::Plugin
|
9
|
+
ROULETTE_DATA_URL = "https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json"
|
10
|
+
HOURS_WHEN_PERSON_CAN_BE_PICKED = (6..14).freeze
|
11
|
+
|
12
|
+
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
|
13
|
+
|
14
|
+
# Assigns GitLab team members to be reviewer and maintainer
|
15
|
+
# for each change category that a Merge Request contains.
|
16
|
+
#
|
17
|
+
# @return [Array<Spin>]
|
18
|
+
def spin(project, categories, branch_name, timezone_experiment: false)
|
19
|
+
team = begin
|
20
|
+
project_team(project)
|
21
|
+
rescue => err
|
22
|
+
warn("Reviewer roulette failed to load team data: #{err.message}")
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
canonical_branch_name = canonical_branch_name(branch_name)
|
27
|
+
|
28
|
+
spin_per_category = categories.each_with_object({}) do |category, memo|
|
29
|
+
memo[category] = spin_for_category(team, project, category, canonical_branch_name, timezone_experiment: timezone_experiment)
|
30
|
+
end
|
31
|
+
|
32
|
+
spin_per_category.map do |category, spin|
|
33
|
+
case category
|
34
|
+
when :test
|
35
|
+
if spin.reviewer.nil?
|
36
|
+
# Fetch an already picked backend reviewer, or pick one otherwise
|
37
|
+
spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer
|
38
|
+
end
|
39
|
+
when :engineering_productivity
|
40
|
+
if spin.maintainer.nil?
|
41
|
+
# Fetch an already picked backend maintainer, or pick one otherwise
|
42
|
+
spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
spin
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Looks up the current list of GitLab team members and parses it into a
|
51
|
+
# useful form
|
52
|
+
#
|
53
|
+
# @return [Array<Teammate>]
|
54
|
+
def team
|
55
|
+
@team ||= begin
|
56
|
+
data = helper.http_get_json(ROULETTE_DATA_URL)
|
57
|
+
data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
|
58
|
+
rescue JSON::ParserError
|
59
|
+
raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Like +team+, but only returns teammates in the current project, based on
|
64
|
+
# project_name.
|
65
|
+
#
|
66
|
+
# @return [Array<Teammate>]
|
67
|
+
def project_team(project_name)
|
68
|
+
team.select { |member| member.in_project?(project_name) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def canonical_branch_name(branch_name)
|
72
|
+
branch_name.gsub(/^[ce]e-|-[ce]e$/, "")
|
73
|
+
end
|
74
|
+
|
75
|
+
def new_random(seed)
|
76
|
+
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
|
80
|
+
# selection will change on next spin
|
81
|
+
# @param [Array<Teammate>] people
|
82
|
+
def spin_for_person(people, random:, timezone_experiment: false)
|
83
|
+
shuffled_people = people.shuffle(random: random)
|
84
|
+
|
85
|
+
if timezone_experiment
|
86
|
+
shuffled_people.find(&method(:valid_person_with_timezone?))
|
87
|
+
else
|
88
|
+
shuffled_people.find(&method(:valid_person?))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# @param [Teammate] person
|
95
|
+
# @return [Boolean]
|
96
|
+
def valid_person?(person)
|
97
|
+
!mr_author?(person) && person.available
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param [Teammate] person
|
101
|
+
# @return [Boolean]
|
102
|
+
def valid_person_with_timezone?(person)
|
103
|
+
valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
|
104
|
+
end
|
105
|
+
|
106
|
+
# @param [Teammate] person
|
107
|
+
# @return [Boolean]
|
108
|
+
def mr_author?(person)
|
109
|
+
person.username == gitlab.mr_author
|
110
|
+
end
|
111
|
+
|
112
|
+
def spin_role_for_category(team, role, project, category)
|
113
|
+
team.select do |member|
|
114
|
+
member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def spin_for_category(team, project, category, branch_name, timezone_experiment: false)
|
119
|
+
reviewers, traintainers, maintainers =
|
120
|
+
%i[reviewer traintainer maintainer].map do |role|
|
121
|
+
spin_role_for_category(team, role, project, category)
|
122
|
+
end
|
123
|
+
|
124
|
+
# TODO: take CODEOWNERS into account?
|
125
|
+
# https://gitlab.com/gitlab-org/gitlab/issues/26723
|
126
|
+
|
127
|
+
# Make traintainers have triple the chance to be picked as a reviewer
|
128
|
+
random = new_random(branch_name)
|
129
|
+
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
|
130
|
+
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
|
131
|
+
|
132
|
+
Spin.new(category, reviewer, maintainer)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|