diff options
-rw-r--r-- | Dangerfile | 1 | ||||
-rw-r--r-- | danger/plugins/helper.rb | 3 | ||||
-rw-r--r-- | danger/plugins/roulette.rb | 10 | ||||
-rw-r--r-- | danger/roulette/Dangerfile | 45 | ||||
-rw-r--r-- | lib/gitlab/danger/helper.rb | 29 | ||||
-rw-r--r-- | lib/gitlab/danger/roulette.rb | 84 | ||||
-rw-r--r-- | spec/lib/gitlab/danger/helper_spec.rb | 97 | ||||
-rw-r--r-- | spec/lib/gitlab/danger/roulette_spec.rb | 101 | ||||
-rw-r--r-- | spec/lib/gitlab/danger/teammate_spec.rb | 16 |
9 files changed, 212 insertions, 174 deletions
diff --git a/Dangerfile b/Dangerfile index 9e3a08949b0..d0a605f8d8e 100644 --- a/Dangerfile +++ b/Dangerfile @@ -1,5 +1,6 @@ # frozen_string_literal: true danger.import_plugin('danger/plugins/helper.rb') +danger.import_plugin('danger/plugins/roulette.rb') unless helper.release_automation? danger.import_dangerfile(path: 'danger/metadata') diff --git a/danger/plugins/helper.rb b/danger/plugins/helper.rb index 581c0720083..2d7a933e801 100644 --- a/danger/plugins/helper.rb +++ b/danger/plugins/helper.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -require 'net/http' -require 'yaml' - require_relative '../../lib/gitlab/danger/helper' module Danger diff --git a/danger/plugins/roulette.rb b/danger/plugins/roulette.rb new file mode 100644 index 00000000000..7c62cff0c92 --- /dev/null +++ b/danger/plugins/roulette.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../../lib/gitlab/danger/roulette' + +module Danger + class Roulette < Plugin + # Put the helper code somewhere it can be tested + include Gitlab::Danger::Roulette + end +end diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile index 62e5526c02b..de314c5b39f 100644 --- a/danger/roulette/Dangerfile +++ b/danger/roulette/Dangerfile @@ -32,7 +32,7 @@ for them. MARKDOWN def spin_for_category(team, project, category, branch_name) - rng = Random.new(Digest::MD5.hexdigest(branch_name).to_i(16)) + random = roulette.new_random(branch_name) reviewers = team.select { |member| member.reviewer?(project, category) } traintainers = team.select { |member| member.traintainer?(project, category) } @@ -42,43 +42,12 @@ def spin_for_category(team, project, category, branch_name) # https://gitlab.com/gitlab-org/gitlab-ce/issues/57653 # Make traintainers have triple the chance to be picked as a reviewer - reviewer = spin_for_person(reviewers + traintainers + traintainers, random: rng) - maintainer = spin_for_person(maintainers, random: rng) + reviewer = roulette.spin_for_person(reviewers + traintainers + traintainers, random: random) + maintainer = roulette.spin_for_person(maintainers, random: random) "| #{helper.label_for_category(category)} | #{reviewer&.markdown_name} | #{maintainer&.markdown_name} |" end -# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the -# selection will change on next spin -def spin_for_person(people, random:) - person = nil - people = people.dup - - people.size.times do - person = people.sample(random: random) - - break person unless out_of_office?(person) - - people -= [person] - end - - person -end - -def out_of_office?(person) - username = CGI.escape(person.username) - api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status" - response = HTTParty.get(api_endpoint) # rubocop:disable Gitlab/HTTParty - - if response.code == 200 - response["message"]&.match(/OOO/i) - else - false # this is no worse than not checking for OOO - end -rescue - false -end - def build_list(items) list = items.map { |filename| "* `#{filename}`" }.join("\n") @@ -101,14 +70,12 @@ categories = changes.keys - [:unknown] # disable the review roulette for such MRs. if changes.any? && !gitlab.mr_labels.include?('single codebase') && !gitlab.mr_labels.include?('CSS cleanup') # Strip leading and trailing CE/EE markers - canonical_branch_name = gitlab - .mr_json['source_branch'] - .gsub(/^[ce]e-/, '') - .gsub(/-[ce]e$/, '') + canonical_branch_name = + roulette.canonical_branch_name(gitlab.mr_json['source_branch']) team = begin - helper.project_team + roulette.project_team(helper.project_name) rescue => err warn("Reviewer roulette failed to load team data: #{err.message}") [] diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index f0ca397609d..7effb802678 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -1,6 +1,4 @@ # frozen_string_literal: true -require 'net/http' -require 'json' require_relative 'teammate' @@ -8,7 +6,6 @@ module Gitlab module Danger module Helper RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot' - ROULETTE_DATA_URL = URI.parse('https://about.gitlab.com/roulette.json').freeze # Returns a list of all files that have been added, modified or renamed. # `git.modified_files` might contain paths that already have been renamed, @@ -49,32 +46,6 @@ module Gitlab ee? ? 'gitlab-ee' : 'gitlab-ce' end - # Looks up the current list of GitLab team members and parses it into a - # useful form - # - # @return [Array<Teammate>] - def team - @team ||= - begin - rsp = Net::HTTP.get_response(ROULETTE_DATA_URL) - raise "Failed to read #{ROULETTE_DATA_URL}: #{rsp.code} #{rsp.message}" unless - rsp.is_a?(Net::HTTPSuccess) - - data = JSON.parse(rsp.body) - 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 - team.select { |member| member.in_project?(project_name) } - end - # @return [Hash<String,Array<String>>] def changes_by_category all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash| diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb new file mode 100644 index 00000000000..062eda38ee4 --- /dev/null +++ b/lib/gitlab/danger/roulette.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'net/http' +require 'json' +require 'cgi' + +require_relative 'teammate' + +module Gitlab + module Danger + module Roulette + ROULETTE_DATA_URL = 'https://about.gitlab.com/roulette.json' + HTTPError = Class.new(RuntimeError) + + # Looks up the current list of GitLab team members and parses it into a + # useful form + # + # @return [Array<Teammate>] + def team + @team ||= + begin + data = 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 + def spin_for_person(people, random:) + person = nil + people = people.dup + + people.size.times do + person = people.sample(random: random) + + break person unless out_of_office?(person) + + people -= [person] + end + + person + end + + private + + def out_of_office?(person) + username = CGI.escape(person.username) + api_endpoint = "https://gitlab.com/api/v4/users/#{username}/status" + response = http_get_json(api_endpoint) + response["message"]&.match?(/OOO/i) + rescue HTTPError, JSON::ParserError + false # this is no worse than not checking for OOO + end + + def http_get_json(url) + rsp = Net::HTTP.get_response(URI.parse(url)) + + unless rsp.is_a?(Net::HTTPSuccess) + raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}" + end + + JSON.parse(rsp.body) + end + end + end +end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 32b90041c64..f7642182a17 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -2,7 +2,6 @@ require 'fast_spec_helper' require 'rspec-parameterized' -require 'webmock/rspec' require 'gitlab/danger/helper' @@ -19,39 +18,6 @@ describe Gitlab::Danger::Helper do end end - let(:teammate_json) do - <<~JSON - [ - { - "username": "in-gitlab-ce", - "name": "CE maintainer", - "projects":{ "gitlab-ce": "maintainer backend" } - }, - { - "username": "in-gitlab-ee", - "name": "EE reviewer", - "projects":{ "gitlab-ee": "reviewer frontend" } - } - ] - JSON - end - - let(:ce_teammate_matcher) do - satisfy do |teammate| - teammate.username == 'in-gitlab-ce' && - teammate.name == 'CE maintainer' && - teammate.projects == { 'gitlab-ce' => 'maintainer backend' } - end - end - - let(:ee_teammate_matcher) do - satisfy do |teammate| - teammate.username == 'in-gitlab-ee' && - teammate.name == 'EE reviewer' && - teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } - end - end - let(:fake_git) { double('fake-git') } subject(:helper) { FakeDanger.new(git: fake_git) } @@ -119,69 +85,6 @@ describe Gitlab::Danger::Helper do end end - describe '#team' do - subject(:team) { helper.team } - - context 'HTTP failure' do - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .to_return(status: 404) - end - - it 'raises a pretty error' do - expect { team }.to raise_error(/Failed to read/) - end - end - - context 'JSON failure' do - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .to_return(body: 'INVALID JSON') - end - - it 'raises a pretty error' do - expect { team }.to raise_error(/Failed to parse/) - end - end - - context 'success' do - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .to_return(body: teammate_json) - end - - it 'returns an array of teammates' do - is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) - end - - it 'memoizes the result' do - expect(team.object_id).to eq(helper.team.object_id) - end - end - end - - describe '#project_team' do - subject { helper.project_team } - - before do - WebMock - .stub_request(:get, 'https://about.gitlab.com/roulette.json') - .to_return(body: teammate_json) - end - - it 'filters team by project_name' do - expect(helper) - .to receive(:project_name) - .at_least(:once) - .and_return('gitlab-ce') - - is_expected.to contain_exactly(ce_teammate_matcher) - end - end - describe '#changes_by_category' do it 'categorizes changed files' do expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] } diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb new file mode 100644 index 00000000000..40dce0c5378 --- /dev/null +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'webmock/rspec' + +require 'gitlab/danger/roulette' + +describe Gitlab::Danger::Roulette do + let(:teammate_json) do + <<~JSON + [ + { + "username": "in-gitlab-ce", + "name": "CE maintainer", + "projects":{ "gitlab-ce": "maintainer backend" } + }, + { + "username": "in-gitlab-ee", + "name": "EE reviewer", + "projects":{ "gitlab-ee": "reviewer frontend" } + } + ] + JSON + end + + let(:ce_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ce' && + teammate.name == 'CE maintainer' && + teammate.projects == { 'gitlab-ce' => 'maintainer backend' } + end + end + + let(:ee_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ee' && + teammate.name == 'EE reviewer' && + teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } + end + end + + subject(:roulette) { Object.new.extend(described_class) } + + describe '#team' do + subject(:team) { roulette.team } + + context 'HTTP failure' do + before do + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(status: 404) + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to read/) + end + end + + context 'JSON failure' do + before do + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(body: 'INVALID JSON') + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to parse/) + end + end + + context 'success' do + before do + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(body: teammate_json) + end + + it 'returns an array of teammates' do + is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) + end + + it 'memoizes the result' do + expect(team.object_id).to eq(roulette.team.object_id) + end + end + end + + describe '#project_team' do + subject { roulette.project_team('gitlab-ce') } + + before do + WebMock + .stub_request(:get, described_class::ROULETTE_DATA_URL) + .to_return(body: teammate_json) + end + + it 'filters team by project_name' do + is_expected.to contain_exactly(ce_teammate_matcher) + end + end +end diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb index 4bc0a4c1398..753c74ff814 100644 --- a/spec/lib/gitlab/danger/teammate_spec.rb +++ b/spec/lib/gitlab/danger/teammate_spec.rb @@ -1,5 +1,9 @@ # frozen_string_literal: true +require 'fast_spec_helper' + +require 'gitlab/danger/teammate' + describe Gitlab::Danger::Teammate do subject { described_class.new({ 'projects' => projects }) } let(:projects) { { project => capabilities } } @@ -9,15 +13,15 @@ describe Gitlab::Danger::Teammate do let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer database'] } it '#reviewer? supports multiple roles per project' do - expect(subject.reviewer?(project, 'backend')).to be_truthy + expect(subject.reviewer?(project, :backend)).to be_truthy end it '#traintainer? supports multiple roles per project' do - expect(subject.traintainer?(project, 'database')).to be_truthy + expect(subject.traintainer?(project, :database)).to be_truthy end it '#maintainer? supports multiple roles per project' do - expect(subject.maintainer?(project, 'frontend')).to be_truthy + expect(subject.maintainer?(project, :frontend)).to be_truthy end end @@ -25,15 +29,15 @@ describe Gitlab::Danger::Teammate do let(:capabilities) { 'reviewer backend' } it '#reviewer? supports one role per project' do - expect(subject.reviewer?(project, 'backend')).to be_truthy + expect(subject.reviewer?(project, :backend)).to be_truthy end it '#traintainer? supports one role per project' do - expect(subject.traintainer?(project, 'database')).to be_falsey + expect(subject.traintainer?(project, :database)).to be_falsey end it '#maintainer? supports one role per project' do - expect(subject.maintainer?(project, 'frontend')).to be_falsey + expect(subject.maintainer?(project, :frontend)).to be_falsey end end end |