# frozen_string_literal: true require 'spec_helper' describe Gitlab::Ci::Pipeline::Seed::Build do let(:project) { create(:project, :repository) } let(:head_sha) { project.repository.head_commit.id } let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: head_sha) } let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage } } let(:previous_stages) { [] } let(:seed_build) { described_class.new(pipeline, attributes, previous_stages) } describe '#attributes' do subject { seed_build.attributes } it { is_expected.to be_a(Hash) } it { is_expected.to include(:name, :project, :ref) } context 'with job:when' do let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } } it { is_expected.to include(when: 'on_failure') } end context 'with job:when:delayed' do let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } } it { is_expected.to include(when: 'delayed', start_in: '3 hours') } end context 'with job:rules:[when:]' do context 'is matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } } it { is_expected.to include(when: 'always') } end context 'is not matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } } it { is_expected.to include(when: 'never') } end end context 'with job:rules:[when:delayed]' do context 'is matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } } it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) } end context 'is not matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } } it { is_expected.to include(when: 'never') } end end context 'with job:rules but no explicit when:' do context 'is matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null' }] } } it { is_expected.to include(when: 'on_success') } end context 'is not matched' do let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null' }] } } it { is_expected.to include(when: 'never') } end end context 'with cache:key' do let(:attributes) do { name: 'rspec', ref: 'master', cache: { key: 'a-value' } } end it { is_expected.to include(options: { cache: { key: 'a-value' } }) } end context 'with cache:key:files' do let(:attributes) do { name: 'rspec', ref: 'master', cache: { key: { files: ['VERSION'] } } } end it 'includes cache options' do cache_options = { options: { cache: { key: 'f155568ad0933d8358f66b846133614f76dd0ca4' } } } is_expected.to include(cache_options) end end context 'with cache:key:prefix' do let(:attributes) do { name: 'rspec', ref: 'master', cache: { key: { prefix: 'something' } } } end it { is_expected.to include(options: { cache: { key: 'something-default' } }) } end context 'with cache:key:files and prefix' do let(:attributes) do { name: 'rspec', ref: 'master', cache: { key: { files: ['VERSION'], prefix: 'something' } } } end it 'includes cache options' do cache_options = { options: { cache: { key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4' } } } is_expected.to include(cache_options) end end context 'with empty cache' do let(:attributes) do { name: 'rspec', ref: 'master', cache: {} } end it { is_expected.to include(options: {}) } end end describe '#bridge?' do subject { seed_build.bridge? } context 'when job is a downstream bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end it { is_expected.to be_truthy } context 'when trigger definition is empty' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: '' } } end it { is_expected.to be_falsey } end end context 'when job is an upstream bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } end it { is_expected.to be_truthy } context 'when upstream definition is empty' do let(:attributes) do { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } end it { is_expected.to be_falsey } end end context 'when job is not a bridge' do it { is_expected.to be_falsey } end end describe '#to_resource' do subject { seed_build.to_resource } context 'when job is not a bridge' do it { is_expected.to be_a(::Ci::Build) } it { is_expected.to be_valid } shared_examples_for 'deployment job' do it 'returns a job with deployment' do expect(subject.deployment).not_to be_nil expect(subject.deployment.deployable).to eq(subject) expect(subject.deployment.environment.name).to eq(expected_environment_name) end end shared_examples_for 'non-deployment job' do it 'returns a job without deployment' do expect(subject.deployment).to be_nil end end shared_examples_for 'ensures environment existence' do it 'has environment' do expect(subject).to be_has_environment expect(subject.environment).to eq(environment_name) expect(subject.metadata.expanded_environment_name).to eq(expected_environment_name) expect(Environment.exists?(name: expected_environment_name)).to eq(true) end end shared_examples_for 'ensures environment inexistence' do it 'does not have environment' do expect(subject).not_to be_has_environment expect(subject.environment).to be_nil expect(subject.metadata.expanded_environment_name).to be_nil expect(Environment.exists?(name: expected_environment_name)).to eq(false) end end context 'when job deploys to production' do let(:environment_name) { 'production' } let(:expected_environment_name) { 'production' } let(:attributes) { { name: 'deploy', ref: 'master', environment: 'production' } } it_behaves_like 'deployment job' it_behaves_like 'ensures environment existence' context 'when the environment name is invalid' do let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } } it_behaves_like 'non-deployment job' it_behaves_like 'ensures environment inexistence' it 'tracks an exception' do expect(Gitlab::ErrorTracking).to receive(:track_exception) .with(an_instance_of(described_class::EnvironmentCreationFailure), project_id: project.id, reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'}) .once subject end end end context 'when job starts a review app' do let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' } let(:expected_environment_name) { "review/#{pipeline.ref}" } let(:attributes) do { name: 'deploy', ref: 'master', environment: environment_name, options: { environment: { name: environment_name } } } end it_behaves_like 'deployment job' it_behaves_like 'ensures environment existence' end context 'when job stops a review app' do let(:environment_name) { 'review/$CI_COMMIT_REF_NAME' } let(:expected_environment_name) { "review/#{pipeline.ref}" } let(:attributes) do { name: 'deploy', ref: 'master', environment: environment_name, options: { environment: { name: environment_name, action: 'stop' } } } end it 'returns a job without deployment' do expect(subject.deployment).to be_nil end it_behaves_like 'non-deployment job' it_behaves_like 'ensures environment existence' end context 'when job belongs to a resource group' do let(:attributes) { { name: 'rspec', ref: 'master', resource_group_key: 'iOS' } } it 'returns a job with resource group' do expect(subject.resource_group).not_to be_nil expect(subject.resource_group.key).to eq('iOS') end end end context 'when job is a bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' }, scheduling_type: :stage } end it { is_expected.to be_a(::Ci::Bridge) } it { is_expected.to be_valid } end it 'memoizes a resource object' do expect(subject.object_id).to eq seed_build.to_resource.object_id end it 'can not be persisted without explicit assignment' do pipeline.save! expect(subject).not_to be_persisted end end describe 'applying job inclusion policies' do subject { seed_build } context 'when no branch policy is specified' do let(:attributes) do { name: 'rspec' } end it { is_expected.to be_included } end context 'when branch policy does not match' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: ['deploy'] } } end it { is_expected.not_to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: ['deploy'] } } end it { is_expected.to be_included } end context 'with both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[deploy] }, except: { refs: %w[deploy] } } end it { is_expected.not_to be_included } end end context 'when branch regexp policy does not match' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[/^deploy$/] } } end it { is_expected.not_to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: %w[/^deploy$/] } } end it { is_expected.to be_included } end context 'with both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[/^deploy$/] }, except: { refs: %w[/^deploy$/] } } end it { is_expected.not_to be_included } end end context 'when branch policy matches' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[deploy master] } } end it { is_expected.to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: %w[deploy master] } } end it { is_expected.not_to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[deploy master] }, except: { refs: %w[deploy master] } } end it { is_expected.not_to be_included } end end context 'when keyword policy matches' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[branches] } } end it { is_expected.to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: %w[branches] } } end it { is_expected.not_to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[branches] }, except: { refs: %w[branches] } } end it { is_expected.not_to be_included } end end context 'when keyword policy does not match' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[tags] } } end it { is_expected.not_to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: %w[tags] } } end it { is_expected.to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[tags] }, except: { refs: %w[tags] } } end it { is_expected.not_to be_included } end end context 'with source-keyword policy' do using RSpec::Parameterized let(:pipeline) do build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source) end context 'matches' do where(:keyword, :source) do [ %w[pushes push], %w[web web], %w[triggers trigger], %w[schedules schedule], %w[api api], %w[external external] ] end with_them do context 'using an only policy' do let(:attributes) do { name: 'rspec', only: { refs: [keyword] } } end it { is_expected.to be_included } end context 'using an except policy' do let(:attributes) do { name: 'rspec', except: { refs: [keyword] } } end it { is_expected.not_to be_included } end context 'using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } end it { is_expected.not_to be_included } end end end context 'non-matches' do where(:keyword, :source) do %w[web trigger schedule api external].map { |source| ['pushes', source] } + %w[push trigger schedule api external].map { |source| ['web', source] } + %w[push web schedule api external].map { |source| ['triggers', source] } + %w[push web trigger api external].map { |source| ['schedules', source] } + %w[push web trigger schedule external].map { |source| ['api', source] } + %w[push web trigger schedule api].map { |source| ['external', source] } end with_them do context 'using an only policy' do let(:attributes) do { name: 'rspec', only: { refs: [keyword] } } end it { is_expected.not_to be_included } end context 'using an except policy' do let(:attributes) do { name: 'rspec', except: { refs: [keyword] } } end it { is_expected.to be_included } end context 'using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: [keyword] }, except: { refs: [keyword] } } end it { is_expected.not_to be_included } end end end end context 'when repository path matches' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } } end it { is_expected.to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } } end it { is_expected.not_to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] }, except: { refs: ["branches@#{pipeline.project_full_path}"] } } end it { is_expected.not_to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] }, except: { refs: ["branches@#{pipeline.project_full_path}"] } } end it { is_expected.not_to be_included } end end context 'when repository path does not match' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[branches@fork] } } end it { is_expected.not_to be_included } end context 'when using except' do let(:attributes) do { name: 'rspec', except: { refs: %w[branches@fork] } } end it { is_expected.to be_included } end context 'when using both only and except policies' do let(:attributes) do { name: 'rspec', only: { refs: %w[branches@fork] }, except: { refs: %w[branches@fork] } } end it { is_expected.not_to be_included } end end context 'using rules:' do using RSpec::Parameterized let(:attributes) { { name: 'rspec', rules: rule_set } } context 'with a matching if: rule' do context 'with an explicit `when: never`' do where(:rule_set) do [ [[{ if: '$VARIABLE == null', when: 'never' }]], [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]], [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]] ] end with_them do it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end end context 'with an explicit `when: always`' do where(:rule_set) do [ [[{ if: '$VARIABLE == null', when: 'always' }]], [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]], [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'always') end end end context 'with an explicit `when: on_failure`' do where(:rule_set) do [ [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'on_failure') end end end context 'with an explicit `when: delayed`' do where(:rule_set) do [ [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]], [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]], [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' }) end end end context 'without an explicit when: value' do where(:rule_set) do [ [[{ if: '$VARIABLE == null' }]], [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]], [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'on_success') end end end end context 'with a matching changes: rule' do let(:pipeline) do create(:ci_pipeline, project: project).tap do |pipeline| stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml]) end end context 'with an explicit `when: never`' do where(:rule_set) do [ [[{ changes: %w[*/**/*.rb], when: 'never' }, { changes: %w[*/**/*.rb], when: 'always' }]], [[{ changes: %w[app/models/ci/pipeline.rb], when: 'never' }, { changes: %w[app/models/ci/pipeline.rb], when: 'always' }]], [[{ changes: %w[spec/**/*.rb], when: 'never' }, { changes: %w[spec/**/*.rb], when: 'always' }]], [[{ changes: %w[*.yml], when: 'never' }, { changes: %w[*.yml], when: 'always' }]], [[{ changes: %w[.*.yml], when: 'never' }, { changes: %w[.*.yml], when: 'always' }]], [[{ changes: %w[**/*], when: 'never' }, { changes: %w[**/*], when: 'always' }]], [[{ changes: %w[*/**/*.rb *.yml], when: 'never' }, { changes: %w[*/**/*.rb *.yml], when: 'always' }]], [[{ changes: %w[.*.yml **/*], when: 'never' }, { changes: %w[.*.yml **/*], when: 'always' }]] ] end with_them do it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end end context 'with an explicit `when: always`' do where(:rule_set) do [ [[{ changes: %w[*/**/*.rb], when: 'always' }, { changes: %w[*/**/*.rb], when: 'never' }]], [[{ changes: %w[app/models/ci/pipeline.rb], when: 'always' }, { changes: %w[app/models/ci/pipeline.rb], when: 'never' }]], [[{ changes: %w[spec/**/*.rb], when: 'always' }, { changes: %w[spec/**/*.rb], when: 'never' }]], [[{ changes: %w[*.yml], when: 'always' }, { changes: %w[*.yml], when: 'never' }]], [[{ changes: %w[.*.yml], when: 'always' }, { changes: %w[.*.yml], when: 'never' }]], [[{ changes: %w[**/*], when: 'always' }, { changes: %w[**/*], when: 'never' }]], [[{ changes: %w[*/**/*.rb *.yml], when: 'always' }, { changes: %w[*/**/*.rb *.yml], when: 'never' }]], [[{ changes: %w[.*.yml **/*], when: 'always' }, { changes: %w[.*.yml **/*], when: 'never' }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'always') end end end context 'without an explicit when: value' do where(:rule_set) do [ [[{ changes: %w[*/**/*.rb] }]], [[{ changes: %w[app/models/ci/pipeline.rb] }]], [[{ changes: %w[spec/**/*.rb] }]], [[{ changes: %w[*.yml] }]], [[{ changes: %w[.*.yml] }]], [[{ changes: %w[**/*] }]], [[{ changes: %w[*/**/*.rb *.yml] }]], [[{ changes: %w[.*.yml **/*] }]] ] end with_them do it { is_expected.to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'on_success') end end end end context 'with no matching rule' do where(:rule_set) do [ [[{ if: '$VARIABLE != null', when: 'never' }]], [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], [[{ if: '$VARIABLE != null', when: 'always' }]], [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], [[{ if: '$VARIABLE != null' }]], [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]], [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]] ] end with_them do it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end end context 'with no rules' do let(:rule_set) { [] } it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end end end describe 'applying needs: dependency' do subject { seed_build } let(:needs_count) { 1 } let(:needs_attributes) do Array.new(needs_count, name: 'build') end let(:attributes) do { name: 'rspec', needs_attributes: needs_attributes } end context 'when build job is not present in prior stages' do it "is included" do is_expected.to be_included end it "returns an error" do expect(subject.errors).to contain_exactly( "rspec: needs 'build'") end end context 'when build job is part of prior stages' do let(:stage_attributes) do { name: 'build', index: 0, builds: [{ name: 'build' }] } end let(:stage_seed) do Gitlab::Ci::Pipeline::Seed::Stage.new(pipeline, stage_attributes, []) end let(:previous_stages) { [stage_seed] } it "is included" do is_expected.to be_included end it "does not have errors" do expect(subject.errors).to be_empty end end context 'when lower limit of needs is reached' do before do stub_feature_flags(ci_dag_limit_needs: true) end let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 } it "returns an error" do expect(subject.errors).to contain_exactly( "rspec: one job can only need 10 others, but you have listed 11. See needs keyword documentation for more details") end end context 'when upper limit of needs is reached' do before do stub_feature_flags(ci_dag_limit_needs: false) end let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 } it "returns an error" do expect(subject.errors).to contain_exactly( "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details") end end end end