2019-09-04 21:01:54 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
require_relative 'teammate'
|
|
|
|
|
|
|
|
module Gitlab
|
|
|
|
module Danger
|
|
|
|
module Roulette
|
2020-07-28 23:09:34 +05:30
|
|
|
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
|
2020-06-23 00:09:42 +05:30
|
|
|
|
|
|
|
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>]
|
2020-07-28 23:09:34 +05:30
|
|
|
def spin(project, categories, branch_name, timezone_experiment: false)
|
2020-06-23 00:09:42 +05:30
|
|
|
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|
|
2020-07-28 23:09:34 +05:30
|
|
|
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)
|
2020-06-23 00:09:42 +05:30
|
|
|
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
|
2019-09-04 21:01:54 +05:30
|
|
|
|
|
|
|
# Looks up the current list of GitLab team members and parses it into a
|
|
|
|
# useful form
|
|
|
|
#
|
|
|
|
# @return [Array<Teammate>]
|
|
|
|
def team
|
|
|
|
@team ||=
|
|
|
|
begin
|
2019-12-21 20:55:43 +05:30
|
|
|
data = Gitlab::Danger::RequestHelper.http_get_json(ROULETTE_DATA_URL)
|
2019-09-04 21:01:54 +05:30
|
|
|
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
|
2019-12-21 20:55:43 +05:30
|
|
|
# @param [Array<Teammate>] people
|
2020-07-28 23:09:34 +05:30
|
|
|
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
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
# @param [Teammate] person
|
|
|
|
# @return [Boolean]
|
2019-09-04 21:01:54 +05:30
|
|
|
def valid_person?(person)
|
2020-07-28 23:09:34 +05:30
|
|
|
!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)
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
|
2019-12-21 20:55:43 +05:30
|
|
|
# @param [Teammate] person
|
|
|
|
# @return [Boolean]
|
2019-09-04 21:01:54 +05:30
|
|
|
def mr_author?(person)
|
|
|
|
person.username == gitlab.mr_author
|
|
|
|
end
|
2020-06-23 00:09:42 +05:30
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
def spin_for_category(team, project, category, branch_name, timezone_experiment: false)
|
2020-06-23 00:09:42 +05:30
|
|
|
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)
|
2020-07-28 23:09:34 +05:30
|
|
|
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random, timezone_experiment: timezone_experiment)
|
|
|
|
maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment)
|
2020-06-23 00:09:42 +05:30
|
|
|
|
2020-07-28 23:09:34 +05:30
|
|
|
Spin.new(category, reviewer, maintainer)
|
2020-06-23 00:09:42 +05:30
|
|
|
end
|
2019-09-04 21:01:54 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|