Commit 4eed16cf authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'feature/improve-mrwbs-and-todos-for-pipelines' into 'master'

Trigger Merge When Pipeline Succeeds on pipeline event

## What does this MR do?

This MR is meant to improve merge when build succeeds triggers, which has an impact on performance.

- [x] Move Merge When Build Succeeds trigger from commit status to pipeline event
- [x] Drop support for triggering event for branches that include commit status submitted without branch (no longer relevant)
- [x] Perform Merge When Pipeline Succeeds asynchronously to improve performance and avoid race conditions
- [x] Add missing feature test that verifies if MWBS feature actually works and merges merge requests
- [x] Update the documentation to reflect change in the behavior

Moved to separate merge request:

- [ ] Rename Merge When Build Succeeds to Merge When Pipeline Succeeds
- [ ] Update documentation to reflect name change for this feature

## Does this MR meet the acceptance criteria?

- [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG) entry added
- Tests
  - [x] Added for this feature/bug
  - [x] All builds are passing

See merge request !6675
parents 2208a878 2f2ad9ea
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel) - Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page - Truncate long labels with ellipsis in labels page
- Adding members no longer silently fails when there is extra whitespace - Adding members no longer silently fails when there is extra whitespace
......
...@@ -3,6 +3,7 @@ module Ci ...@@ -3,6 +3,7 @@ module Ci
extend Ci::Model extend Ci::Model
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue
self.table_name = 'ci_commits' self.table_name = 'ci_commits'
...@@ -56,6 +57,10 @@ module Ci ...@@ -56,6 +57,10 @@ module Ci
pipeline.finished_at = Time.now pipeline.finished_at = Time.now
end end
before_transition do |pipeline|
pipeline.update_duration
end
after_transition [:created, :pending] => :running do |pipeline| after_transition [:created, :pending] => :running do |pipeline|
MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)). MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil) update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
...@@ -66,8 +71,8 @@ module Ci ...@@ -66,8 +71,8 @@ module Ci
update_all(latest_build_finished_at: pipeline.finished_at) update_all(latest_build_finished_at: pipeline.finished_at)
end end
before_transition do |pipeline| after_transition [:created, :pending, :running] => :success do |pipeline|
pipeline.update_duration pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
end end
after_transition do |pipeline, transition| after_transition do |pipeline, transition|
...@@ -292,11 +297,9 @@ module Ci ...@@ -292,11 +297,9 @@ module Ci
# Merge requests for which the current pipeline is running against # Merge requests for which the current pipeline is running against
# the merge request's latest commit. # the merge request's latest commit.
def merge_requests def merge_requests
@merge_requests ||= @merge_requests ||= project.merge_requests
begin .where(source_branch: self.ref)
project.merge_requests.where(source_branch: self.ref). .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
select { |merge_request| merge_request.pipeline.try(:id) == self.id }
end
end end
private private
......
...@@ -86,29 +86,19 @@ class CommitStatus < ActiveRecord::Base ...@@ -86,29 +86,19 @@ class CommitStatus < ActiveRecord::Base
end end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
return if transition.loopback? next if transition.loopback?
commit_status.run_after_commit do commit_status.run_after_commit do
pipeline.try do |pipeline| pipeline.try do |pipeline|
if complete? if complete?
ProcessPipelineWorker.perform_async(pipeline.id) PipelineProcessWorker.perform_async(pipeline.id)
else else
UpdatePipelineWorker.perform_async(pipeline.id) PipelineUpdateWorker.perform_async(pipeline.id)
end end
end end
end end
end end
after_transition [:created, :pending, :running] => :success do |commit_status|
commit_status.run_after_commit do
# TODO, temporary fix for race condition
UpdatePipelineWorker.new.perform(pipeline.id)
MergeRequests::MergeWhenBuildSucceedsService
.new(pipeline.project, nil).trigger(self)
end
end
after_transition any => :failed do |commit_status| after_transition any => :failed do |commit_status|
commit_status.run_after_commit do commit_status.run_after_commit do
MergeRequests::AddTodoWhenBuildFailsService MergeRequests::AddTodoWhenBuildFailsService
......
...@@ -2,14 +2,14 @@ module MergeRequests ...@@ -2,14 +2,14 @@ module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails # Adds a todo to the parent merge_request when a CI build fails
def execute(commit_status) def execute(commit_status)
each_merge_request(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request) todo_service.merge_request_build_failed(merge_request)
end end
end end
# Closes any pending build failed todos for the parent MRs when a build is retried # Closes any pending build failed todos for the parent MRs when a build is retried
def close(commit_status) def close(commit_status)
each_merge_request(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request) todo_service.merge_request_build_retried(merge_request)
end end
end end
......
...@@ -42,28 +42,33 @@ module MergeRequests ...@@ -42,28 +42,33 @@ module MergeRequests
super(:merge_request) super(:merge_request)
end end
def merge_request_from(commit_status) def merge_requests_for(branch)
branches = commit_status.ref origin_merge_requests = @project.origin_merge_requests
.opened.where(source_branch: branch).to_a
# This is for ref-less builds fork_merge_requests = @project.fork_merge_requests
branches ||= @project.repository.branch_names_contains(commit_status.sha) .opened.where(source_branch: branch).to_a
return [] if branches.blank? (origin_merge_requests + fork_merge_requests)
.uniq.select(&:source_project)
end
merge_requests = @project.origin_merge_requests.opened.where(source_branch: branches).to_a def pipeline_merge_requests(pipeline)
merge_requests += @project.fork_merge_requests.opened.where(source_branch: branches).to_a merge_requests_for(pipeline.ref).each do |merge_request|
next unless pipeline == merge_request.pipeline
merge_requests.uniq.select(&:source_project) yield merge_request
end
end end
def each_merge_request(commit_status) def commit_status_merge_requests(commit_status)
merge_request_from(commit_status).each do |merge_request| merge_requests_for(commit_status.ref).each do |merge_request|
pipeline = merge_request.pipeline pipeline = merge_request.pipeline
next unless pipeline next unless pipeline
next unless pipeline.sha == commit_status.sha next unless pipeline.sha == commit_status.sha
yield merge_request, pipeline yield merge_request
end end
end end
end end
......
...@@ -18,12 +18,13 @@ module MergeRequests ...@@ -18,12 +18,13 @@ module MergeRequests
merge_request.save merge_request.save
end end
# Triggers the automatic merge of merge_request once the build succeeds # Triggers the automatic merge of merge_request once the pipeline succeeds
def trigger(commit_status) def trigger(pipeline)
each_merge_request(commit_status) do |merge_request, pipeline| return unless pipeline.success?
pipeline_merge_requests(pipeline) do |merge_request|
next unless merge_request.merge_when_build_succeeds? next unless merge_request.merge_when_build_succeeds?
next unless merge_request.mergeable? next unless merge_request.mergeable?
next unless pipeline.success?
MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
end end
......
class ProcessPipelineWorker class PipelineProcessWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: :default sidekiq_options queue: :default
......
class PipelineSuccessWorker
include Sidekiq::Worker
sidekiq_options queue: :default
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
MergeRequests::MergeWhenBuildSucceedsService
.new(pipeline.project, nil)
.trigger(pipeline)
end
end
end
class UpdatePipelineWorker class PipelineUpdateWorker
include Sidekiq::Worker include Sidekiq::Worker
sidekiq_options queue: :default sidekiq_options queue: :default
......
# Merge When Build Succeeds # Merge When Build Succeeds
When reviewing a merge request that looks ready to merge but still has one or When reviewing a merge request that looks ready to merge but still has one or
more CI builds running, you can set it to be merged automatically when all more CI builds running, you can set it to be merged automatically when the
builds succeed. This way, you don't have to wait for the builds to finish and builds pipeline succeed. This way, you don't have to wait for the builds to
remember to merge the request manually. finish and remember to merge the request manually.
![Enable](img/merge_when_build_succeeds_enable.png) ![Enable](img/merge_when_build_succeeds_enable.png)
When you hit the "Merge When Build Succeeds" button, the status of the merge When you hit the "Merge When Build Succeeds" button, the status of the merge
request will be updated to represent the impending merge. If you cannot wait request will be updated to represent the impending merge. If you cannot wait
for the build to succeed and want to merge immediately, this option is available for the pipeline to succeed and want to merge immediately, this option is
in the dropdown menu on the right of the main button. available in the dropdown menu on the right of the main button.
Both team developers and the author of the merge request have the option to Both team developers and the author of the merge request have the option to
cancel the automatic merge if they find a reason why it shouldn't be merged cancel the automatic merge if they find a reason why it shouldn't be merged
...@@ -18,9 +18,9 @@ after all. ...@@ -18,9 +18,9 @@ after all.
![Status](img/merge_when_build_succeeds_status.png) ![Status](img/merge_when_build_succeeds_status.png)
When the build succeeds, the merge request will automatically be merged. When When the pipeline succeeds, the merge request will automatically be merged.
the build fails, the author gets a chance to retry any failed builds, or to When the pipeline fails, the author gets a chance to retry any failed builds,
push new commits to fix the failure. or to push new commits to fix the failure.
When the builds are retried and succeed on the second try, the merge request When the builds are retried and succeed on the second try, the merge request
will automatically be merged after all. When the merge request is updated with will automatically be merged after all. When the merge request is updated with
...@@ -40,7 +40,7 @@ hit **Save** for the changes to take effect. ...@@ -40,7 +40,7 @@ hit **Save** for the changes to take effect.
![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png) ![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
From now on, every time the build fails you will not be able to merge the merge From now on, every time the pipelinefails you will not be able to merge the
request from the UI, until you make the build pass. merge request from the UI, until you make all relevant builds pass.
![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png) ![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
...@@ -2,18 +2,26 @@ require 'spec_helper' ...@@ -2,18 +2,26 @@ require 'spec_helper'
feature 'Merge When Build Succeeds', feature: true, js: true do feature 'Merge When Build Succeeds', feature: true, js: true do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
before do let(:merge_request) do
project.team << [user, :master] create(:merge_request_with_diffs, source_project: project,
project.enable_ci author: user,
title: 'Bug NS-04')
end end
context "Active build for Merge Request" do let(:pipeline) do
let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } create(:ci_pipeline, project: project,
let!(:ci_build) { create(:ci_build, pipeline: pipeline) } sha: merge_request.diff_head_sha,
ref: merge_request.source_branch)
end
before { project.team << [user, :master] }
context 'when there is active build for merge request' do
background do
create(:ci_build, pipeline: pipeline)
end
before do before do
login_as user login_as user
...@@ -41,26 +49,30 @@ feature 'Merge When Build Succeeds', feature: true, js: true do ...@@ -41,26 +49,30 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end end
end end
context 'When it is enabled' do context 'when merge when build succeeds is enabled' do
let(:merge_request) do let(:merge_request) do
create(:merge_request_with_diffs, :simple, source_project: project, author: user, create(:merge_request_with_diffs, :simple, source_project: project,
merge_user: user, title: "MepMep", merge_when_build_succeeds: true) author: user,
merge_user: user,
title: 'MepMep',
merge_when_build_succeeds: true)
end end
let!(:pipeline) { create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) } let!(:build) do
let!(:ci_build) { create(:ci_build, pipeline: pipeline) } create(:ci_build, pipeline: pipeline)
end
before do before do
login_as user login_as user
visit_merge_request(merge_request) visit_merge_request(merge_request)
end end
it 'cancels the automatic merge' do it 'allows to cancel the automatic merge' do
click_link "Cancel Automatic Merge" click_link "Cancel Automatic Merge"
expect(page).to have_button "Merge When Build Succeeds" expect(page).to have_button "Merge When Build Succeeds"
visit_merge_request(merge_request) # Needed to refresh the page visit_merge_request(merge_request) # refresh the page
expect(page).to have_content "Canceled the automatic merge" expect(page).to have_content "Canceled the automatic merge"
end end
...@@ -70,10 +82,21 @@ feature 'Merge When Build Succeeds', feature: true, js: true do ...@@ -70,10 +82,21 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
click_link "Remove Source Branch When Merged" click_link "Remove Source Branch When Merged"
expect(page).to have_content "The source branch will be removed" expect(page).to have_content "The source branch will be removed"
end end
context 'when build succeeds' do
background { build.success }
it 'merges merge request' do
visit_merge_request(merge_request) # refresh the page
expect(page).to have_content 'The changes were merged'
expect(merge_request.reload).to be_merged
end
end
end end
context 'Build is not active' do context 'when build is not active' do
it "does not allow for enabling" do it "does not allow to enable merge when build succeeds" do
visit_merge_request(merge_request) visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds" expect(page).not_to have_link "Merge When Build Succeeds"
end end
......
...@@ -58,57 +58,67 @@ describe MergeRequests::MergeWhenBuildSucceedsService do ...@@ -58,57 +58,67 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end end
describe "#trigger" do describe "#trigger" do
context 'build with ref' do let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } let(:merge_request_head) do
project.commit(mr_merge_if_green_enabled.source_branch).id
end
it "merges all merge requests with merge when build succeeds enabled" do context 'when triggered by pipeline with valid ref and sha' do
allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline) let(:triggering_pipeline) do
allow(pipeline).to receive(:success?).and_return(true) create(:ci_pipeline, project: project, ref: merge_request_ref,
sha: merge_request_head, status: 'success')
end
it "merges all merge requests with merge when build succeeds enabled" do
expect(MergeWorker).to receive(:perform_async) expect(MergeWorker).to receive(:perform_async)
service.trigger(build) service.trigger(triggering_pipeline)
end end
end end
context 'triggered by an old build' do context 'when triggered by an old pipeline' do
let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } let(:old_pipeline) do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } create(:ci_pipeline, project: project, ref: merge_request_ref,
sha: '1234abcdef', status: 'success')
it "merges all merge requests with merge when build succeeds enabled" do end
allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
allow(pipeline).to receive(:success?).and_return(true)
allow(old_build).to receive(:sha).and_return('1234abcdef')
it 'it does not merge merge request' do
expect(MergeWorker).not_to receive(:perform_async) expect(MergeWorker).not_to receive(:perform_async)
service.trigger(old_build) service.trigger(old_pipeline)
end end
end end
context 'commit status without ref' do context 'when triggered by pipeline from a different branch' do
let(:commit_status) { create(:generic_commit_status, status: 'success') } let(:unrelated_pipeline) do
create(:ci_pipeline, project: project, ref: 'feature',
before { mr_merge_if_green_enabled } sha: merge_request_head, status: 'success')
end
it "doesn't merge a requests for status on other branch" do
allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([])
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async) expect(MergeWorker).not_to receive(:perform_async)
service.trigger(commit_status) service.trigger(unrelated_pipeline)
end
end
end end
it 'discovers branches and merges all merge requests when status is success' do describe "#cancel" do
allow(project.repository).to receive(:branch_names_contains). before do
with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch]) service.cancel(mr_merge_if_green_enabled)
allow(pipeline).to receive(:success?).and_return(true) end
allow_any_instance_of(MergeRequest).to receive(:pipeline).and_return(pipeline)
allow(pipeline).to receive(:success?).and_return(true)
expect(MergeWorker).to receive(:perform_async) it "resets all the merge_when_build_succeeds params" do
service.trigger(commit_status) expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
expect(mr_merge_if_green_enabled.merge_params).to eq({})
expect(mr_merge_if_green_enabled.merge_user).to be nil
end
it 'Posts a system note' do
note = mr_merge_if_green_enabled.notes.last
expect(note.note).to include 'Canceled the automatic merge'
end end
end end
context 'properly handles multiple stages' do describe 'pipeline integration' do
context 'when there are multiple stages in the pipeline' do
let(:ref) { mr_merge_if_green_enabled.source_branch } let(:ref) { mr_merge_if_green_enabled.source_branch }
let(:sha) { project.commit(ref).id } let(:sha) { project.commit(ref).id }
...@@ -148,21 +158,4 @@ describe MergeRequests::MergeWhenBuildSucceedsService do ...@@ -148,21 +158,4 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end end
end end
end end
describe "#cancel" do
before do
service.cancel(mr_merge_if_green_enabled)
end
it "resets all the merge_when_build_succeeds params" do
expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
expect(mr_merge_if_green_enabled.merge_params).to eq({})
expect(mr_merge_if_green_enabled.merge_user).to be nil
end
it 'Posts a system note' do
note = mr_merge_if_green_enabled.notes.last
expect(note.note).to include 'Canceled the automatic merge'
end
end
end end
require 'spec_helper' require 'spec_helper'
describe ProcessPipelineWorker do describe PipelineProcessWorker do
describe '#perform' do describe '#perform' do
context 'when pipeline exists' do context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) } let(:pipeline) { create(:ci_pipeline) }
......
require 'spec_helper'
describe PipelineSuccessWorker do
describe '#perform' do
context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline, status: 'success') }
it 'performs "merge when pipeline succeeds"' do
expect_any_instance_of(
MergeRequests::MergeWhenBuildSucceedsService
).to receive(:trigger)
described_class.new.perform(pipeline.id)
end
end
context 'when pipeline does not exist' do
it 'does not raise exception' do
expect { described_class.new.perform(123) }
.not_to raise_error
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe UpdatePipelineWorker do describe PipelineUpdateWorker do
describe '#perform' do describe '#perform' do
context 'when pipeline exists' do context 'when pipeline exists' do
let(:pipeline) { create(:ci_pipeline) } let(:pipeline) { create(:ci_pipeline) }
......
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