Welcome to mirror list, hosted at ThFree Co, Russian Federation.

roulette.rb « danger « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 21feda2cf209b386ee10ade042b2faec58dd891d (plain)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# frozen_string_literal: true

require_relative 'teammate'
require_relative 'request_helper' unless defined?(Gitlab::Danger::RequestHelper)
require_relative 'weightage/reviewers'
require_relative 'weightage/maintainers'

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, :timezone_experiment)

      def team_mr_author
        team.find { |person| person.username == mr_author_username }
      end

      # 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, timezone_experiment: false)
        spins = categories.sort.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 }

        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&.reviewer?(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 :engineering_productivity
            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
          end
        end

        spins
      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) }
      rescue => err
        warn("Reviewer roulette failed to load team data: #{err.message}")
        []
      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 == mr_author_username
      end

      def mr_author_username
        helper.gitlab_helper&.mr_author || `whoami`
      end

      def mr_source_branch
        return `git rev-parse --abbrev-ref HEAD` unless helper.gitlab_helper&.mr_json

        helper.gitlab_helper.mr_json['source_branch']
      end

      def mr_labels
        helper.gitlab_helper&.mr_labels || []
      end

      def new_random(seed)
        Random.new(Digest::MD5.hexdigest(seed).to_i(16))
      end

      def spin_role_for_category(team, role, project, category)
        team.select do |member|
          member.public_send("#{role}?", project, category, mr_labels) # rubocop:disable GitlabSecurity/PublicSend
        end
      end

      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(mr_source_branch)

        weighted_reviewers = Weightage::Reviewers.new(reviewers, traintainers).execute
        weighted_maintainers = 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
    end
  end
end