diff options
26 files changed, 1248 insertions, 28 deletions
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a0b2acd502b..88ab021817a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -16,6 +16,7 @@ module Ci belongs_to :user belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' + belongs_to :merge_request, class_name: 'MergeRequest' has_internal_id :iid, scope: :project, presence: false, init: ->(s) do s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count @@ -50,6 +51,9 @@ module Ci validates :sha, presence: { unless: :importing? } validates :ref, presence: { unless: :importing? } + validates :merge_request, presence: { if: :merge_request? } + validates :merge_request, absence: { unless: :merge_request? } + validates :tag, inclusion: { in: [false], if: :merge_request? } validates :status, presence: { unless: :importing? } validate :valid_commit_sha, unless: :importing? @@ -171,6 +175,13 @@ module Ci scope :internal, -> { where(source: internal_sources) } + scope :sort_by_merge_request_pipelines, -> do + sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC' + query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend + + order(query) + end + scope :for_user, -> (user) { where(user: user) } # Returns the pipelines in descending order (= newest first), optionally @@ -372,7 +383,7 @@ module Ci end def branch? - !tag? + !tag? && !merge_request? end def stuck? @@ -619,7 +630,12 @@ module Ci # All the merge requests for which the current pipeline runs/ran against def all_merge_requests - @all_merge_requests ||= project.merge_requests.where(source_branch: ref) + @all_merge_requests ||= + if merge_request? + project.merge_requests.where(id: merge_request.id) + else + project.merge_requests.where(source_branch: ref) + end end def detailed_status(current_user) @@ -696,6 +712,8 @@ module Ci def git_ref if branch? Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s + elsif merge_request? + Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s elsif tag? Gitlab::Git::TAG_REF_PREFIX + ref.to_s else diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 8d8d16e2ec1..c0f16066e0b 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -21,7 +21,8 @@ module Ci trigger: 3, schedule: 4, api: 5, - external: 6 + external: 6, + merge_request: 10 } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 92add079a02..6b05ee06199 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -63,6 +63,7 @@ class MergeRequest < ActiveRecord::Base dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_many :cached_closes_issues, through: :merge_requests_closing_issues, source: :issue + has_many :merge_request_pipelines, foreign_key: 'merge_request_id', class_name: 'Ci::Pipeline' belongs_to :assignee, class_name: "User" @@ -1052,12 +1053,17 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end - def all_pipelines + def all_pipelines(shas: all_commit_shas) return Ci::Pipeline.none unless source_project @all_pipelines ||= source_project.pipelines - .where(sha: all_commit_shas, ref: source_branch) - .order(id: :desc) + .where(sha: shas, ref: source_branch) + .where(merge_request: [nil, self]) + .sort_by_merge_request_pipelines + end + + def merge_request_pipeline_exists? + merge_request_pipelines.exists?(sha: diff_head_sha) end def has_test_reports? diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index aef838409e0..477b6710168 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -48,6 +48,7 @@ class PipelineEntity < Grape::Entity expose :tag?, as: :tag expose :branch?, as: :branch + expose :merge_request?, as: :merge_request end expose :commit, using: CommitEntity diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 46a82377c10..c7ce7b7a5f8 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -14,7 +14,7 @@ module Ci Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Create].freeze - def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) + def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, &block) @pipeline = Ci::Pipeline.new command = Gitlab::Ci::Pipeline::Chain::Command.new( @@ -25,6 +25,7 @@ module Ci before_sha: params[:before], trigger_request: trigger_request, schedule: schedule, + merge_request: merge_request, ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors, seeds_block: block, diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 28c3219b37b..fe19abf50f6 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -54,6 +54,24 @@ module MergeRequests merge_request, merge_request.project, current_user, merge_request.assignee) end + def create_merge_request_pipeline(merge_request, user) + return unless Feature.enabled?(:ci_merge_request_pipeline, + merge_request.source_project, + default_enabled: true) + + ## + # UpdateMergeRequestsWorker could be retried by an exception. + # MR pipelines should not be recreated in such case. + return if merge_request.merge_request_pipeline_exists? + + Ci::CreatePipelineService + .new(merge_request.source_project, user, ref: merge_request.source_branch) + .execute(:merge_request, + ignore_skip_ci: true, + save_on_errors: false, + merge_request: merge_request) + end + # Returns all origin and fork merge requests from `@project` satisfying passed arguments. # rubocop: disable CodeReuse/ActiveRecord def merge_requests_for(source_branch, mr_states: [:opened]) diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index 6081a7d1de0..7bb9fa60515 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -25,6 +25,7 @@ module MergeRequests def after_create(issuable) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) + create_merge_request_pipeline(issuable, current_user) update_merge_requests_head_pipeline(issuable) super @@ -49,18 +50,14 @@ module MergeRequests merge_request.update(head_pipeline_id: pipeline.id) if pipeline end - # rubocop: disable CodeReuse/ActiveRecord def head_pipeline_for(merge_request) return unless merge_request.source_project sha = merge_request.source_branch_sha return unless sha - pipelines = merge_request.source_project.pipelines.where(ref: merge_request.source_branch, sha: sha) - - pipelines.order(id: :desc).first + merge_request.all_pipelines(shas: sha).first end - # rubocop: enable CodeReuse/ActiveRecord def set_projects! # @project is used to determine whether the user can set the merge request's diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 5fe48da1cd6..667b5916f38 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -92,6 +92,7 @@ module MergeRequests end merge_request.mark_as_unchecked + create_merge_request_pipeline(merge_request, current_user) UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) end diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index 9ce51662969..e8494ffa002 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -6,10 +6,11 @@ class UpdateHeadPipelineForMergeRequestWorker queue_namespace :pipeline_processing - # rubocop: disable CodeReuse/ActiveRecord def perform(merge_request_id) merge_request = MergeRequest.find(merge_request_id) - pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last + + sha = merge_request.diff_head_sha + pipeline = merge_request.all_pipelines(shas: sha).first return unless pipeline && pipeline.latest? @@ -21,7 +22,6 @@ class UpdateHeadPipelineForMergeRequestWorker merge_request.update_attribute(:head_pipeline_id, pipeline.id) end - # rubocop: enable CodeReuse/ActiveRecord def log_error_message_for(merge_request) Rails.logger.error( diff --git a/changelogs/unreleased/mr-pipelines-2.yml b/changelogs/unreleased/mr-pipelines-2.yml new file mode 100644 index 00000000000..683c626c3ce --- /dev/null +++ b/changelogs/unreleased/mr-pipelines-2.yml @@ -0,0 +1,5 @@ +--- +title: Merge request pipelines +merge_request: 23217 +author: +type: added diff --git a/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb b/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb new file mode 100644 index 00000000000..65476109c61 --- /dev/null +++ b/db/migrate/20181119081539_add_merge_request_id_to_ci_pipelines.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddMergeRequestIdToCiPipelines < ActiveRecord::Migration + DOWNTIME = false + + def up + add_column :ci_pipelines, :merge_request_id, :integer + end + + def down + remove_column :ci_pipelines, :merge_request_id, :integer + end +end diff --git a/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb new file mode 100644 index 00000000000..c2b5b239279 --- /dev/null +++ b/db/migrate/20181120091639_add_foreign_key_to_ci_pipelines_merge_requests.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddForeignKeyToCiPipelinesMergeRequests < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, :merge_request_id + add_concurrent_foreign_key :ci_pipelines, :merge_requests, column: :merge_request_id, on_delete: :cascade + end + + def down + if foreign_key_exists?(:ci_pipelines, :merge_requests, column: :merge_request_id) + remove_foreign_key :ci_pipelines, :merge_requests + end + + remove_concurrent_index :ci_pipelines, :merge_request_id + end +end diff --git a/db/schema.rb b/db/schema.rb index d048be08d77..65a69c2850c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -474,7 +474,9 @@ ActiveRecord::Schema.define(version: 20181126153547) do t.boolean "protected" t.integer "failure_reason" t.integer "iid" + t.integer "merge_request_id" t.index ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree + t.index ["merge_request_id"], name: "index_ci_pipelines_on_merge_request_id", using: :btree t.index ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree t.index ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree t.index ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree @@ -2292,6 +2294,7 @@ ActiveRecord::Schema.define(version: 20181126153547) do add_foreign_key "ci_pipeline_variables", "ci_pipelines", column: "pipeline_id", name: "fk_f29c5f4380", on_delete: :cascade add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify + add_foreign_key "ci_pipelines", "merge_requests", name: "fk_a23be95014", on_delete: :cascade add_foreign_key "ci_pipelines", "projects", name: "fk_86635dbd80", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "ci_runners", column: "runner_id", on_delete: :cascade add_foreign_key "ci_runner_namespaces", "namespaces", on_delete: :cascade diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index b445a872b3d..d33d1edfe35 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -16,6 +16,7 @@ module Gitlab trigger_requests: Array(@command.trigger_request), user: @command.current_user, pipeline_schedule: @command.schedule, + merge_request: @command.merge_request, protected: @command.protected_ref?, variables_attributes: Array(@command.variables_attributes) ) diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 05978804d92..100b9521412 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -8,7 +8,7 @@ module Gitlab Command = Struct.new( :source, :project, :current_user, :origin_ref, :checkout_sha, :after_sha, :before_sha, - :trigger_request, :schedule, + :trigger_request, :schedule, :merge_request, :ignore_skip_ci, :save_incompleted, :seeds_block, :variables_attributes ) do diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb new file mode 100644 index 00000000000..ae1313cf117 --- /dev/null +++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb @@ -0,0 +1,333 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Merge request > User sees merge request pipelines', :js do + include ProjectForksHelper + include TestReportsHelper + + let(:project) { create(:project, :public, :repository) } + let(:user) { project.creator } + + let(:config) do + { + build: { + script: 'build' + }, + test: { + script: 'test', + only: ['merge_requests'] + }, + deploy: { + script: 'deploy', + except: ['merge_requests'] + } + } + end + + before do + stub_application_setting(auto_devops_enabled: false) + stub_feature_flags(ci_merge_request_pipeline: true) + stub_ci_pipeline_yaml_file(YAML.dump(config)) + project.add_maintainer(user) + sign_in(user) + end + + context 'when a user created a merge request in the parent project' do + let(:merge_request) do + create(:merge_request, + source_project: project, + target_project: project, + source_branch: 'feature', + target_branch: 'master') + end + + let!(:push_pipeline) do + Ci::CreatePipelineService.new(project, user, ref: 'feature') + .execute(:push) + end + + let!(:merge_request_pipeline) do + Ci::CreatePipelineService.new(project, user, ref: 'feature') + .execute(:merge_request, merge_request: merge_request) + end + + before do + visit project_merge_request_path(project, merge_request) + + page.within('.merge-request-tabs') do + click_link('Pipelines') + end + end + + it 'sees branch pipelines and merge request pipelines in correct order' do + page.within('.ci-table') do + expect(page).to have_selector('.ci-pending', count: 2) + expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}") + end + end + + it 'sees the latest merge request pipeline as the head pipeline' do + page.within('.ci-widget-content') do + expect(page).to have_content("##{merge_request_pipeline.id}") + end + end + + context 'when a user updated a merge request in the parent project' do + let!(:push_pipeline_2) do + Ci::CreatePipelineService.new(project, user, ref: 'feature') + .execute(:push) + end + + let!(:merge_request_pipeline_2) do + Ci::CreatePipelineService.new(project, user, ref: 'feature') + .execute(:merge_request, merge_request: merge_request) + end + + before do + visit project_merge_request_path(project, merge_request) + + page.within('.merge-request-tabs') do + click_link('Pipelines') + end + end + + it 'sees branch pipelines and merge request pipelines in correct order' do + page.within('.ci-table') do + expect(page).to have_selector('.ci-pending', count: 4) + + expect(all('.js-pipeline-url-link')[0]) + .to have_content("##{merge_request_pipeline_2.id}") + + expect(all('.js-pipeline-url-link')[1]) + .to have_content("##{merge_request_pipeline.id}") + + expect(all('.js-pipeline-url-link')[2]) + .to have_content("##{push_pipeline_2.id}") + + expect(all('.js-pipeline-url-link')[3]) + .to have_content("##{push_pipeline.id}") + end + end + + it 'sees the latest merge request pipeline as the head pipeline' do + page.within('.ci-widget-content') do + expect(page).to have_content("##{merge_request_pipeline_2.id}") + end + end + end + + context 'when a user merges a merge request in the parent project' do + before do + click_button 'Merge when pipeline succeeds' + + wait_for_requests + end + + context 'when merge request pipeline is pending' do + it 'waits the head pipeline' do + expect(page).to have_content('to be merged automatically when the pipeline succeeds') + expect(page).to have_link('Cancel automatic merge') + end + end + + context 'when merge request pipeline succeeds' do + before do + merge_request_pipeline.succeed! + + wait_for_requests + end + + it 'merges the merge request' do + expect(page).to have_content('Merged by') + expect(page).to have_link('Revert') + end + end + + context 'when branch pipeline succeeds' do + before do + push_pipeline.succeed! + + wait_for_requests + end + + it 'waits the head pipeline' do + expect(page).to have_content('to be merged automatically when the pipeline succeeds') + expect(page).to have_link('Cancel automatic merge') + end + end + end + + context 'when there are no `merge_requests` keyword in .gitlab-ci.yml' do + let(:config) do + { + build: { + script: 'build' + }, + test: { + script: 'test' + }, + deploy: { + script: 'deploy' + } + } + end + + it 'sees a branch pipeline in pipeline tab' do + page.within('.ci-table') do + expect(page).to have_selector('.ci-pending', count: 1) + expect(first('.js-pipeline-url-link')).to have_content("##{push_pipeline.id}") + end + end + + it 'sees the latest branch pipeline as the head pipeline' do + page.within('.ci-widget-content') do + expect(page).to have_content("##{push_pipeline.id}") + end + end + end + end + + context 'when a user created a merge request from a forked project to the parent project' do + let(:merge_request) do + create(:merge_request, + source_project: forked_project, + target_project: project, + source_branch: 'feature', + target_branch: 'master') + end + + let!(:push_pipeline) do + Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') + .execute(:push) + end + + let!(:merge_request_pipeline) do + Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') + .execute(:merge_request, merge_request: merge_request) + end + + let(:forked_project) { fork_project(project, user2, repository: true) } + let(:user2) { create(:user) } + + before do + forked_project.add_maintainer(user2) + + visit project_merge_request_path(project, merge_request) + + page.within('.merge-request-tabs') do + click_link('Pipelines') + end + end + + it 'sees branch pipelines and merge request pipelines in correct order' do + page.within('.ci-table') do + expect(page).to have_selector('.ci-pending', count: 2) + expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}") + end + end + + it 'sees the latest merge request pipeline as the head pipeline' do + page.within('.ci-widget-content') do + expect(page).to have_content("##{merge_request_pipeline.id}") + end + end + + it 'sees pipeline list in forked project' do + visit project_pipelines_path(forked_project) + + expect(page).to have_selector('.ci-pending', count: 2) + end + + context 'when a user updated a merge request from a forked project to the parent project' do + let!(:push_pipeline_2) do + Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') + .execute(:push) + end + + let!(:merge_request_pipeline_2) do + Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') + .execute(:merge_request, merge_request: merge_request) + end + + before do + visit project_merge_request_path(project, merge_request) + + page.within('.merge-request-tabs') do + click_link('Pipelines') + end + end + + it 'sees branch pipelines and merge request pipelines in correct order' do + page.within('.ci-table') do + expect(page).to have_selector('.ci-pending', count: 4) + + expect(all('.js-pipeline-url-link')[0]) + .to have_content("##{merge_request_pipeline_2.id}") + + expect(all('.js-pipeline-url-link')[1]) + .to have_content("##{merge_request_pipeline.id}") + + expect(all('.js-pipeline-url-link')[2]) + .to have_content("##{push_pipeline_2.id}") + + expect(all('.js-pipeline-url-link')[3]) + .to have_content("##{push_pipeline.id}") + end + end + + it 'sees the latest merge request pipeline as the head pipeline' do + page.within('.ci-widget-content') do + expect(page).to have_content("##{merge_request_pipeline_2.id}") + end + end + + it 'sees pipeline list in forked project' do + visit project_pipelines_path(forked_project) + + expect(page).to have_selector('.ci-pending', count: 4) + end + end + + context 'when a user merges a merge request from a forked project to the parent project' do + before do + click_button 'Merge when pipeline succeeds' + + wait_for_requests + end + + context 'when merge request pipeline is pending' do + it 'waits the head pipeline' do + expect(page).to have_content('to be merged automatically when the pipeline succeeds') + expect(page).to have_link('Cancel automatic merge') + end + end + + context 'when merge request pipeline succeeds' do + before do + merge_request_pipeline.succeed! + + wait_for_requests + end + + it 'merges the merge request' do + expect(page).to have_content('Merged by') + expect(page).to have_link('Revert') + end + end + + context 'when branch pipeline succeeds' do + before do + push_pipeline.succeed! + + wait_for_requests + end + + it 'waits the head pipeline' do + expect(page).to have_content('to be merged automatically when the pipeline succeeds') + expect(page).to have_link('Cancel automatic merge') + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index 85d73e5c382..fab071405df 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -18,6 +18,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do before_sha: nil, trigger_request: nil, schedule: nil, + merge_request: nil, project: project, current_user: user, variables_attributes: variables_attributes) @@ -76,6 +77,7 @@ describe Gitlab::Ci::Pipeline::Chain::Build do before_sha: nil, trigger_request: nil, schedule: nil, + merge_request: nil, project: project, current_user: user) end @@ -90,4 +92,31 @@ describe Gitlab::Ci::Pipeline::Chain::Build do expect(pipeline).to be_tag end end + + context 'when pipeline is running for a merge request' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + source: :merge_request, + origin_ref: 'feature', + checkout_sha: project.commit.id, + after_sha: nil, + before_sha: nil, + trigger_request: nil, + schedule: nil, + merge_request: merge_request, + project: project, + current_user: user) + end + + let(:merge_request) { build(:merge_request, target_project: project) } + + before do + step.perform! + end + + it 'correctly indicated that this is a merge request pipeline' do + expect(pipeline).to be_merge_request + expect(pipeline.merge_request).to eq(merge_request) + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb index a8dc5356413..053bc421649 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -106,4 +106,34 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do expect(step.break?).to be false end end + + context 'when pipeline source is merge request' do + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + end + + let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } + + let(:merge_request_pipeline) do + build(:ci_pipeline, source: :merge_request, project: project) + end + + let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) } + + context "when config contains 'merge_requests' keyword" do + let(:config) { { rspec: { script: 'echo', only: ['merge_requests'] } } } + + it 'does not break the chain' do + expect(chain).not_to be_break + end + end + + context "when config contains 'merge_request' keyword" do + let(:config) { { rspec: { script: 'echo', only: ['merge_request'] } } } + + it 'does not break the chain' do + expect(chain).not_to be_break + end + end + end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 31ab11bbf8d..fa01eb7901b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -94,6 +94,7 @@ merge_requests: - timelogs - head_pipeline - latest_merge_request_diff +- merge_request_pipelines merge_request_diff: - merge_request - merge_request_diff_commits @@ -121,6 +122,7 @@ pipelines: - artifacts - pipeline_schedule - merge_requests +- merge_request - deployments - environments pipeline_variables: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f7935149b23..d3bfde181bc 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -243,6 +243,7 @@ Ci::Pipeline: - failure_reason - protected - iid +- merge_request_id Ci::Stage: - id - name diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 3076a882445..dcaea9d3f83 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -14,6 +14,7 @@ describe Ci::Pipeline, :mailer do it { is_expected.to belong_to(:user) } it { is_expected.to belong_to(:auto_canceled_by) } it { is_expected.to belong_to(:pipeline_schedule) } + it { is_expected.to belong_to(:merge_request) } it { is_expected.to have_many(:statuses) } it { is_expected.to have_many(:trigger_requests) } @@ -37,6 +38,128 @@ describe Ci::Pipeline, :mailer do end end + describe '.sort_by_merge_request_pipelines' do + subject { described_class.sort_by_merge_request_pipelines } + + context 'when branch pipelines exist' do + let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) } + let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) } + + it 'returns pipelines order by id' do + expect(subject).to eq([branch_pipeline_2, + branch_pipeline_1]) + end + end + + context 'when merge request pipelines exist' do + let!(:merge_request_pipeline_1) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let!(:merge_request_pipeline_2) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns pipelines order by id' do + expect(subject).to eq([merge_request_pipeline_2, + merge_request_pipeline_1]) + end + end + + context 'when both branch pipeline and merge request pipeline exist' do + let!(:branch_pipeline_1) { create(:ci_pipeline, source: :push) } + let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) } + + let!(:merge_request_pipeline_1) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let!(:merge_request_pipeline_2) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns merge request pipeline first' do + expect(subject).to eq([merge_request_pipeline_2, + merge_request_pipeline_1, + branch_pipeline_2, + branch_pipeline_1]) + end + end + end + + describe '.merge_request' do + subject { described_class.merge_request } + + context 'when there is a merge request pipeline' do + let!(:pipeline) { create(:ci_pipeline, source: :merge_request, merge_request: merge_request) } + let(:merge_request) { create(:merge_request) } + + it 'returns merge request pipeline first' do + expect(subject).to eq([pipeline]) + end + end + + context 'when there are no merge request pipelines' do + let!(:pipeline) { create(:ci_pipeline, source: :push) } + + it 'returns empty array' do + expect(subject).to be_empty + end + end + end + + describe 'Validations for merge request pipelines' do + let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) } + + context 'when source is merge request' do + let(:source) { :merge_request } + + context 'when merge request is specified' do + let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') } + + it { expect(pipeline).to be_valid } + end + + context 'when merge request is empty' do + let(:merge_request) { nil } + + it { expect(pipeline).not_to be_valid } + end + end + + context 'when source is web' do + let(:source) { :web } + + context 'when merge request is specified' do + let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') } + + it { expect(pipeline).not_to be_valid } + end + + context 'when merge request is empty' do + let(:merge_request) { nil } + + it { expect(pipeline).to be_valid } + end + end + end + describe 'modules' do it_behaves_like 'AtomicInternalId', validate_presence: false do let(:internal_id_attribute) { :iid } @@ -760,27 +883,85 @@ describe Ci::Pipeline, :mailer do describe '#branch?' do subject { pipeline.branch? } - context 'is not a tag' do + context 'when ref is not a tag' do before do pipeline.tag = false end - it 'return true when tag is set to false' do + it 'return true' do is_expected.to be_truthy end + + context 'when source is merge request' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns false' do + is_expected.to be_falsey + end + end end - context 'is not a tag' do + context 'when ref is a tag' do before do pipeline.tag = true end - it 'return false when tag is set to true' do + it 'return false' do is_expected.to be_falsey end end end + describe '#git_ref' do + subject { pipeline.send(:git_ref) } + + context 'when ref is branch' do + let(:pipeline) { create(:ci_pipeline, tag: false) } + + it 'returns branch ref' do + is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s) + end + end + + context 'when ref is tag' do + let(:pipeline) { create(:ci_pipeline, tag: true) } + + it 'returns branch ref' do + is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.ref.to_s) + end + end + + context 'when ref is merge request' do + let(:pipeline) do + create(:ci_pipeline, + source: :merge_request, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns branch ref' do + is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref.to_s) + end + end + end + describe 'ref_exists?' do context 'when repository exists' do using RSpec::Parameterized::TableSyntax @@ -1855,6 +2036,55 @@ describe Ci::Pipeline, :mailer do expect(pipeline.all_merge_requests).to be_empty end + + context 'when there is a merge request pipeline' do + let(:source_branch) { 'feature' } + let(:target_branch) { 'master' } + + let!(:pipeline) do + create(:ci_pipeline, + source: :merge_request, + project: project, + ref: source_branch, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: source_branch, + target_project: project, + target_branch: target_branch) + end + + it 'returns an associated merge request' do + expect(pipeline.all_merge_requests).to eq([merge_request]) + end + + context 'when there is another merge request pipeline that targets a different branch' do + let(:target_branch_2) { 'merge-test' } + + let!(:pipeline_2) do + create(:ci_pipeline, + source: :merge_request, + project: project, + ref: source_branch, + merge_request: merge_request_2) + end + + let(:merge_request_2) do + create(:merge_request, + source_project: project, + source_branch: source_branch, + target_project: project, + target_branch: target_branch_2) + end + + it 'does not return an associated merge request' do + expect(pipeline.all_merge_requests).not_to include(merge_request_2) + end + end + end end describe '#stuck?' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ad55c280399..9b60054e14a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1206,6 +1206,119 @@ describe MergeRequest do expect(subject.all_pipelines).to contain_exactly(pipeline) end end + + context 'when pipelines exist for the branch and merge request' do + let(:source_ref) { 'feature' } + let(:target_ref) { 'master' } + + let!(:branch_pipeline) do + create(:ci_pipeline, + source: :push, + project: project, + ref: source_ref, + sha: shas.second) + end + + let!(:merge_request_pipeline) do + create(:ci_pipeline, + source: :merge_request, + project: project, + ref: source_ref, + sha: shas.second, + merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: source_ref, + target_project: project, + target_branch: target_ref) + end + + let(:project) { create(:project, :repository) } + let(:shas) { project.repository.commits(source_ref, limit: 2).map(&:id) } + + before do + allow(merge_request).to receive(:all_commit_shas) { shas } + end + + it 'returns merge request pipeline first' do + expect(merge_request.all_pipelines) + .to eq([merge_request_pipeline, + branch_pipeline]) + end + + context 'when there are a branch pipeline and a merge request pipeline' do + let!(:branch_pipeline_2) do + create(:ci_pipeline, + source: :push, + project: project, + ref: source_ref, + sha: shas.first) + end + + let!(:merge_request_pipeline_2) do + create(:ci_pipeline, + source: :merge_request, + project: project, + ref: source_ref, + sha: shas.first, + merge_request: merge_request) + end + + it 'returns merge request pipelines first' do + expect(merge_request.all_pipelines) + .to eq([merge_request_pipeline_2, + merge_request_pipeline, + branch_pipeline_2, + branch_pipeline]) + end + end + + context 'when there are multiple merge request pipelines from the same branch' do + let!(:branch_pipeline_2) do + create(:ci_pipeline, + source: :push, + project: project, + ref: source_ref, + sha: shas.first) + end + + let!(:merge_request_pipeline_2) do + create(:ci_pipeline, + source: :merge_request, + project: project, + ref: source_ref, + sha: shas.first, + merge_request: merge_request_2) + end + + let(:merge_request_2) do + create(:merge_request, + source_project: project, + source_branch: source_ref, + target_project: project, + target_branch: 'stable') + end + + before do + allow(merge_request_2).to receive(:all_commit_shas) { shas } + end + + it 'returns only related merge request pipelines' do + expect(merge_request.all_pipelines) + .to eq([merge_request_pipeline, + branch_pipeline_2, + branch_pipeline]) + + expect(merge_request_2.all_pipelines) + .to eq([merge_request_pipeline_2, + branch_pipeline_2, + branch_pipeline]) + end + end + end end describe '#has_test_reports?' do diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index a4582d1bc64..7bc990f05c2 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -18,7 +18,8 @@ describe Ci::CreatePipelineService do message: 'Message', ref: ref_name, trigger_request: nil, - variables_attributes: nil) + variables_attributes: nil, + merge_request: nil) params = { ref: ref, before: '00000000', after: after, @@ -26,7 +27,7 @@ describe Ci::CreatePipelineService do variables_attributes: variables_attributes } described_class.new(project, user, params).execute( - source, trigger_request: trigger_request) + source, trigger_request: trigger_request, merge_request: merge_request) end context 'valid params' do @@ -60,10 +61,10 @@ describe Ci::CreatePipelineService do context 'when merge requests already exist for this source branch' do let(:merge_request_1) do - create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) + create(:merge_request, source_branch: 'feature', target_branch: "master", source_project: project) end let(:merge_request_2) do - create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project) + create(:merge_request, source_branch: 'feature', target_branch: "v1.1.0", source_project: project) end context 'when related merge request is already merged' do @@ -83,7 +84,7 @@ describe Ci::CreatePipelineService do merge_request_1 merge_request_2 - head_pipeline = execute_service + head_pipeline = execute_service(ref: 'feature', after: nil) expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline) expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline) @@ -123,12 +124,12 @@ describe Ci::CreatePipelineService do let!(:target_project) { create(:project, :repository) } it 'updates head pipeline for merge request' do - merge_request = create(:merge_request, source_branch: 'master', - target_branch: "branch_1", + merge_request = create(:merge_request, source_branch: 'feature', + target_branch: "master", source_project: project, target_project: target_project) - head_pipeline = execute_service + head_pipeline = execute_service(ref: 'feature', after: nil) expect(merge_request.reload.head_pipeline).to eq(head_pipeline) end @@ -656,6 +657,212 @@ describe Ci::CreatePipelineService do end end end + + describe 'Merge request pipelines' do + let(:pipeline) do + execute_service(source: source, merge_request: merge_request, ref: ref_name) + end + + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + end + + let(:ref_name) { 'feature' } + + context 'when source is merge request' do + let(:source) { :merge_request } + + context "when config has merge_requests keywords" do + let(:config) do + { + build: { + stage: 'build', + script: 'echo' + }, + test: { + stage: 'test', + script: 'echo', + only: ['merge_requests'] + }, + pages: { + stage: 'deploy', + script: 'echo', + except: ['merge_requests'] + } + } + end + + context 'when merge request is specified' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref_name, + target_project: project, + target_branch: 'master') + end + + it 'creates a merge request pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).to be_merge_request + expect(pipeline.merge_request).to eq(merge_request) + expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test]) + end + + context 'when ref is tag' do + let(:ref_name) { 'v1.1.0' } + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:tag]).to eq(["is not included in the list"]) + end + end + + context 'when merge request is created from a forked project' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref_name, + target_project: target_project, + target_branch: 'master') + end + + let!(:project) { fork_project(target_project, nil, repository: true) } + let!(:target_project) { create(:project, :repository) } + + it 'creates a merge request pipeline in the forked project' do + expect(pipeline).to be_persisted + expect(project.pipelines).to eq([pipeline]) + expect(target_project.pipelines).to be_empty + end + end + + context "when there are no matched jobs" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo', + except: ['merge_requests'] + } + } + end + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."]) + end + end + end + + context 'when merge request is not specified' do + let(:merge_request) { nil } + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:merge_request]).to eq(["can't be blank"]) + end + end + end + + context "when config does not have merge_requests keywords" do + let(:config) do + { + build: { + stage: 'build', + script: 'echo' + }, + test: { + stage: 'test', + script: 'echo' + }, + pages: { + stage: 'deploy', + script: 'echo' + } + } + end + + context 'when merge request is specified' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref_name, + target_project: project, + target_branch: 'master') + end + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + + expect(pipeline.errors[:base]) + .to eq(['No stages / jobs for this pipeline.']) + end + end + + context 'when merge request is not specified' do + let(:merge_request) { nil } + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + + expect(pipeline.errors[:base]) + .to eq(['No stages / jobs for this pipeline.']) + end + end + end + end + + context 'when source is web' do + let(:source) { :web } + + context "when config has merge_requests keywords" do + let(:config) do + { + build: { + stage: 'build', + script: 'echo' + }, + test: { + stage: 'test', + script: 'echo', + only: ['merge_requests'] + }, + pages: { + stage: 'deploy', + script: 'echo', + except: ['merge_requests'] + } + } + end + + context 'when merge request is specified' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref_name, + target_project: project, + target_branch: 'master') + end + + it 'does not create a merge request pipeline' do + expect(pipeline).not_to be_persisted + expect(pipeline.errors[:merge_request]).to eq(["must be blank"]) + end + end + + context 'when merge request is not specified' do + let(:merge_request) { nil } + + it 'creates a branch pipeline' do + expect(pipeline).to be_persisted + expect(pipeline).to be_web + expect(pipeline.merge_request).to be_nil + expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[build pages]) + end + end + end + end + end end describe '#execute!' do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 74bcc15f912..5a3ecb1019b 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -159,6 +159,78 @@ describe MergeRequests::CreateService do end end end + + describe 'Merge request pipelines' do + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + end + + context "when .gitlab-ci.yml has merge_requests keywords" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo', + only: ['merge_requests'] + } + } + end + + it 'creates a merge request pipeline and sets it as a head pipeline' do + expect(merge_request).to be_persisted + + merge_request.reload + expect(merge_request.merge_request_pipelines.count).to eq(1) + expect(merge_request.actual_head_pipeline).to be_merge_request + end + + context "when branch pipeline was created before a merge request pipline has been created" do + before do + create(:ci_pipeline, project: merge_request.source_project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + tag: false) + + merge_request + end + + it 'sets the latest merge request pipeline as the head pipeline' do + expect(merge_request.actual_head_pipeline).to be_merge_request + end + end + + context "when the 'ci_merge_request_pipeline' feature flag is disabled" do + before do + stub_feature_flags(ci_merge_request_pipeline: false) + end + + it 'does not create a merge request pipeline' do + expect(merge_request).to be_persisted + + merge_request.reload + expect(merge_request.merge_request_pipelines.count).to eq(0) + end + end + end + + context "when .gitlab-ci.yml does not have merge_requests keywords" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo' + } + } + end + + it 'does not create a merge request pipeline' do + expect(merge_request).to be_persisted + + merge_request.reload + expect(merge_request.merge_request_pipelines.count).to eq(0) + end + end + end end it_behaves_like 'new issuable record that supports quick actions' do diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 61c6ba7d550..d29a1091d95 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -132,6 +132,94 @@ describe MergeRequests::RefreshService do end end + describe 'Merge request pipelines' do + before do + stub_ci_pipeline_yaml_file(YAML.dump(config)) + end + + subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') } + + context "when .gitlab-ci.yml has merge_requests keywords" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo', + only: ['merge_requests'] + } + } + end + + it 'create merge request pipeline' do + expect { subject } + .to change { @merge_request.merge_request_pipelines.count }.by(1) + .and change { @fork_merge_request.merge_request_pipelines.count }.by(1) + .and change { @another_merge_request.merge_request_pipelines.count }.by(1) + end + + context "when branch pipeline was created before a merge request pipline has been created" do + before do + create(:ci_pipeline, project: @merge_request.source_project, + sha: @merge_request.diff_head_sha, + ref: @merge_request.source_branch, + tag: false) + + subject + end + + it 'sets the latest merge request pipeline as a head pipeline' do + @merge_request.reload + expect(@merge_request.actual_head_pipeline).to be_merge_request + end + + it 'returns pipelines in correct order' do + @merge_request.reload + expect(@merge_request.all_pipelines.first).to be_merge_request + expect(@merge_request.all_pipelines.second).to be_push + end + end + + context "when MergeRequestUpdateWorker is retried by an exception" do + it 'does not re-create a duplicate merge request pipeline' do + expect do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + end.to change { @merge_request.merge_request_pipelines.count }.by(1) + + expect do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') + end.not_to change { @merge_request.merge_request_pipelines.count } + end + end + + context "when the 'ci_merge_request_pipeline' feature flag is disabled" do + before do + stub_feature_flags(ci_merge_request_pipeline: false) + end + + it 'does not create a merge request pipeline' do + expect { subject } + .not_to change { @merge_request.merge_request_pipelines.count } + end + end + end + + context "when .gitlab-ci.yml does not have merge_requests keywords" do + let(:config) do + { + test: { + stage: 'test', + script: 'echo' + } + } + end + + it 'does not create a merge request pipeline' do + expect { subject } + .not_to change { @merge_request.merge_request_pipelines.count } + end + end + end + context 'push to origin repo source branch when an MR was reopened' do let(:refresh_service) { service.new(@project, @user) } let(:notification_service) { spy('notification_service') } diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index 9adde5fc21a..a2bc264b0f6 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -34,5 +34,33 @@ describe UpdateHeadPipelineForMergeRequestWorker do expect { subject.perform(merge_request.id) }.not_to change { merge_request.reload.head_pipeline_id } end end + + context 'when a merge request pipeline exists' do + let!(:merge_request_pipeline) do + create(:ci_pipeline, + project: project, + source: :merge_request, + sha: latest_sha, + merge_request: merge_request) + end + + it 'sets the merge request pipeline as the head pipeline' do + expect { subject.perform(merge_request.id) } + .to change { merge_request.reload.head_pipeline_id } + .from(nil).to(merge_request_pipeline.id) + end + + context 'when branch pipeline exists' do + let!(:branch_pipeline) do + create(:ci_pipeline, project: project, source: :push, sha: latest_sha) + end + + it 'prioritizes the merge request pipeline as the head pipeline' do + expect { subject.perform(merge_request.id) } + .to change { merge_request.reload.head_pipeline_id } + .from(nil).to(merge_request_pipeline.id) + end + end + end end end |