Skip to content
Snippets Groups Projects
Commit e62bfc78 authored by Shinya Maeda's avatar Shinya Maeda
Browse files

Merge request pipelines

parent 23d92198
No related branches found
No related tags found
No related merge requests found
Showing
with 498 additions and 16 deletions
Loading
Loading
@@ -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
Loading
Loading
@@ -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?
 
Loading
Loading
@@ -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
Loading
Loading
@@ -372,7 +383,7 @@ module Ci
end
 
def branch?
!tag?
!tag? && !merge_request?
end
 
def stuck?
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -21,7 +21,8 @@ module Ci
trigger: 3,
schedule: 4,
api: 5,
external: 6
external: 6,
merge_request: 9
}
end
end
Loading
Loading
Loading
Loading
@@ -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"
 
Loading
Loading
@@ -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?
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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(
Loading
Loading
@@ -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,
Loading
Loading
Loading
Loading
@@ -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])
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
Loading
Loading
@@ -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?
 
Loading
Loading
@@ -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(
Loading
Loading
---
title: Merge request pipelines
merge_request: 23217
author:
type: added
# 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
# 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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
)
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
# 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
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -121,6 +122,7 @@ pipelines:
- artifacts
- pipeline_schedule
- merge_requests
- merge_request
- deployments
- environments
pipeline_variables:
Loading
Loading
Loading
Loading
@@ -243,6 +243,7 @@ Ci::Pipeline:
- failure_reason
- protected
- iid
- merge_request_id
Ci::Stage:
- id
- name
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment