Commit 67a94b2f authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '37354-pipelines-update' into 'master'

Make sure head pippeline always corresponds with an MR

Closes #37354

See merge request gitlab-org/gitlab-ce!14358
parents f9229281 b328cff3
...@@ -283,15 +283,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -283,15 +283,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil) @merge_request.update(merge_error: nil)
if params[:merge_when_pipeline_succeeds].present? if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.head_pipeline return :failed unless @merge_request.actual_head_pipeline
if @merge_request.head_pipeline.active? if @merge_request.actual_head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService ::MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params) .new(@project, current_user, merge_params)
.execute(@merge_request) .execute(@merge_request)
:merge_when_pipeline_succeeds :merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success? elsif @merge_request.actual_head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while # This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time # the tests finish at about the same time
@merge_request.merge_async(current_user.id, params) @merge_request.merge_async(current_user.id, params)
......
...@@ -145,6 +145,13 @@ class MergeRequest < ActiveRecord::Base ...@@ -145,6 +145,13 @@ class MergeRequest < ActiveRecord::Base
'!' '!'
end end
# Use this method whenever you need to make sure the head_pipeline is synced with the
# branch head commit, for example checking if a merge request can be merged.
# For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
def actual_head_pipeline
head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
end
# Pattern used to extract `!123` merge request references from text # Pattern used to extract `!123` merge request references from text
# #
# This pattern supports cross-project references. # This pattern supports cross-project references.
...@@ -822,8 +829,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -822,8 +829,9 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state? def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds? return true unless project.only_allow_merge_if_pipeline_succeeds?
return true unless head_pipeline
!head_pipeline || head_pipeline.success? || head_pipeline.skipped? actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end end
def environments_for(current_user) def environments_for(current_user)
...@@ -997,7 +1005,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -997,7 +1005,7 @@ class MergeRequest < ActiveRecord::Base
return true if autocomplete_precheck return true if autocomplete_precheck
return false unless mergeable?(skip_ci_check: true) return false unless mergeable?(skip_ci_check: true)
return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?) return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
return false if last_diff_sha != diff_head_sha return false if last_diff_sha != diff_head_sha
true true
......
...@@ -163,7 +163,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -163,7 +163,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end end
def pipeline def pipeline
@pipeline ||= head_pipeline @pipeline ||= actual_head_pipeline
end end
def issues_sentence(project, issues) def issues_sentence(project, issues)
......
...@@ -33,7 +33,7 @@ class MergeRequestEntity < IssuableEntity ...@@ -33,7 +33,7 @@ class MergeRequestEntity < IssuableEntity
end end
expose :merge_commit_message expose :merge_commit_message
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
# Booleans # Booleans
expose :merge_ongoing?, as: :merge_ongoing expose :merge_ongoing?, as: :merge_ongoing
......
...@@ -29,7 +29,7 @@ module Ci ...@@ -29,7 +29,7 @@ module Ci
.new(pipeline, command, SEQUENCE) .new(pipeline, command, SEQUENCE)
sequence.build! do |pipeline, sequence| sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted? schedule_head_pipeline_update
if sequence.complete? if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines? cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
...@@ -38,15 +38,18 @@ module Ci ...@@ -38,15 +38,18 @@ module Ci
pipeline.process! pipeline.process!
end end
end end
pipeline
end end
private private
def update_merge_requests_head_pipeline def commit
return unless pipeline.latest? @commit ||= project.commit(origin_sha || origin_ref)
end
MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref) def sha
.update_all(head_pipeline_id: @pipeline.id) commit.try(:id)
end end
def cancel_pending_pipelines def cancel_pending_pipelines
...@@ -69,5 +72,15 @@ module Ci ...@@ -69,5 +72,15 @@ module Ci
@pipeline_created_counter ||= Gitlab::Metrics @pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created") .counter(:pipelines_created_total, "Counter of pipelines created")
end end
def schedule_head_pipeline_update
related_merge_requests.each do |merge_request|
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
def related_merge_requests
MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref)
end
end end
end end
...@@ -76,6 +76,7 @@ module MergeRequests ...@@ -76,6 +76,7 @@ module MergeRequests
end end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end end
end end
......
class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
sidekiq_options queue: 'pipeline_default'
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
return unless pipeline && pipeline.latest?
raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha
merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
end
---
title: Make sure head pippeline always corresponds with the head sha of an MR
merge_request:
author:
type: fixed
...@@ -324,12 +324,12 @@ describe Projects::MergeRequestsController do ...@@ -324,12 +324,12 @@ describe Projects::MergeRequestsController do
end end
context 'when the pipeline succeeds is passed' do context 'when the pipeline succeeds is passed' do
def merge_when_pipeline_succeeds let!(:head_pipeline) do
post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1') create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
end end
before do def merge_when_pipeline_succeeds
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end end
it 'returns :merge_when_pipeline_succeeds' do it 'returns :merge_when_pipeline_succeeds' do
...@@ -354,6 +354,18 @@ describe Projects::MergeRequestsController do ...@@ -354,6 +354,18 @@ describe Projects::MergeRequestsController do
project.update_column(:only_allow_merge_if_pipeline_succeeds, true) project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end end
context 'and head pipeline is not the current one' do
before do
head_pipeline.update(sha: 'not_current_sha')
end
it 'returns :failed' do
merge_when_pipeline_succeeds
expect(json_response).to eq('status' => 'failed')
end
end
it 'returns :merge_when_pipeline_succeeds' do it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds merge_when_pipeline_succeeds
......
...@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do ...@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do
end end
before do before do
visit project_merge_request_path(project, merge_request) merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end end
scenario 'user visits merge request pipelines tab' do scenario 'user visits merge request pipelines tab' do
visit project_merge_request_path(project, merge_request)
expect(page.find('.ci-widget')).to have_content('pending')
page.within('.merge-request-tabs') do page.within('.merge-request-tabs') do
click_link('Pipelines') click_link('Pipelines')
end end
...@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do ...@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do
expect(page).to have_selector('.stage-cell') expect(page).to have_selector('.stage-cell')
end end
scenario 'pipeline sha does not equal last commit sha' do
pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
visit project_merge_request_path(project, merge_request)
wait_for_requests
expect(page.find('.ci-widget')).to have_content(
'Could not connect to the CI server. Please check your settings and try again')
end
end end
context 'without pipelines' do context 'without pipelines' do
......
...@@ -827,16 +827,19 @@ describe MergeRequest do ...@@ -827,16 +827,19 @@ describe MergeRequest do
end end
end end
context 'head pipeline' do
before do
allow(subject).to receive(:diff_head_sha).and_return('lastsha')
end
describe '#head_pipeline' do describe '#head_pipeline' do
describe 'when the source project exists' do it 'returns nil for MR without head_pipeline_id' do
it 'returns the latest pipeline' do subject.update_attribute(:head_pipeline_id, nil)
pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc", head_pipeline_of: subject)
expect(subject.head_pipeline).to eq(pipeline) expect(subject.head_pipeline).to be_nil
end
end end
describe 'when the source project does not exist' do context 'when the source project does not exist' do
it 'returns nil' do it 'returns nil' do
allow(subject).to receive(:source_project).and_return(nil) allow(subject).to receive(:source_project).and_return(nil)
...@@ -845,6 +848,30 @@ describe MergeRequest do ...@@ -845,6 +848,30 @@ describe MergeRequest do
end end
end end
describe '#actual_head_pipeline' do
it 'returns nil for MR with old pipeline' do
pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
subject.update_attribute(:head_pipeline_id, pipeline.id)
expect(subject.actual_head_pipeline).to be_nil
end
it 'returns the pipeline for MR with recent pipeline' do
pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
subject.update_attribute(:head_pipeline_id, pipeline.id)
expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
expect(subject.actual_head_pipeline).to eq(pipeline)
end
it 'returns nil when source project does not exist' do
allow(subject).to receive(:source_project).and_return(nil)
expect(subject.actual_head_pipeline).to be_nil
end
end
end
describe '#has_ci?' do describe '#has_ci?' do
let(:merge_request) { build_stubbed(:merge_request) } let(:merge_request) { build_stubbed(:merge_request) }
...@@ -1179,7 +1206,7 @@ describe MergeRequest do ...@@ -1179,7 +1206,7 @@ describe MergeRequest do
context 'when it is only allowed to merge when build is green' do context 'when it is only allowed to merge when build is green' do
context 'and a failed pipeline is associated' do context 'and a failed pipeline is associated' do
before do before do
pipeline.update(status: 'failed') pipeline.update(status: 'failed', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline } allow(subject).to receive(:head_pipeline) { pipeline }
end end
...@@ -1188,7 +1215,7 @@ describe MergeRequest do ...@@ -1188,7 +1215,7 @@ describe MergeRequest do
context 'and a successful pipeline is associated' do context 'and a successful pipeline is associated' do
before do before do
pipeline.update(status: 'success') pipeline.update(status: 'success', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline } allow(subject).to receive(:head_pipeline) { pipeline }
end end
...@@ -1197,7 +1224,7 @@ describe MergeRequest do ...@@ -1197,7 +1224,7 @@ describe MergeRequest do
context 'and a skipped pipeline is associated' do context 'and a skipped pipeline is associated' do
before do before do
pipeline.update(status: 'skipped') pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline } allow(subject).to receive(:head_pipeline) { pipeline }
end end
......
...@@ -31,7 +31,7 @@ describe MergeRequestPresenter do ...@@ -31,7 +31,7 @@ describe MergeRequestPresenter do
let(:pipeline) { build_stubbed(:ci_pipeline) } let(:pipeline) { build_stubbed(:ci_pipeline) }
before do before do
allow(resource).to receive(:head_pipeline).and_return(pipeline) allow(resource).to receive(:actual_head_pipeline).and_return(pipeline)
end end
context 'success with warnings' do context 'success with warnings' do
......
...@@ -5,23 +5,35 @@ describe MergeRequestEntity do ...@@ -5,23 +5,35 @@ describe MergeRequestEntity do
let(:resource) { create(:merge_request, source_project: project, target_project: project) } let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:request) { double('request', current_user: user) } let(:request) { double('request', current_user: user, project: project) }
subject do subject do
described_class.new(resource, request: request).as_json described_class.new(resource, request: request).as_json
end end
it 'includes pipeline' do describe 'pipeline' do
req = double('request', current_user: user) let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
pipeline = build_stubbed(:ci_pipeline)
allow(resource).to receive(:head_pipeline).and_return(pipeline)
context 'when is up to date' do
let(:req) { double('request', current_user: user, project: project) }
it 'returns pipeline' do
pipeline_payload = PipelineDetailsEntity pipeline_payload = PipelineDetailsEntity
.represent(pipeline, request: req) .represent(pipeline, request: req)
.as_json .as_json
expect(subject[:pipeline]).to eq(pipeline_payload) expect(subject[:pipeline]).to eq(pipeline_payload)
end end
end
context 'when is not up to date' do
it 'returns nil' do
pipeline.update(sha: "not up to date")
expect(subject[:pipeline]).to be_nil
end
end
end
it 'includes issues_links' do it 'includes issues_links' do
issues_links = subject[:issues_links] issues_links = subject[:issues_links]
......
...@@ -57,20 +57,40 @@ describe Ci::CreatePipelineService do ...@@ -57,20 +57,40 @@ describe Ci::CreatePipelineService do
end end
context 'when merge requests already exist for this source branch' do context 'when merge requests already exist for this source branch' do
it 'updates head pipeline of each merge request' do let(:merge_request_1) do
merge_request_1 = create(:merge_request, source_branch: 'master', create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
target_branch: "branch_1", end
source_project: project) let(:merge_request_2) do
create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
end
merge_request_2 = create(:merge_request, source_branch: 'master', context 'when the head pipeline sha equals merge request sha' do
target_branch: "branch_2", it 'updates head pipeline of each merge request' do
source_project: project) merge_request_1
merge_request_2
head_pipeline = execute_service head_pipeline = execute_service
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline) expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline) expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
end end
end
context 'when the head pipeline sha does not equal merge request sha' do
it 'raises the ArgumentError error from worker and does not update the head piepeline of MRs' do
merge_request_1
merge_request_2
allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.to raise_error(ArgumentError)
last_pipeline = Ci::Pipeline.last
expect(merge_request_1.reload.head_pipeline).not_to eq(last_pipeline)
expect(merge_request_2.reload.head_pipeline).not_to eq(last_pipeline)
end
end
context 'when there is no pipeline for source branch' do context 'when there is no pipeline for source branch' do
it "does not update merge request head pipeline" do it "does not update merge request head pipeline" do
...@@ -106,8 +126,7 @@ describe Ci::CreatePipelineService do ...@@ -106,8 +126,7 @@ describe Ci::CreatePipelineService do
target_branch: "branch_1", target_branch: "branch_1",
source_project: project) source_project: project)
allow_any_instance_of(Ci::Pipeline) allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
.to receive(:latest?).and_return(false)
execute_service execute_service
......
...@@ -74,6 +74,20 @@ describe MergeRequests::RefreshService do ...@@ -74,6 +74,20 @@ describe MergeRequests::RefreshService do
end end
end end
context 'when pipeline exists for the source branch' do
let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)}
subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
it 'updates the head_pipeline_id for @merge_request' do
expect { subject }.to change { @merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id)
end
it 'does not update the head_pipeline_id for @fork_merge_request' do
expect { subject }.not_to change { @fork_merge_request.reload.head_pipeline_id }
end
end
context 'push to origin repo source branch when an MR was reopened' do context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) } let(:refresh_service) { service.new(@project, @user) }
......
require 'spec_helper'
describe UpdateHeadPipelineForMergeRequestWorker do
describe '#perform' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
context 'when pipeline exists for the source project and branch' do
before do
create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha)
end
it 'updates the head_pipeline_id of the merge_request' do
expect { subject.perform(merge_request.id) }.to change { merge_request.reload.head_pipeline_id }
end
context 'when merge request sha does not equal pipeline sha' do
before do
merge_request.merge_request_diff.update(head_commit_sha: 'different_sha')
end
it 'does not update head_pipeline_id' do
expect { subject.perform(merge_request.id) }.to raise_error(ArgumentError)
expect(merge_request.reload.head_pipeline_id).to eq(nil)
end
end
end
context 'when pipeline does not exist for the source project and branch' do
it 'does not update the head_pipeline_id of the merge_request' do
expect { subject.perform(merge_request.id) }.not_to change { merge_request.reload.head_pipeline_id }
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment