1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
# frozen_string_literal: true
require_relative 'teammate'
module Gitlab
module Danger
module Roulette
ROULETTE_DATA_URL = 'https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json'
HOURS_WHEN_PERSON_CAN_BE_PICKED = (6..14).freeze
INCLUDE_TIMEZONE_FOR_CATEGORY = {
database: false
}.freeze
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
# Assigns GitLab team members to be reviewer and maintainer
# for each change category that a Merge Request contains.
#
# @return [Array<Spin>]
def spin(project, categories, branch_name, timezone_experiment: false)
team =
begin
project_team(project)
rescue => err
warn("Reviewer roulette failed to load team data: #{err.message}")
[]
end
canonical_branch_name = canonical_branch_name(branch_name)
spin_per_category = categories.each_with_object({}) do |category, memo|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)
memo[category] = spin_for_category(team, project, category, canonical_branch_name, timezone_experiment: including_timezone)
end
spin_per_category.map do |category, spin|
case category
when :test
if spin.reviewer.nil?
# Fetch an already picked backend reviewer, or pick one otherwise
spin.reviewer = spin_per_category[:backend]&.reviewer || spin_for_category(team, project, :backend, canonical_branch_name).reviewer
end
when :engineering_productivity
if spin.maintainer.nil?
# Fetch an already picked backend maintainer, or pick one otherwise
spin.maintainer = spin_per_category[:backend]&.maintainer || spin_for_category(team, project, :backend, canonical_branch_name).maintainer
end
end
spin
end
end
# Looks up the current list of GitLab team members and parses it into a
# useful form
#
# @return [Array<Teammate>]
def team
@team ||=
begin
data = Gitlab::Danger::RequestHelper.http_get_json(ROULETTE_DATA_URL)
data.map { |hash| ::Gitlab::Danger::Teammate.new(hash) }
rescue JSON::ParserError
raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
end
end
# Like +team+, but only returns teammates in the current project, based on
# project_name.
#
# @return [Array<Teammate>]
def project_team(project_name)
team.select { |member| member.in_project?(project_name) }
end
def canonical_branch_name(branch_name)
branch_name.gsub(/^[ce]e-|-[ce]e$/, '')
end
def new_random(seed)
Random.new(Digest::MD5.hexdigest(seed).to_i(16))
end
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
# selection will change on next spin
# @param [Array<Teammate>] people
def spin_for_person(people, random:, timezone_experiment: false)
shuffled_people = people.shuffle(random: random)
if timezone_experiment
shuffled_people.find(&method(:valid_person_with_timezone?))
else
shuffled_people.find(&method(:valid_person?))
end
end
private
# @param [Teammate] person
# @return [Boolean]
def valid_person?(person)
!mr_author?(person) && person.available
end
# @param [Teammate] person
# @return [Boolean]
def valid_person_with_timezone?(person)
valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
end
# @param [Teammate] person
# @return [Boolean]
def mr_author?(person)
person.username == gitlab.mr_author
end
def spin_role_for_category(team, role, project, category)
team.select do |member|
member.public_send("#{role}?", project, category, gitlab.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
end
end
def spin_for_category(team, project, category, branch_name, timezone_experiment: false)
reviewers, traintainers, maintainers =
%i[reviewer traintainer maintainer].map do |role|
spin_role_for_category(team, role, project, category)
end
# TODO: take CODEOWNERS into account?
# https://gitlab.com/gitlab-org/gitlab/issues/26723
# Make traintainers have triple the chance to be picked as a reviewer
random = new_random(branch_name)
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
Spin.new(category, reviewer, maintainer)
end
end
end
end
|