diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 18c674b453bb428491e4d671f8f63a1a6adcfa64..92a539fe72c0f6c2d094b486a55ebe3e7b6fc326 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1230,8 +1230,18 @@ def draft_title alias_method :wip_title, :draft_title def skipped_mergeable_checks(options = {}) + merge_when_checks_pass_strat = options[:auto_merge_strategy] == ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS + + skip_additional_checks = merge_when_checks_pass_strat && + ::Feature.enabled?(:additional_merge_when_checks_ready, project) + { - skip_ci_check: options.fetch(:auto_merge_requested, false) + skip_ci_check: options.fetch(:auto_merge_requested, false), + skip_approved_check: merge_when_checks_pass_strat, + skip_draft_check: skip_additional_checks, + skip_blocked_check: skip_additional_checks, + skip_discussions_check: skip_additional_checks, + skip_external_status_check: skip_additional_checks } end @@ -1280,31 +1290,28 @@ def self.all_mergeability_checks # skip_approved_check # skip_blocked_check # skip_external_status_check - def mergeable_state?(**mergeable_state_check_params) - additional_checks = execute_merge_checks( - self.class.mergeable_state_checks, - params: mergeable_state_check_params - ) - additional_checks.success? + def mergeable_state?(**params) + results = check_mergeability_states(checks: self.class.mergeable_state_checks, **params) + + results.success? end - def mergeable_git_state?(skip_rebase_check: false) - checks = execute_merge_checks( - self.class.mergeable_git_state_checks, - params: { - skip_rebase_check: skip_rebase_check - } - ) + # This runs only git related checks + def mergeable_git_state?(**params) + results = check_mergeability_states(checks: self.class.mergeable_git_state_checks, **params) + + results.success? + end + + # This runs all the checks + def mergeability_checks_pass?(**params) + results = check_mergeability_states(checks: self.class.all_mergeability_checks, **params) - checks.success? + results.success? end def all_mergeability_checks_results - execute_merge_checks( - self.class.all_mergeability_checks, - params: {}, - execute_all: true - ).payload[:results] + check_mergeability_states(checks: self.class.all_mergeability_checks, execute_all: true).payload[:results] end def ff_merge_possible? @@ -2236,6 +2243,14 @@ def auto_merge_when_incomplete_pipeline_succeeds_enabled? ) end + def check_mergeability_states(checks:, execute_all: false, **params) + execute_merge_checks( + checks, + params: params, + execute_all: execute_all + ) + end + def merge_base_pipelines return ::Ci::Pipeline.none unless diff_head_pipeline&.target_sha diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index 9d2d89e56650e81c6773fb412e51fb9884eb76b6..61d2ab6e70a8406039b092f119a81702dfcca227 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -58,16 +58,29 @@ def abort(merge_request, reason) def available_for?(merge_request) strong_memoize("available_for_#{merge_request.id}") do - merge_request.can_be_merged_by?(current_user) && - merge_request.open? && - !merge_request.broken? && - overrideable_available_for_checks(merge_request) && - yield + if Feature.enabled?(:refactor_auto_merge, merge_request.project, type: :gitlab_com_derisk) + merge_request.can_be_merged_by?(current_user) && + merge_request.mergeability_checks_pass?(**skippable_available_for_checks(merge_request)) && + yield + else + merge_request.can_be_merged_by?(current_user) && + merge_request.open? && + !merge_request.broken? && + overrideable_available_for_checks(merge_request) && + yield + end end end private + def skippable_available_for_checks(merge_request) + merge_request.skipped_mergeable_checks( + auto_merge_requested: true, + auto_merge_strategy: strategy + ) + end + def overrideable_available_for_checks(merge_request) !merge_request.draft? && merge_request.mergeable_discussions_state? && diff --git a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb index db67a1a9f26982146b5f78188a80aff25e1daacb..a669eea337a6ffac0f9458aa9ce79cc62802df01 100644 --- a/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb +++ b/app/services/auto_merge/merge_when_pipeline_succeeds_service.rb @@ -34,7 +34,7 @@ def abort(merge_request, reason) def available_for?(merge_request) super do - check_availability(merge_request) + merge_request.auto_merge_available_when_pipeline_succeeds? end end @@ -44,10 +44,6 @@ def add_system_note(merge_request) SystemNoteService.merge_when_pipeline_succeeds(merge_request, project, current_user, merge_request.diff_head_pipeline.sha) if merge_request.saved_change_to_auto_merge_enabled? end - def check_availability(merge_request) - merge_request.auto_merge_available_when_pipeline_succeeds? - end - def notify(merge_request) notification_service.async.merge_when_pipeline_succeeds(merge_request, current_user) if merge_request.saved_change_to_auto_merge_enabled? end diff --git a/app/services/auto_merge_service.rb b/app/services/auto_merge_service.rb index 912248d3a06ea682df4b74030752515caad36cc3..71a2a3241363f73dea51c476889457f58b785a11 100644 --- a/app/services/auto_merge_service.rb +++ b/app/services/auto_merge_service.rb @@ -4,6 +4,8 @@ class AutoMergeService < BaseService include Gitlab::Utils::StrongMemoize STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS = 'merge_when_pipeline_succeeds' + # Currently only EE but will be moved to CE in (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146730) + STRATEGY_MERGE_WHEN_CHECKS_PASS = 'merge_when_checks_pass' STRATEGIES = [STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS].freeze class << self diff --git a/config/feature_flags/gitlab_com_derisk/refactor_auto_merge.yml b/config/feature_flags/gitlab_com_derisk/refactor_auto_merge.yml new file mode 100644 index 0000000000000000000000000000000000000000..901997fd1608d73cc7a92f207a8b1dd6e21046e0 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/refactor_auto_merge.yml @@ -0,0 +1,9 @@ +--- +name: refactor_auto_merge +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/10874 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146153 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443872 +milestone: '16.11' +group: group::code review +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb index 30ffc2bd2c52dd59ae045c006e7295204a0a916b..81c8eb4da6fed22bed37080d1c7b0e7ca2ca69ff 100644 --- a/ee/app/models/ee/merge_request.rb +++ b/ee/app/models/ee/merge_request.rb @@ -201,20 +201,6 @@ def predefined_variables super.concat(merge_request_approval_variables) end - override :skipped_mergeable_checks - def skipped_mergeable_checks(options = {}) - skip_additional_checks = options[:auto_merge_strategy] == AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS && - ::Feature.enabled?(:additional_merge_when_checks_ready, project) - - super.merge( - skip_approved_check: options[:auto_merge_strategy] == AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS, - skip_draft_check: skip_additional_checks, - skip_blocked_check: skip_additional_checks, - skip_external_status_check: skip_additional_checks, - skip_discussions_check: skip_additional_checks - ) - end - override :merge_blocked_by_other_mrs? def merge_blocked_by_other_mrs? strong_memoize(:merge_blocked_by_other_mrs) do diff --git a/ee/app/services/auto_merge/merge_when_checks_pass_service.rb b/ee/app/services/auto_merge/merge_when_checks_pass_service.rb index 55459c3e7a9d16de4bc3cf60eab6fae1422fdb9e..e8ebe47630219ac9a19f81130c9d715fd8171d37 100644 --- a/ee/app/services/auto_merge/merge_when_checks_pass_service.rb +++ b/ee/app/services/auto_merge/merge_when_checks_pass_service.rb @@ -4,6 +4,7 @@ module AutoMerge class MergeWhenChecksPassService < AutoMerge::BaseService extend Gitlab::Utils::Override + override :execute def execute(merge_request) super do add_system_note(merge_request) @@ -49,9 +50,18 @@ def abort(merge_request, reason) end end + override :available_for def available_for?(merge_request) super do - check_availability(merge_request) + if Feature.disabled?(:refactor_auto_merge, merge_request.project, type: :gitlab_com_derisk) + check_availability(merge_request) + else + next false if Feature.disabled?(:merge_when_checks_pass, merge_request.project) + next false if merge_request.project.merge_trains_enabled? + next false if merge_request.mergeable? && !merge_request.diff_head_pipeline&.active? + + next true + end end end @@ -68,10 +78,12 @@ def add_system_note(merge_request) ) end + # rubocop: disable Metrics/CyclomaticComplexity -- Going to be removed once refactor FF is removed def check_availability(merge_request) return false if Feature.disabled?(:merge_when_checks_pass, merge_request.project) return false unless merge_request.approval_feature_available? return false if merge_request.project.merge_trains_enabled? + return false if merge_request.mergeable? && !merge_request.diff_head_pipeline&.active? merge_request.diff_head_pipeline&.active? || !merge_request.approved? || @@ -82,6 +94,7 @@ def check_availability(merge_request) !merge_request.mergeable_discussions_state?) ) end + # rubocop: enable Metrics/CyclomaticComplexity def notify(merge_request) return unless merge_request.saved_change_to_auto_merge_enabled? diff --git a/ee/app/services/ee/auto_merge_service.rb b/ee/app/services/ee/auto_merge_service.rb index 598704dbe68fcbf84f9b20684587d254e64d554b..1f2987099754d52f56433f9a96b098c3bc628371 100644 --- a/ee/app/services/ee/auto_merge_service.rb +++ b/ee/app/services/ee/auto_merge_service.rb @@ -6,12 +6,11 @@ module AutoMergeService STRATEGY_MERGE_TRAIN = 'merge_train' STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS = 'add_to_merge_train_when_pipeline_succeeds' - # `merge_when_checks_pass` enables auto-merge to be set before approval requirements are satisfied - STRATEGY_MERGE_WHEN_CHECKS_PASS = 'merge_when_checks_pass' + EE_STRATEGIES = [ STRATEGY_MERGE_TRAIN, STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_PIPELINE_SUCCEEDS, - STRATEGY_MERGE_WHEN_CHECKS_PASS + ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS ].freeze class_methods do diff --git a/ee/spec/controllers/projects/merge_requests_controller_spec.rb b/ee/spec/controllers/projects/merge_requests_controller_spec.rb index c881637411b4a3ec0cc3d793af7a8f6b740660d6..ece8e409957b0cde7088c895b74e72a728551035 100644 --- a/ee/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/ee/spec/controllers/projects/merge_requests_controller_spec.rb @@ -425,7 +425,7 @@ def update_merge_request(params = {}) context 'with STRATEGY_MERGE_WHEN_CHECKS_PASS requested' do def merge_when_pipeline_succeeds post :merge, params: base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1', - auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS) + auto_merge_strategy: ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS) end shared_examples_for 'merge MR when checks pass' do diff --git a/ee/spec/features/merge_request/code_owner_approvals_reset_after_merging_to_source_branch_spec.rb b/ee/spec/features/merge_request/code_owner_approvals_reset_after_merging_to_source_branch_spec.rb index 8fa6f6173768fd4732f2a5d962ec70ad3ecc4e2e..d7c1b7d0c93b0e43b0d64e9116a446a9ea3015b1 100644 --- a/ee/spec/features/merge_request/code_owner_approvals_reset_after_merging_to_source_branch_spec.rb +++ b/ee/spec/features/merge_request/code_owner_approvals_reset_after_merging_to_source_branch_spec.rb @@ -32,7 +32,8 @@ source_project: project, source_branch: branch_name, target_project: project, - target_branch: 'master' + target_branch: 'master', + merge_status: 'can_be_merged' ) end diff --git a/ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb b/ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb index 054f9eeec4f29fd2dd0f87bd819dea41219a6392..258c38cbf9d363ab55719f1232ad9a2a4c84ad81 100644 --- a/ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb +++ b/ee/spec/features/merge_request/user_merges_with_push_rules_spec.rb @@ -5,7 +5,10 @@ RSpec.describe 'Merge request > User merges with Push Rules', :js, feature_category: :code_review_workflow do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository, push_rule: push_rule) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: 'Bug NS-04') } + let(:merge_request) do + create(:merge_request_with_diffs, source_project: project, author: user, title: 'Bug NS-04', + merge_status: 'can_be_merged') + end before do project.add_maintainer(user) diff --git a/ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb b/ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb index ac3814dd84346b7aa861223b8c87a1294e08a5a4..cfb768d738abfb664f9fe0fe550383b6092306cb 100644 --- a/ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb +++ b/ee/spec/features/merge_trains/two_merge_requests_on_train_spec.rb @@ -14,14 +14,14 @@ create(:merge_request, source_branch: 'feature', source_project: project, target_branch: 'master', target_project: project, - merge_status: 'unchecked') + merge_status: 'can_be_merged') end let(:merge_request_2) do create(:merge_request, source_branch: 'signed-commits', source_project: project, target_branch: 'master', target_project: project, - merge_status: 'unchecked') + merge_status: 'can_be_merged') end let(:ci_yaml) do diff --git a/ee/spec/requests/api/merge_trains_spec.rb b/ee/spec/requests/api/merge_trains_spec.rb index 72a1c3bb25b665bfb1d2031f6c00a7b5ff803da8..8b8f94b94385575b120bb93d627f9e5368846635 100644 --- a/ee/spec/requests/api/merge_trains_spec.rb +++ b/ee/spec/requests/api/merge_trains_spec.rb @@ -243,7 +243,7 @@ create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master', - merge_status: 'unchecked') + merge_status: 'can_be_merged') end let(:params) { {} } diff --git a/ee/spec/services/auto_merge/merge_when_checks_pass_service_spec.rb b/ee/spec/services/auto_merge/merge_when_checks_pass_service_spec.rb index f1ca9078e97466bdce5f0b8c207bbae52db0e23e..c20c482aa2701fc1ccddf5b8295bc7e5ef5a5b61 100644 --- a/ee/spec/services/auto_merge/merge_when_checks_pass_service_spec.rb +++ b/ee/spec/services/auto_merge/merge_when_checks_pass_service_spec.rb @@ -13,121 +13,310 @@ end describe '#available_for?' do - subject { service.available_for?(mr_merge_if_green_enabled) } - - let_it_be(:approver) { create(:user) } - let(:feature_flag) { true } - let(:draft_status) { true } - let(:blocked_status) { true } - let(:discussions_status) { true } - let(:additional_feature_flag) { true } - let(:pipeline_status) { :running } - let(:approvals_required) { 1 } - - before do - create(:ci_pipeline, pipeline_status, - ref: mr_merge_if_green_enabled.source_branch, - sha: mr_merge_if_green_enabled.diff_head_sha, - project: mr_merge_if_green_enabled.source_project) - mr_merge_if_green_enabled.update_head_pipeline - - approval_rule.users << approver - stub_feature_flags(merge_when_checks_pass: feature_flag, - additional_merge_when_checks_ready: additional_feature_flag) - mr_merge_if_green_enabled.update!(title: 'Draft: check') if draft_status - allow(mr_merge_if_green_enabled).to receive(:merge_blocked_by_other_mrs?).and_return(blocked_status) - allow(mr_merge_if_green_enabled).to receive(:mergeable_discussions_state?).and_return(discussions_status) - end - - where(:pipeline_status, :approvals_required, :draft_status, :blocked_status, :discussions_status, - :external_checks_pass, :additional_feature_flag, :result) do - :running | 0 | true | true | false | false | true | true - :running | 0 | false | false | true | true | true | true - :success | 0 | false | false | true | true | true | false - :success | 0 | true | true | false | false | true | true - :success | 0 | true | true | true | false | false | false - :running | 1 | true | true | false | false | true | true - :success | 1 | true | true | false | false | true | true - :success | 1 | false | false | true | true | true | true - :running | 1 | false | false | true | true | true | true - end + context 'when refactor_auto_merge flag is disabled' do + subject { service.available_for?(mr_merge_if_green_enabled) } + + let_it_be(:approver) { create(:user) } + let(:feature_flag) { true } + let(:draft_status) { false } + let(:blocked_status) { false } + let(:discussions_status) { true } + let(:additional_merge_checks_ff) { true } + let(:pipeline_status) { :running } + let(:approvals_required) { 0 } - with_them do - it { is_expected.to eq result } - end + before do + if pipeline_status + create(:ci_pipeline, pipeline_status, + ref: mr_merge_if_green_enabled.source_branch, + sha: mr_merge_if_green_enabled.diff_head_sha, + project: mr_merge_if_green_enabled.source_project) + mr_merge_if_green_enabled.update_head_pipeline + end - context 'when feature flags merge_when_checks_pass and additional_merge_when_checks_ready are disabled"' do - let(:additional_feature_flag) { false } - let(:feature_flag) { false } + approval_rule.users << approver + stub_feature_flags(merge_when_checks_pass: feature_flag, + additional_merge_when_checks_ready: additional_merge_checks_ff, + refactor_auto_merge: false) + mr_merge_if_green_enabled.update!(title: 'Draft: check') if draft_status + allow(mr_merge_if_green_enabled).to receive(:merge_blocked_by_other_mrs?).and_return(blocked_status) + allow(mr_merge_if_green_enabled).to receive(:mergeable_discussions_state?).and_return(discussions_status) + end where(:pipeline_status, :approvals_required, :draft_status, :blocked_status, :discussions_status, - :external_checks_pass, :result) do - :running | 0 | true | true | false | false | false - :success | 0 | false | false | true | true | false - :running | 1 | false | false | true | true | false - :success | 1 | true | false | true | false | false + :external_checks_pass, :additional_merge_checks_ff, :result) do + :running | 0 | false | false | true | true | true | true + :running | 0 | true | true | false | false | true | true + :running | 0 | false | false | true | true | true | true + :success | 0 | false | false | true | true | true | false + :success | 0 | true | true | false | false | true | true + :success | 0 | true | true | true | false | false | false + :running | 1 | true | true | false | false | true | true + :success | 1 | true | true | false | false | true | true + :success | 1 | false | false | true | true | true | true + :running | 1 | false | false | true | true | true | true end with_them do it { is_expected.to eq result } end - end - context 'when the user does not have permission to merge' do - let(:pipeline_status) { :running } - let(:approvals_required) { 0 } + context 'when mergeable and no pipeline' do + let(:pipeline_status) { nil } - before do - allow(mr_merge_if_green_enabled).to receive(:can_be_merged_by?).and_return(false) + it { is_expected.to eq false } end - it { is_expected.to eq false } - end + context 'when feature flags merge_when_checks_pass and additional_merge_when_checks_ready are disabled"' do + let(:additional_merge_checks_ff) { false } + let(:feature_flag) { false } - context 'when there is an open MR dependency and "additional_merge_when_checks_ready" is disabled' do - let(:pipeline_status) { :running } - let(:approvals_required) { 0 } + where(:pipeline_status, :approvals_required, :draft_status, :blocked_status, :discussions_status, + :external_checks_pass, :result) do + :running | 0 | true | true | false | false | false + :success | 0 | false | false | true | true | false + :running | 1 | false | false | true | true | false + :success | 1 | true | false | true | false | false + end - before do - stub_feature_flags(additional_merge_when_checks_ready: false) - stub_licensed_features(blocking_merge_requests: true) - create(:merge_request_block, blocked_merge_request: mr_merge_if_green_enabled) + with_them do + it { is_expected.to eq result } + end end - it { is_expected.to eq false } + context 'when the user does not have permission to merge' do + let(:pipeline_status) { :running } + let(:approvals_required) { 0 } + + before do + allow(mr_merge_if_green_enabled).to receive(:can_be_merged_by?).and_return(false) + end + + it { is_expected.to eq false } + end + + context 'when there is an open MR dependency and "additional_merge_when_checks_ready" is disabled' do + let(:pipeline_status) { :success } + let(:approvals_required) { 0 } + + before do + stub_feature_flags(additional_merge_when_checks_ready: false) + stub_licensed_features(blocking_merge_requests: true) + create(:merge_request_block, blocked_merge_request: mr_merge_if_green_enabled) + end + + it { is_expected.to eq false } + end + + context 'when merge trains are enabled' do + before do + allow(mr_merge_if_green_enabled.project).to receive(:merge_trains_enabled?).and_return(true) + end + + it { is_expected.to eq false } + end end - context 'when merge trains are enabled' do + context 'when refactor_auto_merge flag is enabled' do + subject { service.available_for?(mr_merge_if_green_enabled) } + + let(:feature_flag) { true } + let(:additional_checks_flag) { true } + before do - allow(mr_merge_if_green_enabled.project).to receive(:merge_trains_enabled?).and_return(true) + stub_feature_flags(merge_when_checks_pass: feature_flag, + additional_merge_when_checks_ready: additional_checks_flag, + refactor_auto_merge: true) + end + + context 'when immediately mergeable' do + context 'when a non active pipeline' do + before do + create(:ci_pipeline, :success, + ref: mr_merge_if_green_enabled.source_branch, + sha: mr_merge_if_green_enabled.diff_head_sha, + project: mr_merge_if_green_enabled.source_project) + mr_merge_if_green_enabled.update_head_pipeline + end + + it { is_expected.to eq false } + end + + context 'when an active pipeline' do + before do + create(:ci_pipeline, :running, + ref: mr_merge_if_green_enabled.source_branch, + sha: mr_merge_if_green_enabled.diff_head_sha, + project: mr_merge_if_green_enabled.source_project) + mr_merge_if_green_enabled.update_head_pipeline + end + + it { is_expected.to eq true } + end + end + + context 'when merge when checks pass flag is off' do + let(:feature_flag) { false } + + it { is_expected.to eq false } + end + + context 'when missing approvals' do + let(:approval_rule) do + create(:approval_merge_request_rule, merge_request: mr_merge_if_green_enabled, + approvals_required: approvals_required) + end + + let(:approvals_required) { 1 } + let_it_be(:approver) { create(:user) } + + before do + approval_rule.users << approver + end + + it { is_expected.to eq true } + + context 'when additional merge when checks flag is off' do + let(:additional_checks_flag) { false } + + it { is_expected.to eq true } + end + end + + context 'when draft status' do + before do + mr_merge_if_green_enabled.update!(title: 'Draft: check') + end + + it { is_expected.to eq true } + + context 'when additional merge when checks flag is off' do + let(:additional_checks_flag) { false } + + it { is_expected.to eq false } + end + end + + context 'when blocked status' do + before do + stub_licensed_features(blocking_merge_requests: true) + create(:merge_request_block, blocked_merge_request: mr_merge_if_green_enabled) + allow(mr_merge_if_green_enabled).to receive(:merge_blocked_by_other_mrs?).and_return(true) + end + + it { is_expected.to eq true } + + context 'when additional merge when checks flag is off' do + let(:additional_checks_flag) { false } + + it { is_expected.to eq false } + end end - it { is_expected.to eq false } + context 'when discussions open' do + before do + allow(mr_merge_if_green_enabled).to receive(:mergeable_discussions_state?).and_return(false) + allow(mr_merge_if_green_enabled) + .to receive(:only_allow_merge_if_all_discussions_are_resolved?).and_return(true) + end + + it { is_expected.to eq true } + + context 'when additional merge when checks flag is off' do + let(:additional_checks_flag) { false } + + it { is_expected.to eq false } + end + end + + context 'when pipline is active' do + before do + create(:ci_pipeline, :running, + ref: mr_merge_if_green_enabled.source_branch, + sha: mr_merge_if_green_enabled.diff_head_sha, + project: mr_merge_if_green_enabled.source_project) + + mr_merge_if_green_enabled.update_head_pipeline + end + + it { is_expected.to eq true } + + context 'when additional merge when checks flag is off' do + let(:additional_checks_flag) { false } + + it { is_expected.to eq true } + end + end + + context 'when the user does not have permission to merge' do + before do + allow(mr_merge_if_green_enabled).to receive(:can_be_merged_by?).and_return(false) + end + + it { is_expected.to eq false } + end + + context 'when merge trains are enabled' do + before do + allow(mr_merge_if_green_enabled.project).to receive(:merge_trains_enabled?).and_return(true) + end + + it { is_expected.to eq false } + end end end describe "#execute" do - it_behaves_like 'auto_merge service #execute' do - let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS } - let(:expected_note) do - "enabled an automatic merge when all merge checks for #{pipeline.sha} pass" - end + let(:merge_request) do + create(:merge_request, target_project: project, source_project: project, + source_branch: 'feature', target_branch: 'master') + end + + context 'when the MR is available for auto merge' do + let(:auto_merge_strategy) { ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS } before do - merge_request.update!(merge_params: { sha: pipeline.sha }) + merge_request.update!(merge_params: { sha: pipeline.sha }, title: 'Draft: check') end - end - context 'when no pipeline exists' do - it_behaves_like 'auto_merge service #execute' do - let(:pipeline) { nil } - let(:auto_merge_strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS } - let(:expected_note) do - "enabled an automatic merge when all merge checks for 123456 pass" + context 'when first time enabling' do + before do + allow(merge_request) + .to receive_messages(head_pipeline: pipeline, diff_head_pipeline: pipeline) + allow(MailScheduler::NotificationServiceWorker).to receive(:perform_async) + + service.execute(merge_request) end - before do - merge_request.update!(merge_params: { sha: "123456" }) + it 'sets the params, merge_user, and flag' do + expect(merge_request).to be_valid + expect(merge_request.merge_when_pipeline_succeeds).to be_truthy + expect(merge_request.merge_params).to include 'commit_message' => 'Awesome message' + expect(merge_request.merge_user).to be user + expect(merge_request.auto_merge_strategy).to eq auto_merge_strategy + end + + it 'schedules a notification' do + expect(MailScheduler::NotificationServiceWorker).to have_received(:perform_async).with( + 'merge_when_pipeline_succeeds', merge_request, user).once + end + + it 'creates a system note' do + pipeline = build(:ci_pipeline) + allow(merge_request).to receive(:diff_head_pipeline) { pipeline } + + note = merge_request.notes.last + expect(note.note).to match "enabled an automatic merge when all merge checks for #{pipeline.sha} pass" + end + end + + context 'when mergeable' do + it 'updates the merge params' do + expect(SystemNoteService).not_to receive(:merge_when_pipeline_succeeds) + expect(MailScheduler::NotificationServiceWorker).not_to receive(:perform_async).with( + 'merge_when_pipeline_succeeds', any_args) + + service.execute(mr_merge_if_green_enabled) end end end diff --git a/ee/spec/services/merge_trains/add_merge_request_service_spec.rb b/ee/spec/services/merge_trains/add_merge_request_service_spec.rb index 5b5acba9450301e0c9f17d3ea1788306da43ecc8..e852942e46d547b52d8974f6aa5ab99f66bd920a 100644 --- a/ee/spec/services/merge_trains/add_merge_request_service_spec.rb +++ b/ee/spec/services/merge_trains/add_merge_request_service_spec.rb @@ -14,7 +14,7 @@ create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master', - merge_status: 'unchecked') + merge_status: 'can_be_merged') end let(:service) { described_class.new(merge_request, user, params) } diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb index dd1e73cf57f30680a627ff34a7ddcd7363ff2245..6dda5b606c43c4d9de9a40945d00a47872396739 100644 --- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb +++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb @@ -30,10 +30,12 @@ end context 'when there is active pipeline for merge request' do + let(:feature_flags_state) { true } + before do - create(:ci_build, pipeline: pipeline) + stub_feature_flags(merge_when_checks_pass: feature_flags_state, merge_blocked_component: feature_flags_state) - stub_feature_flags(merge_blocked_component: false) + create(:ci_build, pipeline: pipeline) sign_in(user) visit project_merge_request_path(project, merge_request) @@ -41,8 +43,17 @@ end it 'retains merge request data after clicking Resolve WIP status' do + # rubocop:disable RSpec/AvoidConditionalStatements -- remove when Auto merge goes to Foss + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/146730 + expected_message = if Gitlab.ee? + "Set to auto-merge" + else + "Merge blocked:" + end + # rubocop:enable RSpec/AvoidConditionalStatements + expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}") - expect(page).to have_content "Merge blocked: Select Mark as ready to remove it from Draft status." + expect(page).to have_content expected_message page.within('.mr-state-widget') do click_button('Mark as ready') @@ -54,7 +65,28 @@ # merge request widget refreshes, which masks missing elements # that should already be present. expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}") - expect(page).not_to have_content("Merge blocked: Select Mark as ready to remove it from Draft status.") + expect(page).to have_content("Set to auto-merge") + end + + context 'when the new merge_when_checks_pass and merge blocked components are disabled' do + let(:feature_flags_state) { false } + + it 'retains merge request data after clicking Resolve WIP status' do + expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}") + expect(page).to have_content "Merge blocked: Select Mark as ready to remove it from Draft status." + + page.within('.mr-state-widget') do + click_button('Mark as ready') + end + + wait_for_requests + + # If we don't disable the wait here, the test will wait until the + # merge request widget refreshes, which masks missing elements + # that should already be present. + expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}") + expect(page).not_to have_content("Merge blocked") + end end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 5448d6867cb0a1991ed15e317d05e8d12f5ddd93..0928394b5710a44a12533d5d6449b800e3bd83e3 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -331,12 +331,31 @@ def click_expand_button visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project) end - it 'is allowed to merge' do - # Wait for the `ci_status` and `merge_check` requests - wait_for_requests + context 'when using merge when pipeline succeeds' do + before do + stub_feature_flags(merge_when_checks_pass: false) + end + + it 'is not allowed to merge' do + # Wait for the `ci_status` and `merge_check` requests + wait_for_requests - expect(page).not_to have_selector('.accept-merge-request') + expect(page).not_to have_selector('.accept-merge-request') + end end + # TODO When moving merge when checks pass to FOSS + # context 'when using merge when checks pass' do + # before do + # stub_feature_flags(merge_when_checks_pass: true) + # end + # + # it 'is not allowed to set auto merge' do + # # Wait for the `ci_status` and `merge_check` requests + # wait_for_requests + # + # expect(page).to have_selector('.accept-merge-request') + # end + # end end context 'view merge request with MWPS enabled but automatically merge fails' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index a0b887655b2693b0dc6a303d5e240b414ed4b1a4..9b81144cf4fe615d33aa3bdfa7d0088edc849e6d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3627,15 +3627,40 @@ def set_compare(merge_request) describe '#skipped_mergeable_checks' do subject { build_stubbed(:merge_request).skipped_mergeable_checks(options) } + let(:feature_flag) { true } + + before do + stub_feature_flags(additional_merge_when_checks_ready: feature_flag) + end + where(:options, :skip_ci_check) do {} | false { auto_merge_requested: false } | false { auto_merge_requested: true } | true end - with_them do it { is_expected.to include(skip_ci_check: skip_ci_check) } end + + context 'when auto_merge_requested is true' do + let(:options) { { auto_merge_requested: true, auto_merge_strategy: auto_merge_strategy } } + + where(:auto_merge_strategy, :skip_approved_check, :skip_draft_check, :skip_blocked_check, + :skip_discussions_check, :skip_external_status_check, :feature_flag) do + '' | false | false | false | false | false | true + AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS | false | false | false | false | false | true + AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | true | true | true | true | true + AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS | true | false | false | false | false | false + end + + with_them do + it do + is_expected.to include(skip_approved_check: skip_approved_check, skip_draft_check: skip_draft_check, + skip_blocked_check: skip_blocked_check, skip_discussions_check: skip_discussions_check, + skip_external_status_check: skip_external_status_check) + end + end + end end describe '#check_mergeability' do @@ -6252,6 +6277,26 @@ def transition! end end + describe '#mergeability_checks_pass?' do + let(:merge_request) { build_stubbed(:merge_request) } + let(:result) { instance_double(ServiceResponse, success?: { results: ['result'] }) } + + it 'executes MergeRequests::Mergeability::RunChecksService with all mergeability checks and returns a boolean' do + expect_next_instance_of( + MergeRequests::Mergeability::RunChecksService, + merge_request: merge_request, + params: {} + ) do |svc| + expect(svc) + .to receive(:execute) + .with(described_class.all_mergeability_checks, execute_all: false) + .and_return(result) + end + + expect(merge_request.mergeability_checks_pass?).to be_truthy + end + end + describe '#only_allow_merge_if_pipeline_succeeds?' do let(:merge_request) { build_stubbed(:merge_request) } diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb index f4b3430200c105d34763214654c1aef086bceb3f..5d3ce64898424c3ff5290e312f869fd5f44adb3a 100644 --- a/spec/services/auto_merge/base_service_spec.rb +++ b/spec/services/auto_merge/base_service_spec.rb @@ -305,22 +305,65 @@ def abort_with_error_in_yield describe '#available_for?' do using RSpec::Parameterized::TableSyntax - subject(:available_for) { service.available_for?(merge_request) { true } } + subject(:available_for) { service.available_for?(merge_request) { yield_result } } let(:merge_request) { create(:merge_request) } + let(:yield_result) { true } + let(:checks_pass) { true } + let(:can_be_merged) { true } - where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :result) do - true | true | false | true | false | false | true - false | true | false | true | false | false | false - true | false | false | true | false | false | false - true | true | true | true | false | false | false - true | true | false | false | false | false | false - true | true | false | true | true | false | false - true | true | false | true | false | true | false + context 'when can_be_merged is true' do + before do + allow(merge_request).to receive(:can_be_merged_by?).and_return(can_be_merged) + allow(merge_request).to receive(:mergeability_checks_pass?).and_return(checks_pass) + end + + context 'when the mergeabilty checks pass is true' do + context 'when the yield is true' do + it 'returns true' do + expect(available_for).to be_truthy + end + end + + context 'when the yield is false' do + let(:yield_result) { false } + + it 'returns false' do + expect(available_for).to be_falsey + end + end + end + + context 'when the mergeabilty checks pass is false' do + let(:checks_pass) { false } + + context 'when the yield is true' do + it 'returns false' do + expect(available_for).to be_falsey + end + end + + context 'when the yield is false' do + let(:yield_result) { false } + + it 'returns false' do + expect(available_for).to be_falsey + end + end + end end - with_them do + context 'when can_be_merged is false' do + let(:can_be_merged) { false } + + it 'returns false' do + expect(available_for).to be_falsey + end + end + + context 'when refactor_auto_merge is disabled' do before do + stub_feature_flags(refactor_auto_merge: false) allow(merge_request).to receive(:can_be_merged_by?).and_return(can_be_merged) allow(merge_request).to receive(:open?).and_return(open) allow(merge_request).to receive(:broken?).and_return(broken) @@ -329,8 +372,20 @@ def abort_with_error_in_yield allow(merge_request).to receive(:merge_blocked_by_other_mrs?).and_return(blocked) end - it 'returns the expected results' do - expect(available_for).to eq(result) + where(:can_be_merged, :open, :broken, :discussions, :blocked, :draft, :result) do + true | true | false | true | false | false | true + false | true | false | true | false | false | false + true | false | false | true | false | false | false + true | true | true | true | false | false | false + true | true | false | false | false | false | false + true | true | false | true | true | false | false + true | true | false | true | false | true | false + end + + with_them do + it 'returns the expected results' do + expect(available_for).to eq(result) + end end end end diff --git a/spec/services/merge_requests/merge_orchestration_service_spec.rb b/spec/services/merge_requests/merge_orchestration_service_spec.rb index 7e6eeec2a2d13e4377e27f132faa4365a7151e12..faec5ac2fde5b6fb58051048081bbdc642282da9 100644 --- a/spec/services/merge_requests/merge_orchestration_service_spec.rb +++ b/spec/services/merge_requests/merge_orchestration_service_spec.rb @@ -102,7 +102,7 @@ context 'when merge request is not mergeable' do before do - allow(merge_request).to receive(:mergeable?) { false } + merge_request.update!(merge_status: 'cannot_be_merged') end it { is_expected.to eq(false) } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index e253c06125faf784adccfc0fb4e262cf86e9494f..aee39f8beb20ecb5ec41d7d2af17075d0865e3d9 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -553,7 +553,13 @@ def update_merge_request(opts) head_pipeline_of: merge_request ) - expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, { sha: merge_request.diff_head_sha }) + expected_class = if Gitlab.ee? + AutoMerge::MergeWhenChecksPassService + else + AutoMerge::MergeWhenPipelineSucceedsService + end + + expect(expected_class).to receive(:new).with(project, user, { sha: merge_request.diff_head_sha }) .and_return(service_mock) allow(service_mock).to receive(:available_for?) { true } expect(service_mock).to receive(:execute).with(merge_request)