From e4b0901ab8b95d66ecce4edafb7c2f5d219e72b1 Mon Sep 17 00:00:00 2001 From: Tiger Watson <twatson@gitlab.com> Date: Thu, 25 Jan 2024 10:50:19 +0000 Subject: [PATCH] gitlab-housekeeper: Add helper for determining group and reviewer This will be used by Keeps to determine which group is responsible for a given feature category, which allows a suitable (backend) reviewer to be selected. --- .../lib/gitlab/housekeeper/change.rb | 37 ++++ .../lib/gitlab/housekeeper/git.rb | 10 +- .../lib/gitlab/housekeeper/gitlab_client.rb | 167 ++++++------------ .../lib/gitlab/housekeeper/runner.rb | 23 +-- .../spec/gitlab/housekeeper/change_spec.rb | 54 ++++++ .../spec/gitlab/housekeeper/git_spec.rb | 21 ++- .../gitlab/housekeeper/gitlab_client_spec.rb | 152 +++++++++++----- .../spec/gitlab/housekeeper/runner_spec.rb | 57 +++--- gems/gitlab-housekeeper/spec/spec_helper.rb | 24 +++ keeps/helpers/groups.rb | 37 ++++ .../overdue_finalize_background_migration.rb | 16 +- spec/keeps/helpers/groups_spec.rb | 60 +++++++ 12 files changed, 430 insertions(+), 228 deletions(-) create mode 100644 gems/gitlab-housekeeper/lib/gitlab/housekeeper/change.rb create mode 100644 gems/gitlab-housekeeper/spec/gitlab/housekeeper/change_spec.rb create mode 100644 keeps/helpers/groups.rb create mode 100644 spec/keeps/helpers/groups_spec.rb diff --git a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/change.rb b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/change.rb new file mode 100644 index 0000000000000..749e05317d397 --- /dev/null +++ b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/change.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Housekeeper + class Change + attr_accessor :identifiers, + :title, + :description, + :changed_files, + :labels, + :reviewers + + def mr_description + <<~MARKDOWN + #{description} + + This change was generated by + [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-housekeeper) + MARKDOWN + end + + def commit_message + <<~MARKDOWN + #{title} + + #{mr_description} + + Changelog: other + MARKDOWN + end + + def valid? + @identifiers && @title && @description && @changed_files + end + end + end +end diff --git a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/git.rb b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/git.rb index 329d32885eeef..b19b133c65d3b 100644 --- a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/git.rb +++ b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/git.rb @@ -44,15 +44,7 @@ def create_commit(branch_name, change) Shell.execute("git", "checkout", "-b", branch_name) Shell.execute("git", "add", *change.changed_files) - - commit_message = <<~MSG - #{change.title} - - #{change.description} - MSG - - Shell.execute("git", "commit", "-m", commit_message) - + Shell.execute("git", "commit", "-m", change.commit_message) ensure Shell.execute("git", "checkout", current_branch) end diff --git a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/gitlab_client.rb b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/gitlab_client.rb index 697ab9043ef80..91f1bcf894495 100644 --- a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/gitlab_client.rb +++ b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/gitlab_client.rb @@ -43,6 +43,9 @@ def non_housekeeper_changes( changes << :title if note['body'].start_with?("changed title from") changes << :description if note['body'] == "changed the description" changes << :code if note['body'].match?(/added \d+ commit/) + + changes << :reviewers if note['body'].include?('requested review from') + changes << :reviewers if note['body'].include?('removed review request for') end resource_label_events = get_merge_request_resource_label_events(target_project_id: target_project_id, iid: iid) @@ -61,17 +64,17 @@ def non_housekeeper_changes( changes.to_a end + # rubocop:disable Metrics/ParameterLists def create_or_update_merge_request( + change:, source_project_id:, - title:, - description:, - labels:, source_branch:, target_branch:, target_project_id:, update_title:, update_description:, - update_labels: + update_labels:, + update_reviewers: ) existing_iid = get_existing_merge_request( source_project_id: source_project_id, @@ -82,105 +85,48 @@ def create_or_update_merge_request( if existing_iid update_existing_merge_request( + change: change, existing_iid: existing_iid, - title: title, - description: description, - labels: labels, target_project_id: target_project_id, update_title: update_title, update_description: update_description, - update_labels: update_labels + update_labels: update_labels, + update_reviewers: update_reviewers ) else create_merge_request( + change: change, source_project_id: source_project_id, - title: title, - description: description, - labels: labels, source_branch: source_branch, target_branch: target_branch, target_project_id: target_project_id ) end end + # rubocop:enable Metrics/ParameterLists private def get_merge_request_notes(target_project_id:, iid:) - response = HTTParty.get( - "#{@base_uri}/projects/#{target_project_id}/merge_requests/#{iid}/notes", - query: { - per_page: 100 - }, - headers: { - "Private-Token" => @token - } - ) - - unless (200..299).cover?(response.code) - raise Error, - "Failed to get merge request notes with response code: #{response.code} and body:\n#{response.body}" - end - - JSON.parse(response.body) + request(:get, "/projects/#{target_project_id}/merge_requests/#{iid}/notes", query: { per_page: 100 }) end def get_merge_request_resource_label_events(target_project_id:, iid:) - response = HTTParty.get( - "#{@base_uri}/projects/#{target_project_id}/merge_requests/#{iid}/resource_label_events", - query: { - per_page: 100 - }, - headers: { - "Private-Token" => @token - } - ) - - unless (200..299).cover?(response.code) - raise Error, - "Failed to get merge request label events with response code: #{response.code} and body:\n#{response.body}" - end - - JSON.parse(response.body) + request(:get, "/projects/#{target_project_id}/merge_requests/#{iid}/resource_label_events", + query: { per_page: 100 }) end def current_user_id - @current_user_id = begin - response = HTTParty.get( - "#{@base_uri}/user", - headers: { - "Private-Token" => @token - } - ) - - unless (200..299).cover?(response.code) - raise Error, - "Failed with response code: #{response.code} and body:\n#{response.body}" - end - - data = JSON.parse(response.body) - data['id'] - end + @current_user_id ||= request(:get, "/user")['id'] end def get_existing_merge_request(source_project_id:, source_branch:, target_branch:, target_project_id:) - response = HTTParty.get("#{@base_uri}/projects/#{target_project_id}/merge_requests", - query: { - state: :opened, - source_branch: source_branch, - target_branch: target_branch, - source_project_id: source_project_id - }, - headers: { - 'Private-Token' => @token - }) - - unless (200..299).cover?(response.code) - raise Error, - "Failed with response code: #{response.code} and body:\n#{response.body}" - end - - data = JSON.parse(response.body) + data = request(:get, "/projects/#{target_project_id}/merge_requests", query: { + state: :opened, + source_branch: source_branch, + target_branch: target_branch, + source_project_id: source_project_id + }) return nil if data.empty? @@ -192,63 +138,64 @@ def get_existing_merge_request(source_project_id:, source_branch:, target_branch end def create_merge_request( + change:, source_project_id:, - title:, - description:, - labels:, source_branch:, target_branch:, target_project_id: ) - response = HTTParty.post("#{@base_uri}/projects/#{source_project_id}/merge_requests", body: { - title: title, - description: description, - labels: Array(labels).join(','), + request(:post, "/projects/#{source_project_id}/merge_requests", body: { + title: change.title, + description: change.mr_description, + labels: Array(change.labels).join(','), source_branch: source_branch, target_branch: target_branch, target_project_id: target_project_id, - remove_source_branch: true - }.to_json, - headers: { - 'Private-Token' => @token, - 'Content-Type' => 'application/json' - }) - - return if (200..299).cover?(response.code) - - raise Error, - "Failed with response code: #{response.code} and body:\n#{response.body}" + remove_source_branch: true, + reviewer_ids: usernames_to_ids(change.reviewers) + }) end def update_existing_merge_request( + change:, existing_iid:, - title:, - description:, - labels:, target_project_id:, update_title:, update_description:, - update_labels: + update_labels:, + update_reviewers: ) body = {} - body[:title] = title if update_title - body[:description] = description if update_description - body[:add_labels] = Array(labels).join(',') if update_labels + body[:title] = change.title if update_title + body[:description] = change.mr_description if update_description + body[:add_labels] = Array(change.labels).join(',') if update_labels + body[:reviewer_ids] = usernames_to_ids(change.reviewers) if update_reviewers return if body.empty? - response = HTTParty.put("#{@base_uri}/projects/#{target_project_id}/merge_requests/#{existing_iid}", - body: body.to_json, - headers: { - 'Private-Token' => @token, - 'Content-Type' => 'application/json' - }) + request(:put, "/projects/#{target_project_id}/merge_requests/#{existing_iid}", body: body) + end + + def usernames_to_ids(usernames) + usernames.map do |username| + data = request(:get, "/users", query: { username: username }) + data[0]['id'] + end + end + + def request(method, path, query: {}, body: {}) + response = HTTParty.public_send(method, "#{@base_uri}#{path}", query: query, body: body.to_json, headers: { # rubocop:disable GitlabSecurity/PublicSend + 'Private-Token' => @token, + 'Content-Type' => 'application/json' + }) - return if (200..299).cover?(response.code) + unless (200..299).cover?(response.code) + raise Error, + "Failed with response code: #{response.code} and body:\n#{response.body}" + end - raise Error, - "Failed with response code: #{response.code} and body:\n#{response.body}" + JSON.parse(response.body) end end end diff --git a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb index cebe2acfe5b6f..c86da8b588bf3 100644 --- a/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb +++ b/gems/gitlab-housekeeper/lib/gitlab/housekeeper/runner.rb @@ -4,13 +4,12 @@ require 'gitlab/housekeeper/keep' require 'gitlab/housekeeper/gitlab_client' require 'gitlab/housekeeper/git' +require 'gitlab/housekeeper/change' require 'awesome_print' require 'digest' module Gitlab module Housekeeper - Change = Struct.new(:identifiers, :title, :description, :changed_files, :labels) - class Runner def initialize(max_mrs: 1, dry_run: false, require: [], keeps: nil) @max_mrs = max_mrs @@ -32,6 +31,11 @@ def run @keeps.each do |keep_class| keep = keep_class.new keep.each_change do |change| + unless change.valid? + puts "Ignoring invalid change from: #{keep_class}" + next + end + branch_name = git.commit_in_branch(change) add_standard_change_data(change) @@ -54,14 +58,6 @@ def run def add_standard_change_data(change) change.labels ||= [] change.labels << 'automation:gitlab-housekeeper-authored' - - change.description += <<~MARKDOWN - - This change was generated by - [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-housekeeper) - - Changelog: other - MARKDOWN end def git @@ -112,16 +108,15 @@ def create(change, branch_name) end gitlab_client.create_or_update_merge_request( + change: change, source_project_id: housekeeper_fork_project_id, - title: change.title, - description: change.description, - labels: change.labels, source_branch: branch_name, target_branch: 'master', target_project_id: housekeeper_target_project_id, update_title: !non_housekeeper_changes.include?(:title), update_description: !non_housekeeper_changes.include?(:description), - update_labels: !non_housekeeper_changes.include?(:labels) + update_labels: !non_housekeeper_changes.include?(:labels), + update_reviewers: !non_housekeeper_changes.include?(:reviewers) ) end diff --git a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/change_spec.rb b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/change_spec.rb new file mode 100644 index 0000000000000..8060507210085 --- /dev/null +++ b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/change_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Housekeeper::Change do + let(:change) { described_class.new } + + before do + change.title = 'The title' + change.description = 'The description' + end + + describe '#mr_description' do + it 'includes standard content' do + expect(change.mr_description).to eq( + <<~MARKDOWN + The description + + This change was generated by + [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-housekeeper) + MARKDOWN + ) + end + end + + describe '#commit_message' do + it 'includes standard content' do + expect(change.commit_message).to eq( + <<~MARKDOWN + The title + + The description + + This change was generated by + [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-housekeeper) + + + Changelog: other + MARKDOWN + ) + end + end + + describe '#valid?' do + it 'is not valid if missing required attributes' do + [:identifiers, :title, :description, :changed_files].each do |attribute| + change = create_change + expect(change).to be_valid + change.public_send("#{attribute}=", nil) + expect(change).not_to be_valid + end + end + end +end diff --git a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/git_spec.rb b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/git_spec.rb index 485c46f7ad0f3..81c4342b95fe7 100644 --- a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/git_spec.rb +++ b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/git_spec.rb @@ -50,27 +50,21 @@ def setup_and_checkout_another_branch let(:test_file2) { 'files/test_file2.txt' } it 'commits the given change details to the given branch name' do - title = "The commit title" - description = <<~COMMIT + change = ::Gitlab::Housekeeper::Change.new + change.title = "The commit title" + change.description = <<~COMMIT The commit description can be split over multiple lines! COMMIT - identifiers = %w[GitlabHousekeeper TestBranch] + change.identifiers = %w[GitlabHousekeeper TestBranch] Dir.mkdir('files') File.write(test_file1, "Content in file 1!") File.write(test_file2, "Other content in file 2!") File.write(file_not_to_commit, 'Do not commit!') - changed_files = [test_file1, test_file2] - - change = ::Gitlab::Housekeeper::Change.new( - identifiers, - title, - description, - changed_files - ) + change.changed_files = [test_file1, test_file2] branch_name = nil git.with_branch_from_branch do @@ -91,6 +85,11 @@ def setup_and_checkout_another_branch The commit description can be split over multiple lines! + This change was generated by + [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/tree/master/gems/gitlab-housekeeper) + + Changelog: other + diff --git a/files/test_file2.txt b/files/test_file2.txt new file mode 100644 diff --git a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/gitlab_client_spec.rb b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/gitlab_client_spec.rb index 2a1d2cb79fb99..c52a98058e3a6 100644 --- a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/gitlab_client_spec.rb +++ b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/gitlab_client_spec.rb @@ -23,6 +23,24 @@ } end + let(:added_reviewer_note) do + { + id: 1698248524, + body: "requested review from @bob", + author: { "id" => 1234 }, + system: true + } + end + + let(:removed_reviewer_note) do + { + id: 1698248524, + body: "removed review request for @bob", + author: { "id" => 1234 }, + system: true + } + end + let(:irrelevant_note1) do { id: 1698248523, @@ -138,7 +156,7 @@ context 'when all important things change' do let(:notes) do - [not_a_system_note, updated_title_note, updated_description_note, added_commit_note] + [not_a_system_note, updated_title_note, updated_description_note, added_commit_note, added_reviewer_note] end let(:resource_label_events) do @@ -146,10 +164,7 @@ end it 'returns :title, :description, :code, :labels' do - expect(non_housekeeper_changes).to include(:title) - expect(non_housekeeper_changes).to include(:description) - expect(non_housekeeper_changes).to include(:code) - expect(non_housekeeper_changes).to include(:labels) + expect(non_housekeeper_changes).to match_array([:title, :description, :code, :labels, :reviewers]) end end @@ -159,10 +174,7 @@ end it 'returns :title' do - expect(non_housekeeper_changes).to include(:title) - expect(non_housekeeper_changes).not_to include(:description) - expect(non_housekeeper_changes).not_to include(:code) - expect(non_housekeeper_changes).not_to include(:labels) + expect(non_housekeeper_changes).to match_array([:title]) end end @@ -172,10 +184,7 @@ end it 'returns :description' do - expect(non_housekeeper_changes).not_to include(:title) - expect(non_housekeeper_changes).to include(:description) - expect(non_housekeeper_changes).not_to include(:code) - expect(non_housekeeper_changes).not_to include(:labels) + expect(non_housekeeper_changes).to match_array([:description]) end end @@ -189,10 +198,27 @@ end it 'returns :labels' do - expect(non_housekeeper_changes).not_to include(:title) - expect(non_housekeeper_changes).not_to include(:description) - expect(non_housekeeper_changes).not_to include(:code) - expect(non_housekeeper_changes).to include(:labels) + expect(non_housekeeper_changes).to match_array([:labels]) + end + end + + context 'when reviewers are added' do + let(:notes) do + [not_a_system_note, added_reviewer_note] + end + + it 'returns :reviewers' do + expect(non_housekeeper_changes).to match_array([:reviewers]) + end + end + + context 'when reviewers are removed' do + let(:notes) do + [not_a_system_note, removed_reviewer_note] + end + + it 'returns :reviewers' do + expect(non_housekeeper_changes).to match_array([:reviewers]) end end @@ -204,24 +230,39 @@ end describe '#create_or_update_merge_request' do + let(:reviewer_id) { 999 } + + let(:change) do + create_change + end + let(:params) do { + change: change, source_project_id: 123, - title: 'A new merge request!', - labels: %w[label-1 label-2], - description: 'This merge request is pretty good.', source_branch: 'the-source-branch', target_branch: 'the-target-branch', target_project_id: 456, update_title: true, update_description: true, - update_labels: true + update_labels: true, + update_reviewers: true } end let(:existing_mrs) { [] } before do + # Stub the user id of the reviewers + stub_request(:get, "https://gitlab.com/api/v4/users") + .with( + query: { username: 'thegitlabreviewer' }, + headers: { + 'Private-Token' => 'the-api-token' + } + ) + .to_return(status: 200, body: [{ id: reviewer_id }].to_json) + # Stub the check to see if the merge request already exists stub_request(:get, "https://gitlab.com/api/v4/projects/456/merge_requests?state=opened&source_branch=the-source-branch&target_branch=the-target-branch&source_project_id=123") .with( @@ -236,19 +277,20 @@ stub = stub_request(:post, "https://gitlab.com/api/v4/projects/123/merge_requests") .with( body: { - title: "A new merge request!", - description: "This merge request is pretty good.", - labels: "label-1,label-2", + title: "The change title", + description: change.mr_description, + labels: "some-label-1,some-label-2", source_branch: "the-source-branch", target_branch: "the-target-branch", target_project_id: 456, - remove_source_branch: true + remove_source_branch: true, + reviewer_ids: [reviewer_id] }, headers: { 'Content-Type' => 'application/json', 'Private-Token' => 'the-api-token' }) - .to_return(status: 200, body: "") + .to_return(status: 200, body: '{}') client.create_or_update_merge_request(**params) @@ -264,15 +306,16 @@ stub = stub_request(:put, "https://gitlab.com/api/v4/projects/456/merge_requests/1234") .with( body: { - title: "A new merge request!", - description: "This merge request is pretty good.", - add_labels: "label-1,label-2" + title: "The change title", + description: change.mr_description, + add_labels: "some-label-1,some-label-2", + reviewer_ids: [reviewer_id] }.to_json, headers: { 'Content-Type' => 'application/json', 'Private-Token' => 'the-api-token' }) - .to_return(status: 200, body: "") + .to_return(status: 200, body: '{}') client.create_or_update_merge_request(**params) expect(stub).to have_been_requested @@ -293,14 +336,15 @@ stub = stub_request(:put, "https://gitlab.com/api/v4/projects/456/merge_requests/1234") .with( body: { - description: "This merge request is pretty good.", - add_labels: "label-1,label-2" + description: change.mr_description, + add_labels: "some-label-1,some-label-2", + reviewer_ids: [reviewer_id] }.to_json, headers: { 'Content-Type' => 'application/json', 'Private-Token' => 'the-api-token' } - ).to_return(status: 200, body: "") + ).to_return(status: 200, body: '{}') client.create_or_update_merge_request(**params.merge(update_title: false)) expect(stub).to have_been_requested @@ -312,14 +356,15 @@ stub = stub_request(:put, "https://gitlab.com/api/v4/projects/456/merge_requests/1234") .with( body: { - title: "A new merge request!", - add_labels: "label-1,label-2" + title: "The change title", + add_labels: "some-label-1,some-label-2", + reviewer_ids: [reviewer_id] }.to_json, headers: { 'Content-Type' => 'application/json', 'Private-Token' => 'the-api-token' } - ).to_return(status: 200, body: "") + ).to_return(status: 200, body: '{}') client.create_or_update_merge_request(**params.merge(update_description: false)) expect(stub).to have_been_requested @@ -331,24 +376,49 @@ stub = stub_request(:put, "https://gitlab.com/api/v4/projects/456/merge_requests/1234") .with( body: { - title: "A new merge request!", - description: "This merge request is pretty good." + title: "The change title", + description: change.mr_description, + reviewer_ids: [reviewer_id] }.to_json, headers: { 'Content-Type' => 'application/json', 'Private-Token' => 'the-api-token' } - ).to_return(status: 200, body: "") + ).to_return(status: 200, body: '{}') client.create_or_update_merge_request(**params.merge(update_labels: false)) expect(stub).to have_been_requested end end + context 'when update_reviewers: false' do + it 'does not update the reviewers' do + stub = stub_request(:put, "https://gitlab.com/api/v4/projects/456/merge_requests/1234") + .with( + body: { + title: "The change title", + description: change.mr_description, + add_labels: "some-label-1,some-label-2" + }.to_json, + headers: { + 'Content-Type' => 'application/json', + 'Private-Token' => 'the-api-token' + } + ).to_return(status: 200, body: '{}') + + client.create_or_update_merge_request(**params.merge(update_reviewers: false)) + expect(stub).to have_been_requested + end + end + context 'when there is nothing to update' do it 'does not make a request' do - client.create_or_update_merge_request(**params.merge(update_description: false, update_title: false, - update_labels: false)) + client.create_or_update_merge_request(**params.merge( + update_description: false, + update_title: false, + update_labels: false, + update_reviewers: false + )) end end end diff --git a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/runner_spec.rb b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/runner_spec.rb index 51dcc3b763f96..a967d74db90a6 100644 --- a/gems/gitlab-housekeeper/spec/gitlab/housekeeper/runner_spec.rb +++ b/gems/gitlab-housekeeper/spec/gitlab/housekeeper/runner_spec.rb @@ -8,32 +8,23 @@ let(:fake_keep) { instance_double(Class) } let(:change1) do - ::Gitlab::Housekeeper::Change.new( - %w[the identifier for the first change], - "The title of MR1", - "The description of the MR", - ['change1.txt', 'change2.txt'], - ['example-label'] + create_change( + identifiers: %w[the identifier for the first change], + title: "The title of MR1" ) end let(:change2) do - ::Gitlab::Housekeeper::Change.new( - %w[the identifier for the second change], - "The title of MR2", - "The description of the MR", - ['change1.txt', 'change2.txt'], - ['example-label'] + create_change( + identifiers: %w[the identifier for the second change], + title: "The title of MR2" ) end let(:change3) do - ::Gitlab::Housekeeper::Change.new( - %w[the identifier for the third change], - "The title of MR3", - "The description of the MR", - ['change1.txt', 'change2.txt'], - ['example-label'] + create_change( + identifiers: %w[the identifier for the third change], + title: "The title of MR3" ) end @@ -98,29 +89,27 @@ # Merge requests get created expect(gitlab_client).to receive(:create_or_update_merge_request) .with( + change: change1, source_project_id: '123', - title: 'The title of MR1', - description: /The description of the MR/, - labels: %w[example-label automation:gitlab-housekeeper-authored], source_branch: 'the-identifier-for-the-first-change', target_branch: 'master', target_project_id: '456', update_title: true, update_description: true, - update_labels: true + update_labels: true, + update_reviewers: true ) expect(gitlab_client).to receive(:create_or_update_merge_request) .with( + change: change2, source_project_id: '123', - title: 'The title of MR2', - description: /The description of the MR/, - labels: %w[example-label automation:gitlab-housekeeper-authored], source_branch: 'the-identifier-for-the-second-change', target_branch: 'master', target_project_id: '456', update_title: true, update_description: true, - update_labels: true + update_labels: true, + update_reviewers: true ) described_class.new(max_mrs: 2, keeps: [fake_keep]).run @@ -135,7 +124,7 @@ source_branch: 'the-identifier-for-the-first-change', target_branch: 'master', target_project_id: '456' - ).and_return([:code, :description]) + ).and_return([:code, :description, :reviewers]) # Second change has updated title and description so it should push the code expect(gitlab_client).to receive(:non_housekeeper_changes) @@ -155,29 +144,27 @@ expect(gitlab_client).to receive(:create_or_update_merge_request) .with( + change: change1, source_project_id: '123', - title: 'The title of MR1', - description: /The description of the MR/, - labels: %w[example-label automation:gitlab-housekeeper-authored], source_branch: 'the-identifier-for-the-first-change', target_branch: 'master', target_project_id: '456', update_title: true, update_description: false, - update_labels: true + update_labels: true, + update_reviewers: false ) expect(gitlab_client).to receive(:create_or_update_merge_request) .with( + change: change2, source_project_id: '123', - title: 'The title of MR2', - description: /The description of the MR/, - labels: %w[example-label automation:gitlab-housekeeper-authored], source_branch: 'the-identifier-for-the-second-change', target_branch: 'master', target_project_id: '456', update_title: false, update_description: false, - update_labels: true + update_labels: true, + update_reviewers: true ) described_class.new(max_mrs: 2, keeps: [fake_keep]).run diff --git a/gems/gitlab-housekeeper/spec/spec_helper.rb b/gems/gitlab-housekeeper/spec/spec_helper.rb index f2ebb2298bd93..890a4675f3ed0 100644 --- a/gems/gitlab-housekeeper/spec/spec_helper.rb +++ b/gems/gitlab-housekeeper/spec/spec_helper.rb @@ -6,6 +6,28 @@ require 'webmock/rspec' require 'gitlab/rspec/all' +module HousekeeperFactory + def create_change( + identifiers: %w[the identifier], + title: 'The change title', + description: 'The change description', + changed_files: ['change1.txt', 'change2.txt'], + labels: %w[some-label-1 some-label-2], + reviewers: ['thegitlabreviewer'] + ) + + change = ::Gitlab::Housekeeper::Change.new + change.identifiers = identifiers + change.title = title + change.description = description + change.changed_files = changed_files + change.labels = labels + change.reviewers = reviewers + + change + end +end + RSpec.configure do |config| config.include StubENV # Enable flags like --only-failures and --next-failure @@ -17,4 +39,6 @@ config.expect_with :rspec do |c| c.syntax = :expect end + + config.include(HousekeeperFactory) end diff --git a/keeps/helpers/groups.rb b/keeps/helpers/groups.rb new file mode 100644 index 0000000000000..c66597fabdf8c --- /dev/null +++ b/keeps/helpers/groups.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Keeps + module Helpers + class Groups + Error = Class.new(StandardError) + + def group_for_feature_category(category) + @groups ||= {} + @groups[category] ||= fetch_groups.find do |_, group| + group['categories'].present? && group['categories'].include?(category) + end&.last + end + + def pick_reviewer(group, identifiers) + random_engineer = Digest::SHA256.hexdigest(identifiers.join).to_i(16) % group['backend_engineers'].size + + group['backend_engineers'][random_engineer] + end + + private + + def fetch_groups + @groups_json ||= begin + response = Gitlab::HTTP.get('https://about.gitlab.com/groups.json') + + unless (200..299).cover?(response.code) + raise Error, + "Failed to get group information with response code: #{response.code} and body:\n#{response.body}" + end + + Gitlab::Json.parse(response.body) + end + end + end + end +end diff --git a/keeps/overdue_finalize_background_migration.rb b/keeps/overdue_finalize_background_migration.rb index 035db201b4857..ee937f08a27e7 100644 --- a/keeps/overdue_finalize_background_migration.rb +++ b/keeps/overdue_finalize_background_migration.rb @@ -40,15 +40,16 @@ def each_change next unless migration_record # Finalize the migration - title = "Finalize migration #{job_name}" + change = ::Gitlab::Housekeeper::Change.new + change.title = "Finalize migration #{job_name}" - identifiers = [self.class.name.demodulize, job_name] + change.identifiers = [self.class.name.demodulize, job_name] last_migration_file = last_migration_for_job(job_name) next unless last_migration_file # rubocop:disable Gitlab/DocUrl -- Not running inside rails application - description = <<~MARKDOWN + change.description = <<~MARKDOWN This migration was finished at `#{migration_record.finished_at || migration_record.updated_at}`, you can confirm the status using our [batched background migration chatops commands](https://docs.gitlab.com/ee/development/database/batched_background_migrations.html#monitor-the-progress-and-status-of-a-batched-background-migration). @@ -78,7 +79,7 @@ def each_change .source_root('generator_templates/post_deployment_migration/post_deployment_migration/') generator = ::PostDeploymentMigration::PostDeploymentMigrationGenerator.new([migration_name], force: true) migration_file = generator.invoke_all.first - changed_files = [migration_file] + change.changed_files = [migration_file] add_ensure_call_to_migration(migration_file, queue_method_node, job_name) ::Gitlab::Housekeeper::Shell.execute('rubocop', '-a', migration_file) @@ -89,11 +90,10 @@ def each_change add_finalized_by_to_yaml(migration_yaml_file, generator.migration_number) - changed_files << digest_file - changed_files << migration_yaml_file + change.changed_files << digest_file + change.changed_files << migration_yaml_file - to_create = ::Gitlab::Housekeeper::Change.new(identifiers, title, description, changed_files) - yield(to_create) + yield(change) end end diff --git a/spec/keeps/helpers/groups_spec.rb b/spec/keeps/helpers/groups_spec.rb new file mode 100644 index 0000000000000..c3b9afdeef02a --- /dev/null +++ b/spec/keeps/helpers/groups_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' +require './keeps/helpers/groups' + +RSpec.describe Keeps::Helpers::Groups, feature_category: :tooling do + let(:groups) do + { + 'tenant_scale' => { + 'name' => 'Tenant Scale', + 'section' => 'core_platform', + 'stage' => 'data_stores', + 'categories' => %w[cell groups_and_projects user_profile organization], + 'label' => 'group::tenant scale', + 'extra_labels' => [], + 'slack_channel' => 'g_tenant_scale', + 'backend_engineers' => %w[be1 be2 be3 be4 be5], + 'triage_ops_config' => nil + } + } + end + + before do + stub_request(:get, "https://about.gitlab.com/groups.json").to_return(status: 200, body: groups.to_json) + end + + describe '#group_for_feature_category' do + let(:category) { 'organization' } + + subject(:group) { described_class.new.group_for_feature_category(category) } + + it { is_expected.to eq(groups['tenant_scale']) } + + context 'when the category does not exist' do + let(:category) { 'missing-category' } + + it { is_expected.to eq(nil) } + end + + context 'when the request to fetch groups fails' do + before do + stub_request(:get, "https://about.gitlab.com/groups.json").to_return(status: 404, body: '') + end + + it 'raises an error' do + expect { group }.to raise_error(described_class::Error) + end + end + end + + describe '#pick_reviewer' do + let(:group) { groups['tenant_scale'] } + let(:identifiers) { %w[example identifier] } + let(:expected_index) { Digest::SHA256.hexdigest(identifiers.join).to_i(16) % group['backend_engineers'].size } + + subject { described_class.new.pick_reviewer(group, identifiers) } + + it { is_expected.to eq(group['backend_engineers'][expected_index]) } + end +end -- GitLab