Merge branch 'feature/gb/cross-project-pipeline-trigger' into 'master'
Cross-project pipeline triggers /CE See merge request gitlab-org/gitlab-ce!24664
This commit is contained in:
commit
5ab285490a
|
@ -2,11 +2,13 @@
|
|||
|
||||
module Ci
|
||||
class Bridge < CommitStatus
|
||||
include Ci::Processable
|
||||
include Importable
|
||||
include AfterCommitQueue
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :trigger_request
|
||||
validates :ref, presence: true
|
||||
|
||||
def self.retry(bridge, current_user)
|
||||
|
@ -23,6 +25,21 @@ module Ci
|
|||
.fabricate!
|
||||
end
|
||||
|
||||
def schedulable?
|
||||
false
|
||||
end
|
||||
|
||||
def action?
|
||||
false
|
||||
end
|
||||
|
||||
def artifacts?
|
||||
false
|
||||
end
|
||||
|
||||
def expanded_environment_name
|
||||
end
|
||||
|
||||
def predefined_variables
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
@ -30,5 +47,9 @@ module Ci
|
|||
def execute_hooks
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def to_partial_path
|
||||
'projects/generic_commit_statuses/generic_commit_status'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Ci
|
||||
class Build < CommitStatus
|
||||
prepend ArtifactMigratable
|
||||
include Ci::Processable
|
||||
include TokenAuthenticatable
|
||||
include AfterCommitQueue
|
||||
include ObjectStorage::BackgroundMove
|
||||
|
@ -638,10 +639,6 @@ module Ci
|
|||
super || project.try(:build_coverage_regex)
|
||||
end
|
||||
|
||||
def when
|
||||
read_attribute(:when) || 'on_success'
|
||||
end
|
||||
|
||||
def options
|
||||
read_metadata_attribute(:options, :config_options, {})
|
||||
end
|
||||
|
|
|
@ -25,6 +25,8 @@ module Ci
|
|||
|
||||
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
|
||||
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :processables, -> { processables },
|
||||
class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :variables, class_name: 'Ci::PipelineVariable'
|
||||
|
|
|
@ -14,6 +14,7 @@ module Ci
|
|||
|
||||
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
|
||||
has_many :builds, foreign_key: :stage_id
|
||||
has_many :bridges, foreign_key: :stage_id
|
||||
|
||||
with_options unless: :importing? do
|
||||
validates :project, presence: true
|
||||
|
|
|
@ -41,6 +41,7 @@ class CommitStatus < ActiveRecord::Base
|
|||
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
|
||||
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
|
||||
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
|
||||
scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) }
|
||||
|
||||
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
|
||||
# extend this `Hash` with new values.
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
##
|
||||
# This module implements methods that need to be implemented by CI/CD
|
||||
# entities that are supposed to go through pipeline processing
|
||||
# services.
|
||||
#
|
||||
#
|
||||
module Processable
|
||||
def schedulable?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def action?
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def when
|
||||
read_attribute(:when) || 'on_success'
|
||||
end
|
||||
|
||||
def expanded_environment_name
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@ module Ci
|
|||
update_retried
|
||||
|
||||
new_builds =
|
||||
stage_indexes_of_created_builds.map do |index|
|
||||
stage_indexes_of_created_processables.map do |index|
|
||||
process_stage(index)
|
||||
end
|
||||
|
||||
|
@ -27,7 +27,7 @@ module Ci
|
|||
return if HasStatus::BLOCKED_STATUS.include?(current_status)
|
||||
|
||||
if HasStatus::COMPLETED_STATUSES.include?(current_status)
|
||||
created_builds_in_stage(index).select do |build|
|
||||
created_processables_in_stage(index).select do |build|
|
||||
Gitlab::OptimisticLocking.retry_lock(build) do |subject|
|
||||
Ci::ProcessBuildService.new(project, @user)
|
||||
.execute(build, current_status)
|
||||
|
@ -43,19 +43,19 @@ module Ci
|
|||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def stage_indexes_of_created_builds
|
||||
created_builds.order(:stage_idx).pluck('distinct stage_idx')
|
||||
def stage_indexes_of_created_processables
|
||||
created_processables.order(:stage_idx).pluck('distinct stage_idx')
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def created_builds_in_stage(index)
|
||||
created_builds.where(stage_idx: index)
|
||||
def created_processables_in_stage(index)
|
||||
created_processables.where(stage_idx: index)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def created_builds
|
||||
pipeline.builds.created
|
||||
def created_processables
|
||||
pipeline.processables.created
|
||||
end
|
||||
|
||||
# This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab
|
||||
|
|
|
@ -67,7 +67,7 @@ module Gitlab
|
|||
|
||||
entry :only, Entry::Policy,
|
||||
description: 'Refs policy this job will be executed for.',
|
||||
default: { refs: %w[branches tags] }
|
||||
default: Entry::Policy::DEFAULT_ONLY
|
||||
|
||||
entry :except, Entry::Policy,
|
||||
description: 'Refs policy this job will be executed for.'
|
||||
|
|
|
@ -28,11 +28,15 @@ module Gitlab
|
|||
name.to_s.start_with?('.')
|
||||
end
|
||||
|
||||
def node_type(name)
|
||||
hidden?(name) ? Entry::Hidden : Entry::Job
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def compose!(deps = nil)
|
||||
super do
|
||||
@config.each do |name, config|
|
||||
node = hidden?(name) ? Entry::Hidden : Entry::Job
|
||||
node = node_type(name)
|
||||
|
||||
factory = ::Gitlab::Config::Entry::Factory.new(node)
|
||||
.value(config || {})
|
||||
|
|
|
@ -11,6 +11,8 @@ module Gitlab
|
|||
strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
|
||||
strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
|
||||
|
||||
DEFAULT_ONLY = { refs: %w[branches tags] }.freeze
|
||||
|
||||
class RefsPolicy < ::Gitlab::Config::Entry::Node
|
||||
include ::Gitlab::Config::Entry::Validatable
|
||||
|
||||
|
|
|
@ -38,9 +38,17 @@ module Gitlab
|
|||
)
|
||||
end
|
||||
|
||||
def bridge?
|
||||
@attributes.to_h.dig(:options, :trigger).present?
|
||||
end
|
||||
|
||||
def to_resource
|
||||
strong_memoize(:resource) do
|
||||
::Ci::Build.new(attributes)
|
||||
if bridge?
|
||||
::Ci::Bridge.new(attributes)
|
||||
else
|
||||
::Ci::Build.new(attributes)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,7 +39,13 @@ module Gitlab
|
|||
def to_resource
|
||||
strong_memoize(:stage) do
|
||||
::Ci::Stage.new(attributes).tap do |stage|
|
||||
seeds.each { |seed| stage.builds << seed.to_resource }
|
||||
seeds.each do |seed|
|
||||
if seed.bridge?
|
||||
stage.bridges << seed.to_resource
|
||||
else
|
||||
stage.builds << seed.to_resource
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,6 @@ module Gitlab
|
|||
end
|
||||
|
||||
def details_path
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ module Gitlab
|
|||
|
||||
{ stage_idx: @stages.index(job[:stage]),
|
||||
stage: job[:stage],
|
||||
tag_list: job[:tags] || [],
|
||||
tag_list: job[:tags],
|
||||
name: job[:name].to_s,
|
||||
allow_failure: job[:ignore],
|
||||
when: job[:when] || 'on_success',
|
||||
|
@ -53,8 +53,9 @@ module Gitlab
|
|||
retry: job[:retry],
|
||||
parallel: job[:parallel],
|
||||
instance: job[:instance],
|
||||
start_in: job[:start_in]
|
||||
}.compact }
|
||||
start_in: job[:start_in],
|
||||
trigger: job[:trigger]
|
||||
}.compact }.compact
|
||||
end
|
||||
|
||||
def stage_builds_attributes(stage)
|
||||
|
|
|
@ -10,8 +10,16 @@ FactoryBot.define do
|
|||
|
||||
pipeline factory: :ci_pipeline
|
||||
|
||||
transient { downstream nil }
|
||||
|
||||
after(:build) do |bridge, evaluator|
|
||||
bridge.project ||= bridge.pipeline.project
|
||||
|
||||
if evaluator.downstream.present?
|
||||
bridge.options = bridge.options.to_h.merge(
|
||||
trigger: { project: evaluator.downstream.full_path }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -286,6 +286,49 @@ describe 'Pipeline', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when a bridge job exists' do
|
||||
include_context 'pipeline builds'
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:downstream) { create(:project, :repository) }
|
||||
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline, project: project,
|
||||
ref: 'master',
|
||||
sha: project.commit.id,
|
||||
user: user)
|
||||
end
|
||||
|
||||
let!(:bridge) do
|
||||
create(:ci_bridge, pipeline: pipeline,
|
||||
name: 'cross-build',
|
||||
user: user,
|
||||
downstream: downstream)
|
||||
end
|
||||
|
||||
describe 'GET /:project/pipelines/:id' do
|
||||
before do
|
||||
visit project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
it 'shows the pipeline with a bridge job' do
|
||||
expect(page).to have_selector('.pipeline-visualization')
|
||||
expect(page).to have_content('cross-build')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /:project/pipelines/:id/builds' do
|
||||
before do
|
||||
visit builds_project_pipeline_path(project, pipeline)
|
||||
end
|
||||
|
||||
it 'shows a bridge job on a list' do
|
||||
expect(page).to have_content('cross-build')
|
||||
expect(page).to have_content(bridge.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /:project/pipelines/:id/builds' do
|
||||
include_context 'pipeline builds'
|
||||
|
||||
|
|
|
@ -163,14 +163,14 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
|
|||
->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
|
||||
end
|
||||
|
||||
it 'raises exception' do
|
||||
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
|
||||
end
|
||||
|
||||
it 'wastes pipeline iid' do
|
||||
expect { step.perform! }.to raise_error
|
||||
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
|
||||
|
||||
expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0
|
||||
last_iid = InternalId.ci_pipelines
|
||||
.where(project_id: project.id)
|
||||
.last.last_value
|
||||
|
||||
expect(last_iid).to be > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
|
|||
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
|
||||
|
||||
let(:attributes) do
|
||||
{ name: 'rspec',
|
||||
ref: 'master' }
|
||||
{ name: 'rspec', ref: 'master' }
|
||||
end
|
||||
|
||||
subject do
|
||||
|
@ -21,10 +20,45 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#bridge?' do
|
||||
context 'when job is a bridge' do
|
||||
let(:attributes) do
|
||||
{ name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
|
||||
end
|
||||
|
||||
it { is_expected.to be_bridge }
|
||||
end
|
||||
|
||||
context 'when trigger definition is empty' do
|
||||
let(:attributes) do
|
||||
{ name: 'rspec', ref: 'master', options: { trigger: '' } }
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_bridge }
|
||||
end
|
||||
|
||||
context 'when job is not a bridge' do
|
||||
it { is_expected.not_to be_bridge }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_resource' do
|
||||
it 'returns a valid build resource' do
|
||||
expect(subject.to_resource).to be_a(::Ci::Build)
|
||||
expect(subject.to_resource).to be_valid
|
||||
context 'when job is not a bridge' do
|
||||
it 'returns a valid build resource' do
|
||||
expect(subject.to_resource).to be_a(::Ci::Build)
|
||||
expect(subject.to_resource).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when job is a bridge' do
|
||||
let(:attributes) do
|
||||
{ name: 'rspec', ref: 'master', options: { trigger: 'my/project' } }
|
||||
end
|
||||
|
||||
it 'returns a valid bridge resource' do
|
||||
expect(subject.to_resource).to be_a(::Ci::Bridge)
|
||||
expect(subject.to_resource).to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it 'memoizes a resource object' do
|
||||
|
|
|
@ -62,8 +62,18 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
|
|||
expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
|
||||
expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
|
||||
expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
|
||||
expect(subject.seeds.map(&:attributes))
|
||||
.to all(include(trigger_request: pipeline.trigger_requests.first))
|
||||
end
|
||||
|
||||
context 'when a legacy trigger exists' do
|
||||
before do
|
||||
create(:ci_trigger_request, pipeline: pipeline)
|
||||
end
|
||||
|
||||
it 'returns build seeds including legacy trigger' do
|
||||
expect(pipeline.legacy_trigger).not_to be_nil
|
||||
expect(subject.seeds.map(&:attributes))
|
||||
.to all(include(trigger_request: pipeline.legacy_trigger))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a ref is protected' do
|
||||
|
|
|
@ -21,15 +21,12 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"]
|
||||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -154,12 +151,9 @@ module Gitlab
|
|||
builds:
|
||||
[{ stage_idx: 1,
|
||||
stage: "test",
|
||||
tag_list: [],
|
||||
name: "rspec",
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
coverage_regex: nil,
|
||||
yaml_variables: [],
|
||||
options: { script: ["rspec"] },
|
||||
only: { refs: ["branches"] },
|
||||
|
@ -169,12 +163,9 @@ module Gitlab
|
|||
builds:
|
||||
[{ stage_idx: 2,
|
||||
stage: "deploy",
|
||||
tag_list: [],
|
||||
name: "prod",
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
coverage_regex: nil,
|
||||
yaml_variables: [],
|
||||
options: { script: ["cap prod"] },
|
||||
only: { refs: ["tags"] },
|
||||
|
@ -344,8 +335,6 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
|
@ -356,7 +345,6 @@ module Gitlab
|
|||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -378,8 +366,6 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
|
@ -390,7 +376,6 @@ module Gitlab
|
|||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -410,8 +395,6 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
|
@ -420,7 +403,6 @@ module Gitlab
|
|||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -438,8 +420,6 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
|
@ -448,7 +428,6 @@ module Gitlab
|
|||
},
|
||||
allow_failure: false,
|
||||
when: "on_success",
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -763,8 +742,6 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "rspec",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
|
@ -779,7 +756,6 @@ module Gitlab
|
|||
},
|
||||
when: "on_success",
|
||||
allow_failure: false,
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -976,14 +952,11 @@ module Gitlab
|
|||
stage: "test",
|
||||
stage_idx: 1,
|
||||
name: "normal_job",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
script: ["test"]
|
||||
},
|
||||
when: "on_success",
|
||||
allow_failure: false,
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
@ -1023,28 +996,22 @@ module Gitlab
|
|||
stage: "build",
|
||||
stage_idx: 0,
|
||||
name: "job1",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
script: ["execute-script-for-job"]
|
||||
},
|
||||
when: "on_success",
|
||||
allow_failure: false,
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
expect(subject.second).to eq({
|
||||
stage: "build",
|
||||
stage_idx: 0,
|
||||
name: "job2",
|
||||
coverage_regex: nil,
|
||||
tag_list: [],
|
||||
options: {
|
||||
script: ["execute-script-for-job"]
|
||||
},
|
||||
when: "on_success",
|
||||
allow_failure: false,
|
||||
environment: nil,
|
||||
yaml_variables: []
|
||||
})
|
||||
end
|
||||
|
|
|
@ -114,6 +114,7 @@ ci_pipelines:
|
|||
- stages
|
||||
- statuses
|
||||
- builds
|
||||
- processables
|
||||
- trigger_requests
|
||||
- variables
|
||||
- auto_canceled_by
|
||||
|
@ -137,6 +138,7 @@ stages:
|
|||
- pipeline
|
||||
- statuses
|
||||
- builds
|
||||
- bridges
|
||||
statuses:
|
||||
- project
|
||||
- pipeline
|
||||
|
|
|
@ -39,6 +39,29 @@ describe Ci::Pipeline, :mailer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.processables' do
|
||||
before do
|
||||
create(:ci_build, name: 'build', pipeline: pipeline)
|
||||
create(:ci_bridge, name: 'bridge', pipeline: pipeline)
|
||||
create(:commit_status, name: 'commit status', pipeline: pipeline)
|
||||
create(:generic_commit_status, name: 'generic status', pipeline: pipeline)
|
||||
end
|
||||
|
||||
it 'has an association with processable CI/CD entities' do
|
||||
pipeline.processables.pluck('name').yield_self do |processables|
|
||||
expect(processables).to match_array %w[build bridge]
|
||||
end
|
||||
end
|
||||
|
||||
it 'makes it possible to append a new processable' do
|
||||
pipeline.processables << build(:ci_bridge)
|
||||
|
||||
pipeline.save!
|
||||
|
||||
expect(pipeline.processables.reload.count).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
describe '.sort_by_merge_request_pipelines' do
|
||||
subject { described_class.sort_by_merge_request_pipelines }
|
||||
|
||||
|
|
Loading…
Reference in New Issue