class Danger::Roulette

Common helper functions for our danger scripts. See Danger::Helper for more details

Constants

HOURS_WHEN_PERSON_CAN_BE_PICKED
HTTPError
INCLUDE_TIMEZONE_FOR_CATEGORY
ROULETTE_DATA_URL
Spin

Public Instance Methods

spin(project, categories = [nil], timezone_experiment: false) click to toggle source

Assigns GitLab team members to be reviewer and maintainer for the given categories.

@param project [String] A project path. @param categories [Array<Symbol>] An array of categories symbols. @param timezone_experiment [Boolean] Whether to select reviewers based in timezone or not.

@return [Array<Spin>]

# File lib/danger/plugins/roulette.rb, line 36
def spin(project, categories = [nil], timezone_experiment: false)
  spins = categories.sort_by(&:to_s).map do |category|
    including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(category, timezone_experiment)

    spin_for_category(project, category, timezone_experiment: including_timezone)
  end

  backend_spin = spins.find { |spin| spin.category == :backend }
  frontend_spin = spins.find { |spin| spin.category == :frontend }

  spins.each do |spin|
    including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(spin.category, timezone_experiment)
    case spin.category
    when :qa
      # MR includes QA changes, but also other changes, and author isn't an SET
      if categories.size > 1 && !team_mr_author&.any_capability?(project, spin.category)
        spin.optional_role = :maintainer
      end
    when :test
      spin.optional_role = :maintainer

      if spin.reviewer.nil?
        # Fetch an already picked backend reviewer, or pick one otherwise
        spin.reviewer = backend_spin&.reviewer || spin_for_category(project, :backend, timezone_experiment: including_timezone).reviewer
      end
    when :tooling, :engineering_productivity # Deprecated as of 2.3.0 in favor of tooling
      if spin.maintainer.nil?
        # Fetch an already picked backend maintainer, or pick one otherwise
        spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
      end
    when :ci_template
      if spin.maintainer.nil?
        # Fetch an already picked backend maintainer, or pick one otherwise
        spin.maintainer = backend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
      end
    when :product_intelligence
      spin.optional_role = :maintainer

      if spin.maintainer.nil?
        # Fetch an already picked maintainer, or pick one otherwise
        spin.maintainer = backend_spin&.maintainer || frontend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
      end
    end
  end

  spins
end
team_mr_author() click to toggle source

Finds the Gitlab::Dangerfiles::Teammate object whose username matches the MR author username.

@return [Gitlab::Dangerfiles::Teammate]

# File lib/danger/plugins/roulette.rb, line 24
def team_mr_author
  company_members.find { |person| person.username == helper.mr_author }
end

Private Instance Methods

company_members() click to toggle source

Looks up the current list of GitLab team members and parses it into a useful form.

@return [Array<Gitlab::Dangerfiles::Teammate>]

# File lib/danger/plugins/roulette.rb, line 167
def company_members
  @company_members ||= begin
      data = http_get_json(ROULETTE_DATA_URL)
      data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
    rescue JSON::ParserError
      raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
    end
end
http_get_json(url) click to toggle source

Fetches the given url and parse its response as JSON.

@param [String] url

@return [Hash, Array]

# File lib/danger/plugins/roulette.rb, line 153
def http_get_json(url)
  rsp = Net::HTTP.get_response(URI.parse(url))

  unless rsp.is_a?(Net::HTTPOK)
    raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
  end

  JSON.parse(rsp.body)
end
mr_author?(person) click to toggle source

@param [Gitlab::Dangerfiles::Teammate] person @return [Boolean]

# File lib/danger/plugins/roulette.rb, line 100
def mr_author?(person)
  person.username == helper.mr_author
end
new_random(seed) click to toggle source
# File lib/danger/plugins/roulette.rb, line 104
def new_random(seed)
  Random.new(Digest::MD5.hexdigest(seed).to_i(16))
end
project_team(project_name) click to toggle source

Like team, but only returns teammates in the current project, based on project_name.

@return [Array<Gitlab::Dangerfiles::Teammate>]

# File lib/danger/plugins/roulette.rb, line 180
def project_team(project_name)
  company_members.select { |member| member.in_project?(project_name) }
rescue => err
  warn("Reviewer roulette failed to load team data: #{err.message}")
  []
end
spin_for_category(project, category, timezone_experiment: false) click to toggle source
# File lib/danger/plugins/roulette.rb, line 130
def spin_for_category(project, category, timezone_experiment: false)
  team = project_team(project)
  reviewers, traintainers, maintainers =
    %i[reviewer traintainer maintainer].map do |role|
      spin_role_for_category(team, role, project, category)
    end

  random = new_random(helper.mr_source_branch)

  weighted_reviewers = Gitlab::Dangerfiles::Weightage::Reviewers.new(reviewers, traintainers).execute
  weighted_maintainers = Gitlab::Dangerfiles::Weightage::Maintainers.new(maintainers).execute

  reviewer = spin_for_person(weighted_reviewers, random: random, timezone_experiment: timezone_experiment)
  maintainer = spin_for_person(weighted_maintainers, random: random, timezone_experiment: timezone_experiment)

  Spin.new(category, reviewer, maintainer, false, timezone_experiment)
end
spin_for_person(people, random:, timezone_experiment: false) click to toggle source

Known issue: If someone is rejected due to OOO, and then becomes not OOO, the selection will change on next spin.

@param [Array<Gitlab::Dangerfiles::Teammate>] people

@return [Gitlab::Dangerfiles::Teammate]

# File lib/danger/plugins/roulette.rb, line 120
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
spin_role_for_category(team, role, project, category) click to toggle source
# File lib/danger/plugins/roulette.rb, line 108
def spin_role_for_category(team, role, project, category)
  team.select do |member|
    member.public_send("#{role}?", project, category, helper.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
  end
end
valid_person?(person) click to toggle source

@param [Gitlab::Dangerfiles::Teammate] person @return [Boolean]

# File lib/danger/plugins/roulette.rb, line 88
def valid_person?(person)
  !mr_author?(person) && person.available
end
valid_person_with_timezone?(person) click to toggle source

@param [Gitlab::Dangerfiles::Teammate] person @return [Boolean]

# File lib/danger/plugins/roulette.rb, line 94
def valid_person_with_timezone?(person)
  valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
end