Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/services/ci/create_downstream_pipeline_service_spec.rb')
-rw-r--r--spec/services/ci/create_downstream_pipeline_service_spec.rb545
1 files changed, 545 insertions, 0 deletions
diff --git a/spec/services/ci/create_downstream_pipeline_service_spec.rb b/spec/services/ci/create_downstream_pipeline_service_spec.rb
new file mode 100644
index 00000000000..f26f9eeb6f0
--- /dev/null
+++ b/spec/services/ci/create_downstream_pipeline_service_spec.rb
@@ -0,0 +1,545 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
+ let_it_be(:user) { create(:user) }
+ let(:upstream_project) { create(:project, :repository) }
+ let_it_be(:downstream_project) { create(:project, :repository) }
+
+ let!(:upstream_pipeline) do
+ create(:ci_pipeline, :running, project: upstream_project)
+ end
+
+ let(:trigger) do
+ {
+ trigger: {
+ project: downstream_project.full_path,
+ branch: 'feature'
+ }
+ }
+ end
+
+ let(:bridge) do
+ create(:ci_bridge, status: :pending,
+ user: user,
+ options: trigger,
+ pipeline: upstream_pipeline)
+ end
+
+ let(:service) { described_class.new(upstream_project, user) }
+
+ before do
+ upstream_project.add_developer(user)
+ end
+
+ context 'when downstream project has not been found' do
+ let(:trigger) do
+ { trigger: { project: 'unknown/project' } }
+ end
+
+ it 'does not create a pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'changes pipeline bridge job status to failed' do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason)
+ .to eq 'downstream_bridge_project_not_found'
+ end
+ end
+
+ context 'when user can not access downstream project' do
+ it 'does not create a new pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'changes status of the bridge build' do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason)
+ .to eq 'downstream_bridge_project_not_found'
+ end
+ end
+
+ context 'when user does not have access to create pipeline' do
+ before do
+ downstream_project.add_guest(user)
+ end
+
+ it 'does not create a new pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'changes status of the bridge build' do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq 'insufficient_bridge_permissions'
+ end
+ end
+
+ context 'when user can create pipeline in a downstream project' do
+ let(:stub_config) { true }
+
+ before do
+ downstream_project.add_developer(user)
+ stub_ci_pipeline_yaml_file(YAML.dump(rspec: { script: 'rspec' })) if stub_config
+ end
+
+ it 'creates only one new pipeline' do
+ expect { service.execute(bridge) }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'creates a new pipeline in a downstream project' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq downstream_project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+
+ it 'updates bridge status when downstream pipeline gets processed' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.reload).to be_pending
+ expect(bridge.reload).to be_success
+ end
+
+ context 'when bridge job has already any downstream pipelines' do
+ before do
+ bridge.sourced_pipelines.create!(
+ source_pipeline: bridge.pipeline,
+ source_project: bridge.project,
+ project: bridge.project,
+ pipeline: create(:ci_pipeline, project: bridge.project)
+ )
+ end
+
+ it 'logs an error and exits' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ instance_of(described_class::DuplicateDownstreamPipelineError),
+ bridge_id: bridge.id, project_id: bridge.project.id)
+ .and_call_original
+ expect(Ci::CreatePipelineService).not_to receive(:new)
+ expect(service.execute(bridge)).to be_nil
+ end
+ end
+
+ context 'when target ref is not specified' do
+ let(:trigger) do
+ { trigger: { project: downstream_project.full_path } }
+ end
+
+ it 'is using default branch name' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.ref).to eq 'master'
+ end
+ end
+
+ context 'when downstream pipeline has yaml configuration error' do
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(job: { invalid: 'yaml' }))
+ end
+
+ it 'creates only one new pipeline' do
+ expect { service.execute(bridge) }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'creates a new pipeline in a downstream project' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq downstream_project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+
+ it 'updates the bridge status when downstream pipeline gets processed' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.reload).to be_failed
+ expect(bridge.reload).to be_failed
+ end
+ end
+
+ context 'when downstream project is the same as the job project' do
+ let(:trigger) do
+ { trigger: { project: upstream_project.full_path } }
+ end
+
+ context 'detects a circular dependency' do
+ it 'does not create a new pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+ end
+
+ it 'changes status of the bridge build' do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq 'invalid_bridge_trigger'
+ end
+ end
+
+ context 'when "include" is provided' do
+ let(:file_content) do
+ YAML.dump(
+ rspec: { script: 'rspec' },
+ echo: { script: 'echo' })
+ end
+
+ shared_examples 'creates a child pipeline' do
+ it 'creates only one new pipeline' do
+ expect { service.execute(bridge) }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'creates a child pipeline in the same project' do
+ pipeline = service.execute(bridge)
+ pipeline.reload
+
+ expect(pipeline.builds.map(&:name)).to match_array(%w[rspec echo])
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq bridge.project
+ expect(bridge.sourced_pipelines.first.pipeline).to eq pipeline
+ expect(pipeline.triggered_by_pipeline).to eq upstream_pipeline
+ expect(pipeline.source_bridge).to eq bridge
+ expect(pipeline.source_bridge).to be_a ::Ci::Bridge
+ end
+
+ it 'updates bridge status when downstream pipeline gets processed' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.reload).to be_pending
+ expect(bridge.reload).to be_success
+ end
+
+ it 'propagates parent pipeline settings to the child pipeline' do
+ pipeline = service.execute(bridge)
+ pipeline.reload
+
+ expect(pipeline.ref).to eq(upstream_pipeline.ref)
+ expect(pipeline.sha).to eq(upstream_pipeline.sha)
+ expect(pipeline.source_sha).to eq(upstream_pipeline.source_sha)
+ expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)
+ expect(pipeline.target_sha).to eq(upstream_pipeline.target_sha)
+
+ expect(pipeline.trigger_requests.last).to eq(bridge.trigger_request)
+ end
+ end
+
+ before do
+ upstream_project.repository.create_file(
+ user, 'child-pipeline.yml', file_content, message: 'message', branch_name: 'master')
+
+ upstream_pipeline.update!(sha: upstream_project.commit.id)
+ end
+
+ let(:stub_config) { false }
+
+ let(:trigger) do
+ {
+ trigger: { include: 'child-pipeline.yml' }
+ }
+ end
+
+ it_behaves_like 'creates a child pipeline'
+
+ it 'updates the bridge job to success' do
+ expect { service.execute(bridge) }.to change { bridge.status }.to 'success'
+ end
+
+ context 'when bridge uses "depend" strategy' do
+ let(:trigger) do
+ {
+ trigger: { include: 'child-pipeline.yml', strategy: 'depend' }
+ }
+ end
+
+ it 'does not update the bridge job status' do
+ expect { service.execute(bridge) }.not_to change { bridge.status }
+ end
+ end
+
+ context 'when latest sha for the ref changed in the meantime' do
+ before do
+ upstream_project.repository.create_file(
+ user, 'another-change', 'test', message: 'message', branch_name: 'master')
+ end
+
+ # it does not auto-cancel pipelines from the same family
+ it_behaves_like 'creates a child pipeline'
+ end
+
+ context 'when the parent is a merge request pipeline' do
+ let(:merge_request) { create(:merge_request, source_project: bridge.project, target_project: bridge.project) }
+ let(:file_content) do
+ YAML.dump(
+ workflow: { rules: [{ if: '$CI_MERGE_REQUEST_ID' }] },
+ rspec: { script: 'rspec' },
+ echo: { script: 'echo' })
+ end
+
+ before do
+ bridge.pipeline.update!(source: :merge_request_event, merge_request: merge_request)
+ end
+
+ it_behaves_like 'creates a child pipeline'
+
+ it 'propagates the merge request to the child pipeline' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.merge_request).to eq(merge_request)
+ expect(pipeline).to be_merge_request
+ end
+ end
+
+ context 'when upstream pipeline is a child pipeline' do
+ let!(:pipeline_source) do
+ create(:ci_sources_pipeline,
+ source_pipeline: create(:ci_pipeline, project: upstream_pipeline.project),
+ pipeline: upstream_pipeline
+ )
+ end
+
+ before do
+ upstream_pipeline.update!(source: :parent_pipeline)
+ end
+
+ it 'does not create a further child pipeline' do
+ expect { service.execute(bridge) }
+ .not_to change { Ci::Pipeline.count }
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq 'bridge_pipeline_is_child_pipeline'
+ end
+ end
+ end
+ end
+
+ context 'when downstream pipeline creation errors out' do
+ let(:stub_config) { false }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
+ end
+
+ it 'creates only one new pipeline' do
+ expect { service.execute(bridge) }
+ .to change { Ci::Pipeline.count }.by(1)
+ end
+
+ it 'creates a new pipeline in the downstream project' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.user).to eq bridge.user
+ expect(pipeline.project).to eq downstream_project
+ end
+
+ it 'drops the bridge' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.reload).to be_failed
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
+ end
+ end
+
+ context 'when bridge job status update raises state machine errors' do
+ let(:stub_config) { false }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(invalid: { yaml: 'error' }))
+ bridge.drop!
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception)
+ .with(
+ instance_of(Ci::Bridge::InvalidTransitionError),
+ bridge_id: bridge.id,
+ downstream_pipeline_id: kind_of(Numeric))
+
+ service.execute(bridge)
+ end
+ end
+
+ context 'when bridge job has YAML variables defined' do
+ before do
+ bridge.yaml_variables = [{ key: 'BRIDGE', value: 'var', public: true }]
+ end
+
+ it 'passes bridge variables to downstream pipeline' do
+ pipeline = service.execute(bridge)
+
+ expect(pipeline.variables.first)
+ .to have_attributes(key: 'BRIDGE', value: 'var')
+ end
+ end
+
+ context 'when pipeline variables are defined' do
+ before do
+ upstream_pipeline.variables.create!(key: 'PIPELINE_VARIABLE', value: 'my-value')
+ end
+
+ it 'does not pass pipeline variables directly downstream' do
+ pipeline = service.execute(bridge)
+
+ pipeline.variables.map(&:key).tap do |variables|
+ expect(variables).not_to include 'PIPELINE_VARIABLE'
+ end
+ end
+
+ context 'when using YAML variables interpolation' do
+ before do
+ bridge.yaml_variables = [{ key: 'BRIDGE', value: '$PIPELINE_VARIABLE-var', public: true }]
+ end
+
+ it 'makes it possible to pass pipeline variable downstream' do
+ pipeline = service.execute(bridge)
+
+ pipeline.variables.find_by(key: 'BRIDGE').tap do |variable|
+ expect(variable.value).to eq 'my-value-var'
+ end
+ end
+ end
+ end
+
+ # TODO: Move this context into a feature spec that uses
+ # multiple pipeline processing services. Location TBD in:
+ # https://gitlab.com/gitlab-org/gitlab/issues/36216
+ context 'when configured with bridge job rules' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ downstream_project.add_maintainer(upstream_project.owner)
+ end
+
+ let(:config) do
+ <<-EOY
+ hello:
+ script: echo world
+
+ bridge-job:
+ rules:
+ - if: $CI_COMMIT_REF_NAME == "master"
+ trigger:
+ project: #{downstream_project.full_path}
+ branch: master
+ EOY
+ end
+
+ let(:primary_pipeline) do
+ Ci::CreatePipelineService.new(upstream_project, upstream_project.owner, { ref: 'master' })
+ .execute(:push, save_on_errors: false)
+ end
+
+ let(:bridge) { primary_pipeline.processables.find_by(name: 'bridge-job') }
+ let(:service) { described_class.new(upstream_project, upstream_project.owner) }
+
+ context 'that include the bridge job' do
+ it 'creates the downstream pipeline' do
+ expect { service.execute(bridge) }
+ .to change(downstream_project.ci_pipelines, :count).by(1)
+ end
+ end
+ end
+
+ context 'when user does not have access to push protected branch of downstream project' do
+ before do
+ create(:protected_branch, :maintainers_can_push,
+ project: downstream_project, name: 'feature')
+ end
+
+ it 'changes status of the bridge build' do
+ service.execute(bridge)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq 'insufficient_bridge_permissions'
+ end
+ end
+
+ context 'when there is no such branch in downstream project' do
+ let(:trigger) do
+ {
+ trigger: {
+ project: downstream_project.full_path,
+ branch: 'invalid_branch'
+ }
+ }
+ end
+
+ it 'does not create a pipeline and drops the bridge' do
+ expect { service.execute(bridge) }.not_to change(downstream_project.ci_pipelines, :count)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
+ expect(bridge.options[:downstream_errors]).to eq(['Reference not found'])
+ end
+ end
+
+ context 'when downstream pipeline has a branch rule and does not satisfy' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ let(:config) do
+ <<-EOY
+ hello:
+ script: echo world
+ only:
+ - invalid_branch
+ EOY
+ end
+
+ it 'does not create a pipeline and drops the bridge' do
+ expect { service.execute(bridge) }.not_to change(downstream_project.ci_pipelines, :count)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
+ expect(bridge.options[:downstream_errors]).to eq(['No stages / jobs for this pipeline.'])
+ end
+ end
+
+ context 'when downstream pipeline has invalid YAML' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ let(:config) do
+ <<-EOY
+ test:
+ stage: testx
+ script: echo 1
+ EOY
+ end
+
+ it 'creates the pipeline but drops the bridge' do
+ expect { service.execute(bridge) }.to change(downstream_project.ci_pipelines, :count).by(1)
+
+ expect(bridge.reload).to be_failed
+ expect(bridge.failure_reason).to eq('downstream_pipeline_creation_failed')
+ expect(bridge.options[:downstream_errors]).to eq(
+ ['test job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post']
+ )
+ end
+ end
+ end
+end