diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4679e8b74d7c9c31e2352868831c652f10a0116a..7d3cb62e4eee4689f4549a01710cdd40e96a858b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -764,7 +764,7 @@ def dependencies # find all jobs that are needed if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists? - depended_jobs = depended_jobs.where(name: needs.select(:name)) + depended_jobs = depended_jobs.where(name: needs.artifacts.select(:name)) end # find all jobs that are dependent on @@ -772,6 +772,8 @@ def dependencies depended_jobs = depended_jobs.where(name: options[:dependencies]) end + # if both needs and dependencies are used, + # the end result will be an intersection between them depended_jobs end diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 6531dfd332fee601d812d9d97c1fb1d65f053dcc..0b243c20e67bbcebfe7a0d70e9c944a0269d7270 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -10,5 +10,6 @@ class BuildNeed < ApplicationRecord validates :name, presence: true, length: { maximum: 128 } scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') } + scope :artifacts, -> { where(artifacts: true) } end end diff --git a/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml b/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d9ba35b44c4f22464482d25ec919a271f4f24484 --- /dev/null +++ b/changelogs/unreleased/ci-merge-dependencies-and-artifacts-with-needs.yml @@ -0,0 +1,5 @@ +--- +title: Control passing artifacts from CI DAG needs +merge_request: 19943 +author: +type: added diff --git a/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb new file mode 100644 index 0000000000000000000000000000000000000000..2fbd003b2e5495f343a3a5fac70ee8b9f48dcaf2 --- /dev/null +++ b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddArtifactsToCiBuildNeed < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:ci_build_needs, :artifacts, + :boolean, + default: true, + allow_null: false) + end + + def down + remove_column(:ci_build_needs, :artifacts) + end +end diff --git a/db/schema.rb b/db/schema.rb index 9dccceb79f0745dcfc744749ac2c6debe78585f1..7f380c969e21f9d41dbaf556be2cdd9b15574a84 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -595,6 +595,7 @@ create_table "ci_build_needs", id: :serial, force: :cascade do |t| t.integer "build_id", null: false t.text "name", null: false + t.boolean "artifacts", default: true, null: false t.index ["build_id", "name"], name: "index_ci_build_needs_on_build_id_and_name", unique: true end diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ce3d6247d866184c1bb8fac806d61d9c15425b78..8dd7e2f76f3eb88d2fc06267ab0ee5923243215a 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2232,6 +2232,49 @@ This example creates three paths of execution: - Related to the above, stages must be explicitly defined for all jobs that have the keyword `needs:` or are referred to by one. +#### Artifact downloads with `needs` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.6. + +When using `needs`, artifact downloads are controlled with `artifacts: true` or `artifacts: false`. +The `dependencies` keyword should not be used with `needs`, as this is deprecated since GitLab 12.6. + +In the example below, the `rspec` job will download the `build_job` artifacts, while the +`rubocop` job will not: + +```yaml +build_job: + stage: build + artifacts: + paths: + - binaries/ + +rspec: + stage: test + needs: + - job: build_job + artifacts: true + +rubocop: + stage: test + needs: + - job: build_job + artifacts: false +``` + +Additionally, in the three syntax examples below, the `rspec` job will download the artifacts +from all three `build_jobs`, as `artifacts` is true for `build_job_1`, and will +**default** to true for both `build_job_2` and `build_job_3`. + +```yaml +rspec: + needs: + - job: build_job_1 + artifacts: true + - job: build_job_2 + - build_job_3 +``` + ### `coverage` > [Introduced][ce-7447] in GitLab 8.17. diff --git a/ee/lib/ee/gitlab/ci/config/entry/need.rb b/ee/lib/ee/gitlab/ci/config/entry/need.rb index 1f34412364ff5b3a50e569de498262e8fc915133..844d7eb9fb7ab4e9bacdaf73e592e0de6e079a37 100644 --- a/ee/lib/ee/gitlab/ci/config/entry/need.rb +++ b/ee/lib/ee/gitlab/ci/config/entry/need.rb @@ -9,10 +9,12 @@ module Need extend ActiveSupport::Concern prepended do - strategy :Bridge, class: EE::Gitlab::Ci::Config::Entry::Need::Bridge, if: -> (config) { config.is_a?(Hash) } + strategy :BridgeHash, + class: EE::Gitlab::Ci::Config::Entry::Need::BridgeHash, + if: -> (config) { config.is_a?(Hash) && !config.key?(:job) } end - class Bridge < ::Gitlab::Config::Entry::Node + class BridgeHash < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable diff --git a/ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb b/ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb index 2883851ce33e6e0cf1ff84589c50d958c78cd26b..b14f80e0aec496e26739bc4dfda70dee91333ab3 100644 --- a/ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb +++ b/ee/spec/lib/ee/gitlab/ci/config/entry/need_spec.rb @@ -29,7 +29,7 @@ describe '#errors' do it 'is returns an error about an empty config' do expect(subject.errors) - .to include("bridge config can't be blank") + .to include("bridge hash config can't be blank") end end end diff --git a/ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb b/ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb index 2df14ae0b4cb12e545255c5ef3a8df313968d104..c7272adaef5f99db734d8d69e258953c423eda1b 100644 --- a/ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb +++ b/ee/spec/lib/ee/gitlab/ci/config/entry/needs_spec.rb @@ -48,7 +48,13 @@ describe '.compose!' do context 'when valid job entries composed' do - let(:config) { ['first_job_name', pipeline: 'some/project'] } + let(:config) do + [ + 'first_job_name', + { job: 'second_job_name', artifacts: false }, + { pipeline: 'some/project' } + ] + end before do needs.compose! @@ -61,7 +67,10 @@ describe '#value' do it 'returns key value' do expect(needs.value).to eq( - job: [{ name: 'first_job_name' }], + job: [ + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: false } + ], bridge: [{ pipeline: 'some/project' }] ) end @@ -69,7 +78,7 @@ describe '#descendants' do it 'creates valid descendant nodes' do - expect(needs.descendants.count).to eq 2 + expect(needs.descendants.count).to eq(3) expect(needs.descendants) .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need)) end diff --git a/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb b/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb index e782c762daacd735fef3b6ffa39296e6e4a7e537..ab053a76014f62a38fabc58e1b7d5c0aa4de16d6 100644 --- a/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/ee/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -67,7 +67,7 @@ bridge_needs: { pipeline: 'some/project' } }, needs_attributes: [ - { name: "build" } + { name: "build", artifacts: true } ], when: "on_success", allow_failure: false, diff --git a/lib/gitlab/ci/config/entry/need.rb b/lib/gitlab/ci/config/entry/need.rb index b6db546d8ff42ec45b728b2b908e212dd9d3979f..61bd09fd5f3bb9b2bac4612296888b1849b3dbb0 100644 --- a/lib/gitlab/ci/config/entry/need.rb +++ b/lib/gitlab/ci/config/entry/need.rb @@ -5,9 +5,10 @@ module Ci class Config module Entry class Need < ::Gitlab::Config::Entry::Simplifiable - strategy :Job, if: -> (config) { config.is_a?(String) } + strategy :JobString, if: -> (config) { config.is_a?(String) } + strategy :JobHash, if: -> (config) { config.is_a?(Hash) && config.key?(:job) } - class Job < ::Gitlab::Config::Entry::Node + class JobString < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable validations do @@ -20,7 +21,30 @@ def type end def value - { name: @config } + { name: @config, artifacts: true } + end + end + + class JobHash < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + ALLOWED_KEYS = %i[job artifacts].freeze + attributes :job, :artifacts + + validations do + validates :config, presence: true + validates :config, allowed_keys: ALLOWED_KEYS + validates :job, type: String, presence: true + validates :artifacts, boolean: true, allow_nil: true + end + + def type + :job + end + + def value + { name: job, artifacts: artifacts || artifacts.nil? } end end diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb index e714ef225f5938c36c839e98dd63dc196cdba077..1139efee9e833674eaefe81699b48b0203f56704 100644 --- a/lib/gitlab/ci/config/normalizer.rb +++ b/lib/gitlab/ci/config/normalizer.rb @@ -44,7 +44,7 @@ def expand_needs(job_needs) if all_job_names = parallelized_jobs[job_need_name] all_job_names.map do |job_name| - { name: job_name } + job_need.merge(name: job_name) end else job_need diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb index d119e60490085b92115ab94736e56779b7ab0669..92b71c5f6cc091fe762368f17c41c1a955738d25 100644 --- a/spec/lib/gitlab/ci/config/entry/need_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb @@ -5,31 +5,177 @@ describe ::Gitlab::Ci::Config::Entry::Need do subject(:need) { described_class.new(config) } - context 'when job is specified' do - let(:config) { 'job_name' } + shared_examples 'job type' do + describe '#type' do + subject(:need_type) { need.type } - describe '#valid?' do - it { is_expected.to be_valid } + it { is_expected.to eq(:job) } + end + end + + context 'with simple config' do + context 'when job is specified' do + let(:config) { 'job_name' } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'when need is empty' do + let(:config) { '' } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about an empty config' do + expect(need.errors) + .to contain_exactly("job string config can't be blank") + end + end + + it_behaves_like 'job type' end + end + + context 'with complex config' do + context 'with job name and artifacts true' do + let(:config) { { job: 'job_name', artifacts: true } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'with job name and artifacts false' do + let(:config) { { job: 'job_name', artifacts: false } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: false) + end + end + + it_behaves_like 'job type' + end + + context 'with job name and artifacts nil' do + let(:config) { { job: 'job_name', artifacts: nil } } - describe '#value' do - it 'returns job needs configuration' do - expect(need.value).to eq(name: 'job_name') + describe '#valid?' do + it { is_expected.to be_valid } end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'without artifacts key' do + let(:config) { { job: 'job_name' } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + it 'returns job needs configuration' do + expect(need.value).to eq(name: 'job_name', artifacts: true) + end + end + + it_behaves_like 'job type' + end + + context 'when job name is empty' do + let(:config) { { job: '', artifacts: true } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about an empty config' do + expect(need.errors) + .to contain_exactly("job hash job can't be blank") + end + end + + it_behaves_like 'job type' + end + + context 'when job name is not a string' do + let(:config) { { job: :job_name, artifacts: false } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about job type' do + expect(need.errors) + .to contain_exactly('job hash job should be a string') + end + end + + it_behaves_like 'job type' + end + + context 'when job has unknown keys' do + let(:config) { { job: 'job_name', artifacts: false, some: :key } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'is returns an error about job type' do + expect(need.errors) + .to contain_exactly('job hash config contains unknown keys: some') + end + end + + it_behaves_like 'job type' end end - context 'when need is empty' do - let(:config) { '' } + context 'when need config is not a string or a hash' do + let(:config) { :job_name } describe '#valid?' do it { is_expected.not_to be_valid } end describe '#errors' do - it 'is returns an error about an empty config' do + it 'is returns an error about job type' do expect(need.errors) - .to contain_exactly("job config can't be blank") + .to contain_exactly('unknown strategy has an unsupported type') end end end diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb index f4a76b52d30c0e2c2f37dc3f569b95a94ea582b2..b8b84b5efd20cdc35d171e10373fd5b7c25be4ff 100644 --- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb @@ -51,9 +51,34 @@ end end end + + context 'when wrong needs type is used' do + let(:config) { [{ job: 'job_name', artifacts: true, some: :key }] } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + it 'returns error about incorrect type' do + expect(needs.errors).to contain_exactly( + 'need config contains unknown keys: some') + end + end + end end describe '.compose!' do + shared_examples 'entry with descendant nodes' do + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(needs.descendants.count).to eq 2 + expect(needs.descendants) + .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need)) + end + end + end + context 'when valid job entries composed' do let(:config) { %w[first_job_name second_job_name] } @@ -65,18 +90,80 @@ it 'returns key value' do expect(needs.value).to eq( job: [ - { name: 'first_job_name' }, - { name: 'second_job_name' } + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: true } ] ) end end - describe '#descendants' do - it 'creates valid descendant nodes' do - expect(needs.descendants.count).to eq 2 - expect(needs.descendants) - .to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need)) + it_behaves_like 'entry with descendant nodes' + end + + context 'with complex job entries composed' do + let(:config) do + [ + { job: 'first_job_name', artifacts: true }, + { job: 'second_job_name', artifacts: false } + ] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns key value' do + expect(needs.value).to eq( + job: [ + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: false } + ] + ) + end + end + + it_behaves_like 'entry with descendant nodes' + end + + context 'with mixed job entries composed' do + let(:config) do + [ + 'first_job_name', + { job: 'second_job_name', artifacts: false } + ] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns key value' do + expect(needs.value).to eq( + job: [ + { name: 'first_job_name', artifacts: true }, + { name: 'second_job_name', artifacts: false } + ] + ) + end + end + + it_behaves_like 'entry with descendant nodes' + end + + context 'with empty config' do + let(:config) do + [] + end + + before do + needs.compose! + end + + describe '#value' do + it 'returns empty value' do + expect(needs.value).to eq({}) end end end diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index bf88047838755a37ecd16f84d1b4c577a0979893..db62fb7524dfa769dd34b8dbd011baa0f6c8b710 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -105,7 +105,7 @@ context 'for needs' do let(:expanded_job_attributes) do expanded_job_names.map do |job_name| - { name: job_name } + { name: job_name, extra: :key } end end @@ -117,7 +117,7 @@ script: 'echo 1', needs: { job: [ - { name: job_name.to_s } + { name: job_name.to_s, extra: :key } ] } } @@ -140,8 +140,8 @@ script: 'echo 1', needs: { job: [ - { name: job_name.to_s }, - { name: "other_job" } + { name: job_name.to_s, extra: :key }, + { name: "other_job", extra: :key } ] } } @@ -153,7 +153,7 @@ end it "includes the regular job in dependencies" do - expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job') + expect(subject.dig(:final_job, :needs, :job)).to include(name: 'other_job', extra: :key) end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 5dc51f83b3cfc717b5071d59a40d99bc3e9c554d..ed2d97b1a38237731c4cde722548c032ddefd850 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1525,8 +1525,48 @@ module Ci name: "test1", options: { script: ["test"] }, needs_attributes: [ - { name: "build1" }, - { name: "build2" } + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true } + ], + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + end + end + + context 'needs two builds' do + let(:needs) do + [ + { job: 'parallel', artifacts: false }, + { job: 'build1', artifacts: true }, + 'build2' + ] + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(7) + expect(subject.builds[0]).to eq( + stage: "build", + stage_idx: 1, + name: "build1", + options: { + script: ["test"] + }, + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + expect(subject.builds[4]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + options: { script: ["test"] }, + needs_attributes: [ + { name: "parallel 1/2", artifacts: false }, + { name: "parallel 2/2", artifacts: false }, + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true } ], when: "on_success", allow_failure: false, @@ -1546,8 +1586,37 @@ module Ci name: "test1", options: { script: ["test"] }, needs_attributes: [ - { name: "parallel 1/2" }, - { name: "parallel 2/2" } + { name: "parallel 1/2", artifacts: true }, + { name: "parallel 2/2", artifacts: true } + ], + when: "on_success", + allow_failure: false, + yaml_variables: [] + ) + end + end + + context 'needs dependencies artifacts' do + let(:needs) do + [ + "build1", + { job: "build2" }, + { job: "parallel", artifacts: true } + ] + end + + it "does create jobs with valid specification" do + expect(subject.builds.size).to eq(7) + expect(subject.builds[4]).to eq( + stage: "test", + stage_idx: 2, + name: "test1", + options: { script: ["test"] }, + needs_attributes: [ + { name: "build1", artifacts: true }, + { name: "build2", artifacts: true }, + { name: "parallel 1/2", artifacts: true }, + { name: "parallel 2/2", artifacts: true } ], when: "on_success", allow_failure: false, diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index 450dd550a8f33859d1129ecff8705f2afbcd6169..d1186fa981d1c38966e7b42788ff94a90e2af67f 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -10,4 +10,11 @@ it { is_expected.to validate_presence_of(:build) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(128) } + + describe '.artifacts' do + let_it_be(:with_artifacts) { create(:ci_build_need, artifacts: true) } + let_it_be(:without_artifacts) { create(:ci_build_need, artifacts: false) } + + it { expect(described_class.artifacts).to contain_exactly(with_artifacts) } + end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index d491300add2791c4927c8f4da3f9fdfec275fbd8..916f5536ebda084581f6402e6fbccbb464916b17 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -741,20 +741,26 @@ before do needs.to_a.each do |need| - create(:ci_build_need, build: final, name: need) + create(:ci_build_need, build: final, **need) end end subject { final.dependencies } - context 'when depedencies are defined' do + context 'when dependencies are defined' do let(:dependencies) { %w(rspec staging) } it { is_expected.to contain_exactly(rspec_test, staging) } end context 'when needs are defined' do - let(:needs) { %w(build rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: true }, + { name: 'staging', artifacts: true } + ] + end it { is_expected.to contain_exactly(build, rspec_test, staging) } @@ -767,13 +773,44 @@ end end + context 'when need artifacts are defined' do + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: false }, + { name: 'staging', artifacts: true } + ] + end + + it { is_expected.to contain_exactly(build, staging) } + end + context 'when needs and dependencies are defined' do let(:dependencies) { %w(rspec staging) } - let(:needs) { %w(build rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: true }, + { name: 'staging', artifacts: true } + ] + end it { is_expected.to contain_exactly(rspec_test, staging) } end + context 'when needs and dependencies contradict' do + let(:dependencies) { %w(rspec staging) } + let(:needs) do + [ + { name: 'build', artifacts: true }, + { name: 'rspec', artifacts: false }, + { name: 'staging', artifacts: true } + ] + end + + it { is_expected.to contain_exactly(staging) } + end + context 'when nor dependencies or needs are defined' do it { is_expected.to contain_exactly(build, rspec_test, rubocop_test, staging) } end diff --git a/spec/services/ci/create_pipeline_service/needs_spec.rb b/spec/services/ci/create_pipeline_service/needs_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5ef7e592b361694dbd4c35f9238cda76fcc36ea2 --- /dev/null +++ b/spec/services/ci/create_pipeline_service/needs_spec.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::CreatePipelineService do + context 'needs' do + let_it_be(:user) { create(:admin) } + let_it_be(:project) { create(:project, :repository, creator: user) } + + let(:ref) { 'refs/heads/master' } + let(:source) { :push } + let(:service) { described_class.new(project, user, { ref: ref }) } + let(:pipeline) { service.execute(source) } + + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'with a valid config' do + let(:config) do + <<~YAML + build_a: + stage: build + script: + - make + artifacts: + paths: + - binaries/ + build_b: + stage: build + script: + - make + artifacts: + paths: + - other_binaries/ + build_c: + stage: build + script: + - make + build_d: + stage: build + script: + - make + parallel: 3 + + test_a: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + artifacts: true + - job: build_c + artifacts: false + dependencies: + - build_a + + test_b: + stage: test + script: + - ls + parallel: 2 + needs: + - build_a + - job: build_b + artifacts: true + - job: build_d + artifacts: false + + test_c: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + - job: build_c + artifacts: true + YAML + end + + let(:test_a_build) { pipeline.builds.find_by!(name: 'test_a') } + + it 'creates a pipeline with builds' do + expected_builds = [ + 'build_a', 'build_b', 'build_c', 'build_d 1/3', 'build_d 2/3', + 'build_d 3/3', 'test_a', 'test_b 1/2', 'test_b 2/2', 'test_c' + ] + + expect(pipeline).to be_persisted + expect(pipeline.builds.pluck(:name)).to contain_exactly(*expected_builds) + end + + it 'saves needs' do + expect(test_a_build.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_c', 'artifacts' => false) + ) + end + + it 'saves dependencies' do + expect(test_a_build.options) + .to match(a_hash_including('dependencies' => ['build_a'])) + end + + it 'artifacts default to true' do + test_job = pipeline.builds.find_by!(name: 'test_c') + + expect(test_job.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_c', 'artifacts' => true) + ) + end + + it 'saves parallel jobs' do + ['1/2', '2/2'].each do |part| + test_job = pipeline.builds.find_by(name: "test_b #{part}") + + expect(test_job.needs.map(&:attributes)) + .to contain_exactly( + a_hash_including('name' => 'build_a', 'artifacts' => true), + a_hash_including('name' => 'build_b', 'artifacts' => true), + a_hash_including('name' => 'build_d 1/3', 'artifacts' => false), + a_hash_including('name' => 'build_d 2/3', 'artifacts' => false), + a_hash_including('name' => 'build_d 3/3', 'artifacts' => false) + ) + end + end + end + + context 'with an invalid config' do + let(:config) do + <<~YAML + build_a: + stage: build + script: + - make + artifacts: + paths: + - binaries/ + + build_b: + stage: build + script: + - make + artifacts: + paths: + - other_binaries/ + + test_a: + stage: test + script: + - ls + needs: + - build_a + - job: build_b + artifacts: string + YAML + end + + it { expect(pipeline).to be_persisted } + it { expect(pipeline.builds.any?).to be_falsey } + + it 'assigns an error to the pipeline' do + expect(pipeline.yaml_errors) + .to eq('jobs:test_a:needs:need artifacts should be a boolean value') + end + end + end +end