diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/lib/gitlab/ci | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) | |
download | gitlab-ce-71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e.tar.gz |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/lib/gitlab/ci')
49 files changed, 2478 insertions, 1328 deletions
diff --git a/spec/lib/gitlab/ci/artifacts/logger_spec.rb b/spec/lib/gitlab/ci/artifacts/logger_spec.rb index 7753cb0d25e..7a2f8b6ea37 100644 --- a/spec/lib/gitlab/ci/artifacts/logger_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/logger_spec.rb @@ -9,23 +9,27 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do describe '.log_created' do it 'logs information about created artifact' do - artifact = create(:ci_job_artifact, :archive) - - expect(Gitlab::AppLogger).to receive(:info).with( - hash_including( - message: 'Artifact created', - job_artifact_id: artifact.id, - size: artifact.size, - type: artifact.file_type, - build_id: artifact.job_id, - project_id: artifact.project_id, - 'correlation_id' => an_instance_of(String), - 'meta.feature_category' => 'test', - 'meta.caller_id' => 'caller' + artifact_1 = create(:ci_job_artifact, :archive) + artifact_2 = create(:ci_job_artifact, :metadata) + artifacts = [artifact_1, artifact_2] + + artifacts.each do |artifact| + expect(Gitlab::AppLogger).to receive(:info).with( + hash_including( + message: 'Artifact created', + job_artifact_id: artifact.id, + size: artifact.size, + file_type: artifact.file_type, + build_id: artifact.job_id, + project_id: artifact.project_id, + 'correlation_id' => an_instance_of(String), + 'meta.feature_category' => 'test', + 'meta.caller_id' => 'caller' + ) ) - ) + end - described_class.log_created(artifact) + described_class.log_created(artifacts) end end @@ -43,7 +47,7 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do job_artifact_id: artifact.id, expire_at: artifact.expire_at, size: artifact.size, - type: artifact.file_type, + file_type: artifact.file_type, build_id: artifact.job_id, project_id: artifact.project_id, method: method, diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb index 9ff9200322e..314714c543b 100644 --- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::AutoRetry do +RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authoring do let(:auto_retry) { described_class.new(build) } describe '#allowed?' do @@ -112,5 +112,13 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do expect(result).to eq ['always'] end end + + context 'with retry[:when] set to nil' do + let(:build) { create(:ci_build, options: { retry: { when: nil } }) } + + it 'returns always array' do + expect(result).to eq ['always'] + end + end end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb index 31c7437cfe0..ebdb738f10b 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/if_spec.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'support/helpers/stubbed_feature' -require 'support/helpers/stub_feature_flags' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If do +RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::If, feature_category: :continuous_integration do include StubFeatureFlags subject(:if_clause) { described_class.new(expression) } diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb new file mode 100644 index 00000000000..d9beae0555c --- /dev/null +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do + let_it_be(:user) { create(:user) } + + let(:path) { described_class.new(address: address, content_filename: 'template.yml') } + let(:settings) { Settingslogic.new({ 'component_fqdn' => current_host }) } + let(:current_host) { 'acme.com/' } + + before do + allow(::Settings).to receive(:gitlab_ci).and_return(settings) + end + + describe 'FQDN path' do + let_it_be(:existing_project) { create(:project, :repository) } + + let(:project_path) { existing_project.full_path } + let(:address) { "acme.com/#{project_path}/component@#{version}" } + let(:version) { 'master' } + + context 'when project exists' do + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + + context 'when content exists' do + let(:content) { 'image: alpine' } + + before do + allow_next_instance_of(Repository) do |instance| + allow(instance) + .to receive(:blob_data_at) + .with(existing_project.commit('master').id, 'component/template.yml') + .and_return(content) + end + end + + context 'when user has permissions to read code' do + before do + existing_project.add_developer(user) + end + + it 'fetches the content' do + expect(path.fetch_content!(current_user: user)).to eq(content) + end + end + + context 'when user does not have permissions to download code' do + it 'raises an error when fetching the content' do + expect { path.fetch_content!(current_user: user) } + .to raise_error(Gitlab::Access::AccessDeniedError) + end + end + end + end + + context 'when project path is nested under a subgroup' do + let(:existing_group) { create(:group, :nested) } + let(:existing_project) { create(:project, :repository, group: existing_group) } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + end + + context 'when current GitLab instance is installed on a relative URL' do + let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } + let(:current_host) { 'acme.com/gitlab/' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('component/template.yml') + end + end + + context 'when version does not exist' do + let(:version) { 'non-existent' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to eq(existing_project) + expect(path.host).to eq(current_host) + expect(path.sha).to be_nil + expect(path.project_file_path).to eq('component/template.yml') + end + + it 'returns nil when fetching the content' do + expect(path.fetch_content!(current_user: user)).to be_nil + end + end + + context 'when project does not exist' do + let(:project_path) { 'non-existent/project' } + + it 'provides the expected attributes', :aggregate_failures do + expect(path.project).to be_nil + expect(path.host).to eq(current_host) + expect(path.sha).to be_nil + expect(path.project_file_path).to be_nil + end + + it 'returns nil when fetching the content' do + expect(path.fetch_content!(current_user: user)).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb index fd7f85c9298..5eecff5b592 100644 --- a/spec/lib/gitlab/ci/config/entry/include_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb @@ -44,6 +44,12 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do it { is_expected.to be_valid } end + context 'when using "component"' do + let(:config) { { component: 'path/to/component@1.0' } } + + it { is_expected.to be_valid } + end + context 'when using "artifact"' do context 'and specifying "job"' do let(:config) { { artifact: 'test.yml', job: 'generator' } } diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 40702e75404..1fd3cf3c99f 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Context do +RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_authoring do let(:project) { build(:project) } let(:user) { double('User') } let(:sha) { '12345' } @@ -14,7 +14,8 @@ RSpec.describe Gitlab::Ci::Config::External::Context do describe 'attributes' do context 'with values' do it { is_expected.to have_attributes(**attributes) } - it { expect(subject.expandset).to eq(Set.new) } + it { expect(subject.expandset).to eq([]) } + it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) } it { expect(subject.execution_deadline).to eq(0) } it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } @@ -25,11 +26,39 @@ RSpec.describe Gitlab::Ci::Config::External::Context do let(:attributes) { { project: nil, user: nil, sha: nil } } it { is_expected.to have_attributes(**attributes) } - it { expect(subject.expandset).to eq(Set.new) } + it { expect(subject.expandset).to eq([]) } + it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) } it { expect(subject.execution_deadline).to eq(0) } it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } end + + context 'when FF ci_includes_count_duplicates is disabled' do + before do + stub_feature_flags(ci_includes_count_duplicates: false) + end + + context 'with values' do + it { is_expected.to have_attributes(**attributes) } + it { expect(subject.expandset).to eq(Set.new) } + it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) } + it { expect(subject.execution_deadline).to eq(0) } + it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } + it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } + it { expect(subject.variables_hash).to include('a' => 'b') } + end + + context 'without values' do + let(:attributes) { { project: nil, user: nil, sha: nil } } + + it { is_expected.to have_attributes(**attributes) } + it { expect(subject.expandset).to eq(Set.new) } + it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) } + it { expect(subject.execution_deadline).to eq(0) } + it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } + it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } + end + end end describe '#set_deadline' do diff --git a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb index a8dc7897082..45a15fb5f36 100644 --- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Artifact do +RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_authoring do let(:parent_pipeline) { create(:ci_pipeline) } let(:variables) {} let(:context) do @@ -31,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact do describe '#valid?' do subject(:valid?) do - external_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([external_file]) external_file.valid? end @@ -162,7 +162,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact do user: anything } expect(context).to receive(:mutate).with(expected_attrs).and_call_original - external_file.validate! + + expect(valid?).to be_truthy external_file.content end end diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index 8475c3a8b19..55d95d0c1f8 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Base do +RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_authoring do let(:variables) {} let(:context_params) { { sha: 'HEAD', variables: variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } @@ -51,7 +51,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base do describe '#valid?' do subject(:valid?) do - file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([file]) file.valid? end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb new file mode 100644 index 00000000000..a162a1a8abf --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do + let_it_be(:context_project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:project_variables) { project.predefined_variables } + + let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } + let(:external_resource) { described_class.new(params, context) } + let(:params) { { component: 'gitlab.com/acme/components/my-component@1.0' } } + let(:fetch_service) { instance_double(::Ci::Components::FetchService) } + let(:response) { ServiceResponse.error(message: 'some error message') } + + let(:context_params) do + { + project: context_project, + sha: '12345', + user: user, + variables: project_variables + } + end + + before do + allow(::Ci::Components::FetchService) + .to receive(:new) + .with( + address: params[:component], + current_user: context.user + ).and_return(fetch_service) + + allow(fetch_service).to receive(:execute).and_return(response) + end + + describe '#matching?' do + subject(:matching) { external_resource.matching? } + + context 'when component is specified' do + let(:params) { { component: 'some-value' } } + + it { is_expected.to be_truthy } + + context 'when feature flag ci_include_components is disabled' do + before do + stub_feature_flags(ci_include_components: false) + end + + it { is_expected.to be_falsey } + end + end + + context 'when component is not specified' do + let(:params) { { local: 'some-value' } } + + it { is_expected.to be_falsy } + end + end + + describe '#valid?' do + subject(:valid?) do + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([external_resource]) + external_resource.valid? + end + + context 'when the context project does not have a repository' do + before do + allow(context_project).to receive(:repository).and_return(nil) + end + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Unable to use components outside of a project context') + end + end + + context 'when location is not provided' do + let(:params) { { component: 123 } } + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Included file `123` needs to be a string') + end + end + + context 'when component path is provided' do + context 'when component is not found' do + let(:response) do + ServiceResponse.error(message: 'Content not found') + end + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to eq('Content not found') + end + end + + context 'when component is found' do + let(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end + + let(:response) do + ServiceResponse.success(payload: { + content: content, + path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + }) + end + + it 'is valid' do + expect(subject).to be_truthy + expect(external_resource.content).to eq(content) + end + + context 'when content is not a valid YAML' do + let(:content) { 'the-content' } + + it 'is invalid' do + expect(subject).to be_falsy + expect(external_resource.error_message).to match(/does not have valid YAML syntax/) + end + end + end + end + end + + describe '#metadata' do + subject(:metadata) { external_resource.metadata } + + let(:component_path) do + instance_double(::Gitlab::Ci::Components::InstancePath, + project: project, + sha: '12345', + project_file_path: 'my-component/template.yml') + end + + let(:response) do + ServiceResponse.success(payload: { path: component_path }) + end + + it 'returns the metadata' do + is_expected.to include( + context_project: context_project.full_path, + context_sha: context.sha, + type: :component, + location: 'gitlab.com/acme/components/my-component@1.0', + blob: a_string_ending_with("#{project.full_path}/-/blob/12345/my-component/template.yml"), + raw: nil, + extra: {} + ) + end + end + + describe '#expand_context' do + let(:component_path) do + instance_double(::Gitlab::Ci::Components::InstancePath, + project: project, + sha: '12345') + end + + let(:response) do + ServiceResponse.success(payload: { path: component_path }) + end + + subject { external_resource.send(:expand_context_attrs) } + + it 'inherits user and variables while changes project and sha' do + is_expected.to include( + project: project, + sha: '12345', + user: context.user, + variables: context.variables) + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index a77acb45978..b5895b4bc81 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -30,6 +30,40 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip .to receive(:check_execution_time!) end + describe '.initialize' do + context 'when a local is specified' do + let(:params) { { local: 'file' } } + + it 'sets the location' do + expect(local_file.location).to eq('file') + end + + context 'when the local is prefixed with a slash' do + let(:params) { { local: '/file' } } + + it 'removes the slash' do + expect(local_file.location).to eq('file') + end + end + + context 'when the local is prefixed with multiple slashes' do + let(:params) { { local: '//file' } } + + it 'removes slashes' do + expect(local_file.location).to eq('file') + end + end + end + + context 'with a missing local' do + let(:params) { { local: nil } } + + it 'sets the location to an empty string' do + expect(local_file.location).to eq('') + end + end + end + describe '#matching?' do context 'when a local is specified' do let(:params) { { local: 'file' } } @@ -58,7 +92,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip describe '#valid?' do subject(:valid?) do - local_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([local_file]) local_file.valid? end @@ -88,10 +122,13 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret', 'masked' => true }]) } let(:location) { '/lib/gitlab/ci/templates/secret/existent-file.yml' } - it 'returns false and adds an error message about an empty file' do + before do allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("") - local_file.validate! - expect(local_file.errors).to include("Local file `/lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!") + end + + it 'returns false and adds an error message about an empty file' do + expect(valid?).to be_falsy + expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/xxxxxx/existent-file.yml` is empty!") end end @@ -101,7 +138,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip it 'returns false and adds an error message stating that included file does not exist' do expect(valid?).to be_falsy - expect(local_file.errors).to include("Sha #{sha} is not valid!") + expect(local_file.errors).to include("Local file `lib/gitlab/ci/templates/existent-file.yml` does not exist!") end end end @@ -143,11 +180,11 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } before do - local_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([local_file]) end it 'returns an error message' do - expect(local_file.error_message).to eq("Local file `/lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!") + expect(local_file.error_message).to eq("Local file `lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!") end end @@ -203,7 +240,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip context_project: project.full_path, context_sha: sha, type: :local, - location: '/lib/gitlab/ci/templates/existent-file.yml', + location: 'lib/gitlab/ci/templates/existent-file.yml', blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/lib/gitlab/ci/templates/existent-file.yml", raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/lib/gitlab/ci/templates/existent-file.yml", extra: {} diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 0ba92d1e92d..abe38cdbc3e 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -2,7 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Project do +RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_authoring do + include RepoHelpers + let_it_be(:context_project) { create(:project) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -12,11 +14,12 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:project_file) { described_class.new(params, context) } let(:variables) { project.predefined_variables.to_runner_variables } + let(:project_sha) { project.commit.sha } let(:context_params) do { project: context_project, - sha: '12345', + sha: project_sha, user: context_user, parent_pipeline: parent_pipeline, variables: variables @@ -67,7 +70,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do describe '#valid?' do subject(:valid?) do - project_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([project_file]) project_file.valid? end @@ -76,10 +79,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, file: '/file.yml' } end - let(:root_ref_sha) { project.repository.root_ref_sha } - - before do - stub_project_blob(root_ref_sha, '/file.yml') { 'image: image:1.0' } + around do |example| + create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do + example.run + end end it { is_expected.to be_truthy } @@ -99,10 +102,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do { project: project.full_path, ref: 'master', file: '/file.yml' } end - let(:ref_sha) { project.commit('master').sha } - - before do - stub_project_blob(ref_sha, '/file.yml') { 'image: image:1.0' } + around do |example| + create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do + example.run + end end it { is_expected.to be_truthy } @@ -114,15 +117,16 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do end let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } - let(:root_ref_sha) { project.repository.root_ref_sha } - before do - stub_project_blob(root_ref_sha, '/secret_file.yml') { '' } + around do |example| + create_and_delete_files(project, { '/secret_file.yml' => '' }) do + example.run + end end it 'returns false' do expect(valid?).to be_falsy - expect(project_file.error_message).to include("Project `#{project.full_path}` file `/xxxxxxxxxxx.yml` is empty!") + expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxx.yml` is empty!") end end @@ -146,7 +150,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do it 'returns false' do expect(valid?).to be_falsy - expect(project_file.error_message).to include("Project `#{project.full_path}` file `/xxxxxxxxxxxxxxxxxxx.yml` does not exist!") + expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxxxxxxxxxx.yml` does not exist!") end end @@ -157,7 +161,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do it 'returns false' do expect(valid?).to be_falsy - expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!') + expect(project_file.error_message).to include('Included file `invalid-file` does not have YAML extension!') end end @@ -200,7 +204,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do is_expected.to include( user: user, project: project, - sha: project.commit('master').id, + sha: project_sha, parent_pipeline: parent_pipeline, variables: project.predefined_variables.to_runner_variables) end @@ -216,45 +220,43 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do it { is_expected.to eq( context_project: context_project.full_path, - context_sha: '12345', + context_sha: project_sha, type: :file, - location: '/file.yml', - blob: "http://localhost/#{project.full_path}/-/blob/#{project.commit('master').id}/file.yml", - raw: "http://localhost/#{project.full_path}/-/raw/#{project.commit('master').id}/file.yml", + location: 'file.yml', + blob: "http://localhost/#{project.full_path}/-/blob/#{project_sha}/file.yml", + raw: "http://localhost/#{project.full_path}/-/raw/#{project_sha}/file.yml", extra: { project: project.full_path, ref: 'HEAD' } ) } context 'when project name and ref include masked variables' do + let(:project_name) { 'my_project_name' } + let(:branch_name) { 'merge-commit-analyze-after' } + let(:project) { create(:project, :repository, name: project_name) } + let(:namespace_path) { project.namespace.full_path } + let(:included_project_sha) { project.commit(branch_name).sha } + let(:variables) do Gitlab::Ci::Variables::Collection.new( [ - { key: 'VAR1', value: 'a_secret_variable_value1', masked: true }, - { key: 'VAR2', value: 'a_secret_variable_value2', masked: true } + { key: 'VAR1', value: project_name, masked: true }, + { key: 'VAR2', value: branch_name, masked: true } ]) end - let(:params) { { project: 'a_secret_variable_value1', ref: 'a_secret_variable_value2', file: '/file.yml' } } + let(:params) { { project: project.full_path, ref: branch_name, file: '/file.yml' } } it { is_expected.to eq( context_project: context_project.full_path, - context_sha: '12345', + context_sha: project_sha, type: :file, - location: '/file.yml', - blob: nil, - raw: nil, - extra: { project: 'xxxxxxxxxxxxxxxxxxxxxxxx', ref: 'xxxxxxxxxxxxxxxxxxxxxxxx' } + location: 'file.yml', + blob: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/blob/#{included_project_sha}/file.yml", + raw: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/raw/#{included_project_sha}/file.yml", + extra: { project: "#{namespace_path}/xxxxxxxxxxxxxxx", ref: 'xxxxxxxxxxxxxxxxxxxxxxxxxx' } ) } end end - - private - - def stub_project_blob(ref, path) - allow_next_instance_of(Repository) do |instance| - allow(instance).to receive(:blob_data_at).with(ref, path) { yield } - end - end end diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index 8d93cdcf378..2ce3c257a43 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Remote do +RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_authoring do include StubRequests let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } @@ -55,7 +55,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do describe "#valid?" do subject(:valid?) do - remote_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([remote_file]) remote_file.valid? end @@ -138,7 +138,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote do describe "#error_message" do subject(:error_message) do - remote_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([remote_file]) remote_file.error_message end diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb index 074e7a1d32d..83e98874118 100644 --- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Template do +RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_authoring do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } @@ -46,7 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Template do describe "#valid?" do subject(:valid?) do - template_file.validate! + Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([template_file]) template_file.valid? end diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb index 5f321a696c9..11c79e19cff 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -17,11 +17,14 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: describe '#process' do let(:locations) do - [{ local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' }] + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { component: 'gitlab.com/org/component@1.0' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] end subject(:process) { matcher.process(locations) } @@ -30,6 +33,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: is_expected.to contain_exactly( an_instance_of(Gitlab::Ci::Config::External::File::Local), an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Component), an_instance_of(Gitlab::Ci::Config::External::File::Remote), an_instance_of(Gitlab::Ci::Config::External::File::Template), an_instance_of(Gitlab::Ci::Config::External::File::Artifact) @@ -42,8 +46,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - '`{"invalid":"file.yml"}` does not have a valid subkey for include. ' \ - 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + /`{"invalid":"file.yml"}` does not have a valid subkey for include. Valid subkeys are:/ ) end @@ -53,8 +56,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error with a masked sentence' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - '`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. ' \ - 'Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`' + /`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/ ) end end @@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: it 'raises an error' do expect { process }.to raise_error( Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError, - "Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`" + /Each include must use only one of:/ ) end end diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb index 7c7252c6b0e..a219666f24e 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -25,6 +25,10 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: my_test: script: echo Hello World YAML + 'myfolder/file3.yml' => <<~YAML, + my_deploy: + script: echo Hello World + YAML 'nested_configs.yml' => <<~YAML include: - local: myfolder/file1.yml @@ -58,16 +62,63 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: let(:files) do [ Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), - Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context) + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file3.yml' }, context) ] end it 'returns an array of file objects' do - expect(process.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml') + expect(process.map(&:location)).to contain_exactly( + 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml' + ) + end + + it 'adds files to the expandset' do + expect { process }.to change { context.expandset.count }.by(3) + end + + it 'calls Gitaly only once for all files', :request_store do + # 1 for project.commit.id, 1 for the files + expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(2) + end + end + + context 'when files are project files' do + let_it_be(:included_project) { create(:project, :repository, namespace: project.namespace, creator: user) } + + let(:files) do + [ + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file1.yml', project: included_project.full_path }, context + ), + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file2.yml', project: included_project.full_path }, context + ), + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file3.yml', project: included_project.full_path }, context + ) + ] + end + + around(:all) do |example| + create_and_delete_files(included_project, project_files) do + example.run + end + end + + it 'returns an array of file objects' do + expect(process.map(&:location)).to contain_exactly( + 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml' + ) end it 'adds files to the expandset' do - expect { process }.to change { context.expandset.count }.by(2) + expect { process }.to change { context.expandset.count }.by(3) + end + + it 'calls Gitaly only once for all files', :request_store do + # 1 for project.commit.id, 3 for the sha check, 1 for the files + expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(5) end end @@ -99,7 +150,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: end end - context 'when max_includes is exceeded' do + context 'when total file count exceeds max_includes' do context 'when files are nested' do let(:files) do [ @@ -107,11 +158,8 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: ] end - before do - allow(context).to receive(:max_includes).and_return(1) - end - it 'raises Processor::IncludeError' do + allow(context).to receive(:max_includes).and_return(1) expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError) end end @@ -124,13 +172,36 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: ] end - before do + it 'raises Mapper::TooManyIncludesError' do allow(context).to receive(:max_includes).and_return(1) + expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError) end + end - it 'raises Mapper::TooManyIncludesError' do + context 'when files are duplicates' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context) + ] + end + + it 'raises error' do + allow(context).to receive(:max_includes).and_return(2) expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError) end + + context 'when FF ci_includes_count_duplicates is disabled' do + before do + stub_feature_flags(ci_includes_count_duplicates: false) + end + + it 'does not raise error' do + allow(context).to receive(:max_includes).and_return(2) + expect { process }.not_to raise_error + end + end end end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 9d0e57d4292..b3115617084 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -2,9 +2,7 @@ require 'spec_helper' -# This will be use with the FF ci_refactoring_external_mapper_verifier in the next MR. -# It can be removed when the FF is removed. -RSpec.shared_context 'gitlab_ci_config_external_mapper' do +RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do include StubRequests include RepoHelpers @@ -124,7 +122,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do end it 'returns ambigious specification error' do - expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, '`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are: `local`, `project`, `remote`, `template`, `artifact`') + expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /`{"invalid":"secret-file.yml"}` does not have a valid subkey for include. Valid subkeys are:/) end end @@ -138,7 +136,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do end it 'returns ambigious specification error' do - expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, 'Each include must use only one of: `local`, `project`, `remote`, `template`, `artifact`') + expect { subject }.to raise_error(described_class::AmbigiousSpecificationError, /Each include must use only one of/) end end @@ -168,7 +166,7 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do an_instance_of(Gitlab::Ci::Config::External::File::Project)) end - it_behaves_like 'logging config file fetch', 'config_file_fetch_project_content_duration_s', 2 + it_behaves_like 'logging config file fetch', 'config_file_fetch_project_content_duration_s', 1 end end @@ -232,9 +230,20 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do expect { process }.not_to raise_error end - it 'has expanset with one' do + it 'has expanset with two' do process - expect(context.expandset.size).to eq(1) + expect(context.expandset.size).to eq(2) + end + + context 'when FF ci_includes_count_duplicates is disabled' do + before do + stub_feature_flags(ci_includes_count_duplicates: false) + end + + it 'has expanset with one' do + process + expect(context.expandset.size).to eq(1) + end end end @@ -464,7 +473,3 @@ RSpec.shared_context 'gitlab_ci_config_external_mapper' do end end end - -RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do - it_behaves_like 'gitlab_ci_config_external_mapper' -end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index c9efaf2e1af..bb65c2ef10c 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -52,7 +52,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, - "Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!" + "Local file `lib/gitlab/ci/templates/non-existent-file.yml` does not exist!" ) end end @@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, - "Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" + "Included file `lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" ) end end @@ -313,7 +313,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel expect(context.includes).to contain_exactly( { type: :local, - location: '/local/file.yml', + location: 'local/file.yml', blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/local/file.yml", raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/local/file.yml", extra: {}, @@ -334,14 +334,14 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel context_project: project.full_path, context_sha: sha }, { type: :file, - location: '/templates/my-workflow.yml', + location: 'templates/my-workflow.yml', blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-workflow.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-workflow.yml", extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: sha }, { type: :local, - location: '/templates/my-build.yml', + location: 'templates/my-build.yml', blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml", extra: {}, @@ -400,6 +400,44 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel end end + describe 'include:component' do + let(:values) do + { + include: { component: "#{Gitlab.config.gitlab.host}/#{another_project.full_path}/component-x@master" }, + image: 'image:1.0' + } + end + + let(:other_project_files) do + { + '/component-x/template.yml' => <<~YAML + component_x_job: + script: echo Component X + YAML + } + end + + before do + another_project.add_developer(user) + end + + it 'appends the file to the values' do + output = processor.perform + expect(output.keys).to match_array([:image, :component_x_job]) + end + + context 'when feature flag ci_include_components is disabled' do + before do + stub_feature_flags(ci_include_components: false) + end + + it 'returns an error' do + expect { processor.perform } + .to raise_error(described_class::IncludeError, /does not have a valid subkey for include./) + end + end + end + context 'when a valid project file is defined' do let(:values) do { @@ -465,7 +503,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel expect(context.includes).to contain_exactly( { type: :file, - location: '/templates/my-build.yml', + location: 'templates/my-build.yml', blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-build.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-build.yml", extra: { project: another_project.full_path, ref: 'HEAD' }, @@ -474,7 +512,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel { type: :file, blob: "http://localhost/#{another_project.full_path}/-/blob/#{another_project.commit.sha}/templates/my-test.yml", raw: "http://localhost/#{another_project.full_path}/-/raw/#{another_project.commit.sha}/templates/my-test.yml", - location: '/templates/my-test.yml', + location: 'templates/my-test.yml', extra: { project: another_project.full_path, ref: 'HEAD' }, context_project: project.full_path, context_sha: sha } diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index e2bb55f3854..227b62d8ce8 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Rules do +RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_authoring do let(:rule_hashes) {} subject(:rules) { described_class.new(rule_hashes) } diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb new file mode 100644 index 00000000000..4b34553f55e --- /dev/null +++ b/spec/lib/gitlab/ci/config/yaml_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do + describe '.load!' do + it 'loads a single-doc YAML file' do + yaml = <<~YAML + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + image: 'image:1.0', + texts: { + nested_key: 'value1', + more_text: { + more_nested_key: 'value2' + } + } + }) + end + + it 'loads the first document from a multi-doc YAML file' do + yaml = <<~YAML + spec: + inputs: + test_input: + --- + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + spec: { + inputs: { + test_input: nil + } + } + }) + end + + context 'when ci_multi_doc_yaml is disabled' do + before do + stub_feature_flags(ci_multi_doc_yaml: false) + end + + it 'loads a single-doc YAML file' do + yaml = <<~YAML + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + image: 'image:1.0', + texts: { + nested_key: 'value1', + more_text: { + more_nested_key: 'value2' + } + } + }) + end + + it 'loads the first document from a multi-doc YAML file' do + yaml = <<~YAML + spec: + inputs: + test_input: + --- + image: 'image:1.0' + texts: + nested_key: 'value1' + more_text: + more_nested_key: 'value2' + YAML + + config = described_class.load!(yaml) + + expect(config).to eq({ + spec: { + inputs: { + test_input: nil + } + } + }) + end + end + end +end diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb index 4b750cf3bcf..2c07e4d2224 100644 --- a/spec/lib/gitlab/ci/cron_parser_spec.rb +++ b/spec/lib/gitlab/ci/cron_parser_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Gitlab::Ci::CronParser do end end - context 'when slash used' do + context 'when */ used' do let(:cron) { '*/10 */6 */10 */10 *' } let(:cron_timezone) { 'UTC' } @@ -63,7 +63,7 @@ RSpec.describe Gitlab::Ci::CronParser do end end - context 'when range and slash used' do + context 'when range and / are used' do let(:cron) { '3-59/10 * * * *' } let(:cron_timezone) { 'UTC' } @@ -74,6 +74,17 @@ RSpec.describe Gitlab::Ci::CronParser do end end + context 'when / is used' do + let(:cron) { '3/10 * * * *' } + let(:cron_timezone) { 'UTC' } + + it_behaves_like returns_time_for_epoch + + it 'returns specific time' do + expect(subject.min).to be_in([3, 13, 23, 33, 43, 53]) + end + end + context 'when cron_timezone is TZInfo format' do before do allow(Time).to receive(:zone) diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/interpolation/access_spec.rb new file mode 100644 index 00000000000..9f6108a328d --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/access_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_authoring do + subject { described_class.new(access, ctx) } + + let(:access) do + 'inputs.data' + end + + let(:ctx) do + { inputs: { data: 'abcd' }, env: { 'ENV' => 'dev' } } + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abcd' + end + + context 'when there are too many objects in the access path' do + let(:access) { 'a.b.c.d.e.f.g.h' } + + it 'only support MAX_ACCESS_OBJECTS steps' do + expect(subject.objects.count).to eq 5 + end + end + + context 'when access expression size is too large' do + before do + stub_const("#{described_class}::MAX_ACCESS_BYTESIZE", 10) + end + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to eq 'maximum interpolation expression size exceeded' + end + end + + context 'when there are not enough objects in the access path' do + let(:access) { 'abc[123]' } + + it 'returns an error when there are no objects found' do + expect(subject).not_to be_valid + expect(subject.errors.first) + .to eq 'invalid interpolation access pattern' + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb new file mode 100644 index 00000000000..7f2be505d17 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_authoring do + subject { described_class.new(block, data, ctx) } + + let(:data) do + 'inputs.data' + end + + let(:block) do + "$[[ #{data} ]]" + end + + let(:ctx) do + { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } } + end + + it 'knows its content' do + expect(subject.content).to eq 'inputs.data' + end + + it 'properly evaluates the access pattern' do + expect(subject.value).to eq 'abc' + end + + describe '.match' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 ]] $[[ access2 ]]', &b) } + .to yield_successive_args(['$[[ access1 ]]', 'access1'], ['$[[ access2 ]]', 'access2']) + end + + it 'matches an empty block' do + expect { |b| described_class.match('$[[]]', &b) } + .to yield_with_args('$[[]]', '') + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb new file mode 100644 index 00000000000..e5987776e00 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/config_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_authoring do + subject { described_class.new(YAML.safe_load(config)) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + describe '#replace!' do + it 'replaces each od the nodes with a block return value' do + result = subject.replace! { |node| "abc#{node}cde" } + + expect(result).to eq({ + 'abctestcde' => { 'abcspeccde' => { 'abcenvcde' => 'abc$[[ inputs.env ]]cde' } }, + 'abc$[[ inputs.key ]]cde' => { + 'abcnamecde' => 'abc$[[ inputs.key ]]cde', + 'abcscriptcde' => 'abcmy-valuecde' + } + }) + end + end + + context 'when config size is exceeded' do + before do + stub_const("#{described_class}::MAX_NODES", 7) + end + + it 'returns a config size error' do + replaced = 0 + + subject.replace! { replaced += 1 } + + expect(replaced).to eq 4 + expect(subject.errors.size).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/interpolation/context_spec.rb new file mode 100644 index 00000000000..ada896f4980 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/context_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_authoring do + subject { described_class.new(ctx) } + + let(:ctx) do + { inputs: { key: 'abc' } } + end + + describe '#depth' do + it 'returns a max depth of the hash' do + expect(subject.depth).to eq 2 + end + end + + context 'when interpolation context is too complex' do + let(:ctx) do + { inputs: { key: { aaa: { bbb: 'ccc' } } } } + end + + it 'raises an exception' do + expect { described_class.new(ctx) } + .to raise_error(described_class::ContextTooComplexError) + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/interpolation/template_spec.rb new file mode 100644 index 00000000000..8a243b4db05 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/template_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_authoring do + subject { described_class.new(YAML.safe_load(config), ctx) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + CFG + end + + let(:ctx) do + { inputs: { env: 'dev', key: 'abc' } } + end + + it 'collects interpolation blocks' do + expect(subject.size).to eq 2 + end + + it 'interpolates the values properly' do + expect(subject.interpolated).to eq YAML.safe_load <<~RESULT + test: + spec: + env: dev + + abc: + name: abc + script: my-value + RESULT + end + + context 'when interpolation can not be performed' do + let(:config) { '$[[ xxx.yyy ]]: abc' } + + it 'does not interpolate the config' do + expect(subject).not_to be_valid + expect(subject.interpolated).to be_nil + end + end + + context 'when template consists of nested arrays with hashes and values' do + let(:config) do + <<~CFG + test: + - a-$[[ inputs.key ]]-b + - c-$[[ inputs.key ]]-d: + d-$[[ inputs.key ]]-e + val: 1 + CFG + end + + it 'performs a valid interpolation' do + result = { 'test' => ['a-abc-b', { 'c-abc-d' => 'd-abc-e', 'val' => 1 }] } + + expect(subject).to be_valid + expect(subject.interpolated).to eq result + end + end + + context 'when template contains symbols that need interpolation' do + subject do + described_class.new({ '$[[ inputs.key ]]'.to_sym => 'cde' }, ctx) + end + + it 'performs a valid interpolation' do + expect(subject).to be_valid + expect(subject.interpolated).to eq({ 'abc' => 'cde' }) + end + end + + context 'when template is too large' do + before do + stub_const('Gitlab::Ci::Interpolation::Config::MAX_NODES', 1) + end + + it 'returns an error' do + expect(subject.interpolated).to be_nil + expect(subject.errors.count).to eq 1 + expect(subject.errors.first).to eq 'config too large' + end + end + + context 'when there are too many interpolation blocks' do + before do + stub_const("#{described_class}::MAX_BLOCKS", 1) + end + + it 'returns an error' do + expect(subject.interpolated).to be_nil + expect(subject.errors.count).to eq 1 + expect(subject.errors.first).to eq 'too many interpolation blocks' + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb index 30bcce21be2..6772c62ab93 100644 --- a/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb +++ b/spec/lib/gitlab/ci/parsers/instrumentation_spec.rb @@ -8,14 +8,14 @@ RSpec.describe Gitlab::Ci::Parsers::Instrumentation do Class.new do prepend Gitlab::Ci::Parsers::Instrumentation - def parse!(arg1, arg2) + def parse!(arg1, arg2:) "parse #{arg1} #{arg2}" end end end it 'sets metrics for duration of parsing' do - result = parser_class.new.parse!('hello', 'world') + result = parser_class.new.parse!('hello', arg2: 'world') expect(result).to eq('parse hello world') diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 03cab021c17..5d2d22c04fc 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -203,24 +203,35 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do end context 'and name is not provided' do - context 'when CVE identifier exists' do - it 'combines identifier with location to create name' do + context 'when location does not exist' do + let(:location) { nil } + + it 'returns only identifier name' do finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' } - expect(finding.name).to eq("CVE-2017-11429 in yarn.lock") + expect(finding.name).to eq("CVE-2017-11429") end end - context 'when CWE identifier exists' do - it 'combines identifier with location to create name' do - finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' } - expect(finding.name).to eq("CWE-2017-11429 in yarn.lock") + context 'when location exists' do + context 'when CVE identifier exists' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' } + expect(finding.name).to eq("CVE-2017-11429 in yarn.lock") + end + end + + context 'when CWE identifier exists' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' } + expect(finding.name).to eq("CWE-2017-11429 in yarn.lock") + end end - end - context 'when neither CVE nor CWE identifier exist' do - it 'combines identifier with location to create name' do - finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' } - expect(finding.name).to eq("other-2017-11429 in yarn.lock") + context 'when neither CVE nor CWE identifier exist' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' } + expect(finding.name).to eq("other-2017-11429 in yarn.lock") + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index 16deeb6916f..31bffcbeb2a 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -2,208 +2,20 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do +RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } - - let(:prev_pipeline) { create(:ci_pipeline, project: project) } - let(:new_commit) { create(:commit, project: project) } - let(:pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) } - - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) - end - - let(:step) { described_class.new(pipeline, command) } - - before do - create(:ci_build, :interruptible, :running, pipeline: prev_pipeline) - create(:ci_build, :interruptible, :success, pipeline: prev_pipeline) - create(:ci_build, :created, pipeline: prev_pipeline) - - create(:ci_build, :interruptible, pipeline: pipeline) - end + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) } + let_it_be(:step) { described_class.new(pipeline, command) } describe '#perform!' do subject(:perform) { step.perform! } - before do - expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') - expect(build_statuses(pipeline)).to contain_exactly('pending') - end - - context 'when auto-cancel is enabled' do - before do - project.update!(auto_cancel_pending_pipelines: 'enabled') - end - - it 'cancels only previous interruptible builds' do - perform - - expect(build_statuses(prev_pipeline)).to contain_exactly('canceled', 'success', 'canceled') - expect(build_statuses(pipeline)).to contain_exactly('pending') - end - - it 'logs canceled pipelines' do - allow(Gitlab::AppLogger).to receive(:info) - - perform - - expect(Gitlab::AppLogger).to have_received(:info).with( - class: described_class.name, - message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}", - canceled_pipeline_id: prev_pipeline.id, - canceled_by_pipeline_id: pipeline.id, - canceled_by_pipeline_source: pipeline.source - ) - end - - it 'cancels the builds with 2 queries to avoid query timeout' do - second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/ - recorder = ActiveRecord::QueryRecorder.new { perform } - second_query = recorder.occurrences.keys.filter { |occ| occ =~ second_query_regex } - - expect(second_query).to be_one - end - - context 'when the previous pipeline has a child pipeline' do - let(:child_pipeline) { create(:ci_pipeline, child_of: prev_pipeline) } - - context 'when the child pipeline has interruptible running jobs' do - before do - create(:ci_build, :interruptible, :running, pipeline: child_pipeline) - create(:ci_build, :interruptible, :running, pipeline: child_pipeline) - end - - it 'cancels all child pipeline builds' do - expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running') - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled') - end - - context 'when the child pipeline includes completed interruptible jobs' do - before do - create(:ci_build, :interruptible, :failed, pipeline: child_pipeline) - create(:ci_build, :interruptible, :success, pipeline: child_pipeline) - end - - it 'cancels all child pipeline builds with a cancelable_status' do - expect(build_statuses(child_pipeline)).to contain_exactly('running', 'running', 'failed', 'success') - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled', 'failed', 'success') - end - end - end - - context 'when the child pipeline has started non-interruptible job' do - before do - create(:ci_build, :interruptible, :running, pipeline: child_pipeline) - # non-interruptible started - create(:ci_build, :success, pipeline: child_pipeline) - end + it 'enqueues CancelRedundantPipelinesWorker' do + expect(Ci::CancelRedundantPipelinesWorker).to receive(:perform_async).with(pipeline.id) - it 'does not cancel any child pipeline builds' do - expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('running', 'success') - end - end - - context 'when the child pipeline has non-interruptible non-started job' do - before do - create(:ci_build, :interruptible, :running, pipeline: child_pipeline) - end - - not_started_statuses = Ci::HasStatus::AVAILABLE_STATUSES - Ci::HasStatus::STARTED_STATUSES - context 'when the jobs are cancelable' do - cancelable_not_started_statuses = Set.new(not_started_statuses).intersection(Ci::HasStatus::CANCELABLE_STATUSES) - cancelable_not_started_statuses.each do |status| - it "cancels all child pipeline builds when build status #{status} included" do - # non-interruptible but non-started - create(:ci_build, status.to_sym, pipeline: child_pipeline) - - expect(build_statuses(child_pipeline)).to contain_exactly('running', status) - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('canceled', 'canceled') - end - end - end - - context 'when the jobs are not cancelable' do - not_cancelable_not_started_statuses = not_started_statuses - Ci::HasStatus::CANCELABLE_STATUSES - not_cancelable_not_started_statuses.each do |status| - it "does not cancel child pipeline builds when build status #{status} included" do - # non-interruptible but non-started - create(:ci_build, status.to_sym, pipeline: child_pipeline) - - expect(build_statuses(child_pipeline)).to contain_exactly('running', status) - - perform - - expect(build_statuses(child_pipeline)).to contain_exactly('canceled', status) - end - end - end - end - end - - context 'when the pipeline is a child pipeline' do - let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) } - let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } - - before do - create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) - create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) - end - - it 'does not cancel any builds' do - expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') - expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') - - perform - - expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') - expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') - end - end - - context 'when the previous pipeline source is webide' do - let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) } - - it 'does not cancel builds of the previous pipeline' do - perform - - expect(build_statuses(prev_pipeline)).to contain_exactly('created', 'running', 'success') - expect(build_statuses(pipeline)).to contain_exactly('pending') - end - end + subject end - - context 'when auto-cancel is disabled' do - before do - project.update!(auto_cancel_pending_pipelines: 'disabled') - end - - it 'does not cancel any build' do - subject - - expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') - expect(build_statuses(pipeline)).to contain_exactly('pending') - end - end - end - - private - - def build_statuses(pipeline) - pipeline.builds.pluck(:status) end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb deleted file mode 100644 index bec80a43a76..00000000000 --- a/spec/lib/gitlab/ci/pipeline/chain/create_deployments_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Pipeline::Chain::CreateDeployments, feature_category: :continuous_integration do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:user) { create(:user) } - - let(:stage) { build(:ci_stage, project: project, statuses: [job]) } - let(:pipeline) { create(:ci_pipeline, project: project, stages: [stage]) } - - let(:command) do - Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) - end - - let(:step) { described_class.new(pipeline, command) } - - describe '#perform!' do - subject { step.perform! } - - before do - stub_feature_flags(move_create_deployments_to_worker: false) - job.pipeline = pipeline - end - - context 'when a pipeline contains a deployment job' do - let!(:job) { build(:ci_build, :start_review_app, project: project) } - let!(:environment) { create(:environment, project: project, name: job.expanded_environment_name) } - - it 'creates a deployment record' do - expect { subject }.to change { Deployment.count }.by(1) - - job.reset - expect(job.deployment.project).to eq(job.project) - expect(job.deployment.ref).to eq(job.ref) - expect(job.deployment.sha).to eq(job.sha) - expect(job.deployment.deployable).to eq(job) - expect(job.deployment.deployable_type).to eq('CommitStatus') - expect(job.deployment.environment).to eq(job.persisted_environment) - end - - context 'when the corresponding environment does not exist' do - let!(:environment) {} - - it 'does not create a deployment record' do - expect { subject }.not_to change { Deployment.count } - - expect(job.deployment).to be_nil - end - end - end - - context 'when a pipeline contains a teardown job' do - let!(:job) { build(:ci_build, :stop_review_app, project: project) } - let!(:environment) { create(:environment, name: job.expanded_environment_name) } - - it 'does not create a deployment record' do - expect { subject }.not_to change { Deployment.count } - - expect(job.deployment).to be_nil - end - end - - context 'when a pipeline does not contain a deployment job' do - let!(:job) { build(:ci_build, project: project) } - - it 'does not create any deployments' do - expect { subject }.not_to change { Deployment.count } - end - end - end -end diff --git a/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb new file mode 100644 index 00000000000..b955d0e7cee --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/metrics_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Chain::Metrics, feature_category: :continuous_integration do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let_it_be(:pipeline) do + create(:ci_pipeline, project: project, ref: 'master', user: user, name: 'Build pipeline') + end + + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + origin_ref: 'master') + end + + let(:step) { described_class.new(pipeline, command) } + + subject(:run_chain) { step.perform! } + + it 'does not break the chain' do + run_chain + + expect(step.break?).to be false + end + + context 'with pipeline name' do + it 'creates snowplow event' do + run_chain + + expect_snowplow_event( + category: described_class.to_s, + action: 'create_pipeline_with_name', + project: pipeline.project, + user: pipeline.user, + namespace: pipeline.project.namespace + ) + end + end + + context 'without pipeline name' do + let_it_be(:pipeline) do + create(:ci_pipeline, project: project, ref: 'master', user: user) + end + + it 'does not create snowplow event' do + run_chain + + expect_no_snowplow_event + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 9373888aada..df18e1e4f48 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :pipeline_execution do +RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities, feature_category: :continuous_integration do let(:project) { create(:project, :test_repo) } let_it_be(:user) { create(:user) } diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb index 47f172922a5..1a622000c1b 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'support/helpers/stubbed_feature' -require 'support/helpers/stub_feature_flags' -require_dependency 're2' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do +RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches, feature_category: :continuous_integration do include StubFeatureFlags let(:left) { double('left') } diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb index 9e7ea3e4ea4..a60b00457fb 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' -require 'support/helpers/stubbed_feature' -require 'support/helpers/stub_feature_flags' -require_dependency 're2' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do +RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches, feature_category: :continuous_integration do include StubFeatureFlags let(:left) { double('left') } diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 1f7f800e238..3043d7f5381 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -12,953 +12,860 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au let(:attributes) { { name: 'rspec', ref: 'master', scheduling_type: :stage, when: 'on_success' } } let(:previous_stages) { [] } let(:current_stage) { instance_double(Gitlab::Ci::Pipeline::Seed::Stage, seeds_names: [attributes[:name]]) } - let(:current_ci_stage) { build(:ci_stage, pipeline: pipeline) } - let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage], current_ci_stage) } + let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) } - shared_examples 'build seed' do - describe '#attributes' do - subject { seed_build.attributes } + describe '#attributes' do + subject { seed_build.attributes } - it { is_expected.to be_a(Hash) } - it { is_expected.to include(:name, :project, :ref) } + 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' } } + context 'with job:when' do + let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } } - it { is_expected.to include(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', options: { start_in: '3 hours' } } } + + it { is_expected.to include(when: 'delayed', options: { 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 'with job:when:delayed' do - let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', options: { start_in: '3 hours' } } } + 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 'with job:rules:[when:]' do - context 'is matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } } + 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 + let(:base_attributes) { { name: 'rspec', ref: 'master' } } - it { is_expected.to include(when: 'always') } + context 'with a manual job' do + context 'with a matched rule' do + let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) } + + it { is_expected.to include(when: 'manual') } end context 'is not matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } } + let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) } it { is_expected.to include(when: 'never') } end end - context 'with job:rules:[when:delayed]' do + context 'with an automatic job' do context 'is matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } } + let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) } - it { is_expected.to include(when: 'delayed', options: { start_in: '3 hours' }) } + it { is_expected.to include(when: 'on_success') } end context 'is not matched' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } } + let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) } it { is_expected.to include(when: 'never') } end end + end - context 'with job: rules but no explicit when:' do - let(:base_attributes) { { name: 'rspec', ref: 'master' } } - - context 'with a manual job' do - context 'with a matched rule' do - let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR == null' }]) } - - it { is_expected.to include(when: 'manual') } - end - - context 'is not matched' do - let(:attributes) { base_attributes.merge(when: 'manual', rules: [{ if: '$VAR != null' }]) } - - it { is_expected.to include(when: 'never') } - end - end + context 'with job:rules:[variables:]' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + job_variables: [{ key: 'VAR1', value: 'var 1' }, + { key: 'VAR2', value: 'var 2' }], + rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } + end - context 'with an automatic job' do - context 'is matched' do - let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR == null' }]) } + it do + is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR2', value: 'var 2' }]) + end + end - it { is_expected.to include(when: 'on_success') } - end + context 'with job:tags' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + job_variables: [{ key: 'VARIABLE', value: 'value' }], + tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] + } + end - context 'is not matched' do - let(:attributes) { base_attributes.merge(when: 'on_success', rules: [{ if: '$VAR != null' }]) } + it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } + it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } + end - 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 - context 'with job:rules:[variables:]' do + it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) } + + context 'with cache:key:files' do let(:attributes) do - { name: 'rspec', + { + name: 'rspec', ref: 'master', - job_variables: [{ key: 'VAR1', value: 'var 1' }, - { key: 'VAR2', value: 'var 2' }], - rules: [{ if: '$VAR == null', variables: { VAR1: 'new var 1', VAR3: 'var 3' } }] } + cache: [{ + key: { + files: ['VERSION'] + } + }] + } end - it do - is_expected.to include(yaml_variables: [{ key: 'VAR1', value: 'new var 1' }, - { key: 'VAR3', value: 'var 3' }, - { key: 'VAR2', value: 'var 2' }]) - end + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')] + } + } - it 'expects the same results on to_resource' do - expect(seed_build.to_resource.yaml_variables).to include({ key: 'VAR1', value: 'new var 1' }, - { key: 'VAR3', value: 'var 3' }, - { key: 'VAR2', value: 'var 2' }) + is_expected.to include(cache_options) end end - context 'with job:tags' do + context 'with cache:key:prefix' do let(:attributes) do { name: 'rspec', ref: 'master', - job_variables: [{ key: 'VARIABLE', value: 'value' }], - tag_list: ['static-tag', '$VARIABLE', '$NO_VARIABLE'] + cache: [{ + key: { + prefix: 'something' + } + }] } end - it { is_expected.to include(tag_list: ['static-tag', 'value', '$NO_VARIABLE']) } - it { is_expected.to include(yaml_variables: [{ key: 'VARIABLE', value: 'value' }]) } + it { is_expected.to include(options: { cache: [a_hash_including( key: 'something-default' )] }) } end - context 'with cache:key' do + context 'with cache:key:files and prefix' do let(:attributes) do { name: 'rspec', ref: 'master', cache: [{ - key: 'a-value' + key: { + files: ['VERSION'], + prefix: 'something' + } }] } end - it { is_expected.to include(options: { cache: [a_hash_including(key: 'a-value')] }) } - - 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: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')] - } + it 'includes cache options' do + cache_options = { + options: { + cache: [a_hash_including(key: 'something-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: [a_hash_including( key: 'something-default' )] }) } + is_expected.to include(cache_options) end + end + end - context 'with cache:key:files and prefix' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: [{ - key: { - files: ['VERSION'], - prefix: 'something' - } - }] - } - end + context 'with empty cache' do + let(:attributes) do + { + name: 'rspec', + ref: 'master', + cache: {} + } + end - it 'includes cache options' do - cache_options = { - options: { - cache: [a_hash_including(key: 'something-f155568ad0933d8358f66b846133614f76dd0ca4')] - } - } + it { is_expected.to include({}) } + end - is_expected.to include(cache_options) - end - end + context 'with allow_failure' do + let(:options) do + { allow_failure_criteria: { exit_codes: [42] } } end - context 'with empty cache' do - let(:attributes) do - { - name: 'rspec', - ref: 'master', - cache: {} - } - end + let(:rules) do + [{ if: '$VAR == null', when: 'always' }] + end - it { is_expected.to include({}) } + let(:attributes) do + { + name: 'rspec', + ref: 'master', + options: options, + rules: rules + } end - context 'with allow_failure' do - let(:options) do - { allow_failure_criteria: { exit_codes: [42] } } - end + context 'when rules does not override allow_failure' do + it { is_expected.to match a_hash_including(options: options) } + end + context 'when rules set allow_failure to true' do let(:rules) do - [{ if: '$VAR == null', when: 'always' }] + [{ if: '$VAR == null', when: 'always', allow_failure: true }] end - let(:attributes) do - { - name: 'rspec', - ref: 'master', - options: options, - rules: rules - } - end - - context 'when rules does not override allow_failure' do - it { is_expected.to match a_hash_including(options: options) } - end - - context 'when rules set allow_failure to true' do - let(:rules) do - [{ if: '$VAR == null', when: 'always', allow_failure: true }] - end - - it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } - - context 'when options contain other static values' do - let(:options) do - { image: 'busybox', allow_failure_criteria: { exit_codes: [42] } } - end - - it { is_expected.to match a_hash_including(options: { image: 'busybox', allow_failure_criteria: nil }) } + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } + end - it 'deep merges options when exporting to_resource' do - expect(seed_build.to_resource.options).to match a_hash_including( - image: 'busybox', allow_failure_criteria: nil - ) - end - end + context 'when rules set allow_failure to false' do + let(:rules) do + [{ if: '$VAR == null', when: 'always', allow_failure: false }] end - context 'when rules set allow_failure to false' do - let(:rules) do - [{ if: '$VAR == null', when: 'always', allow_failure: false }] - end - - it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } - end + it { is_expected.to match a_hash_including(options: { allow_failure_criteria: nil }) } end + end - context 'with workflow:rules:[variables:]' do - let(:attributes) do - { name: 'rspec', - ref: 'master', - yaml_variables: [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }], - job_variables: [{ key: 'VAR2', value: 'var 2' }, + context 'with workflow:rules:[variables:]' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + yaml_variables: [{ key: 'VAR2', value: 'var 2' }, { key: 'VAR3', value: 'var 3' }], - root_variables_inheritance: root_variables_inheritance } - end - - context 'when the pipeline has variables' do - let(:root_variables) do - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var pipeline 2' }, - { key: 'VAR3', value: 'var pipeline 3' }, - { key: 'VAR4', value: 'new var pipeline 4' }] - end - - context 'when root_variables_inheritance is true' do - let(:root_variables_inheritance) { true } + job_variables: [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }], + root_variables_inheritance: root_variables_inheritance } + end - it 'returns calculated yaml variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }, - { key: 'VAR4', value: 'new var pipeline 4' }] - ) - end - end + context 'when the pipeline has variables' do + let(:root_variables) do + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var pipeline 2' }, + { key: 'VAR3', value: 'var pipeline 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] + end - context 'when root_variables_inheritance is false' do - let(:root_variables_inheritance) { false } + context 'when root_variables_inheritance is true' do + let(:root_variables_inheritance) { true } - it 'returns job variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }] - ) - end + it 'returns calculated yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }, + { key: 'VAR4', value: 'new var pipeline 4' }] + ) end + end - context 'when root_variables_inheritance is an array' do - let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) } + context 'when root_variables_inheritance is false' do + let(:root_variables_inheritance) { false } - it 'returns calculated yaml variables' do - expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR1', value: 'var overridden pipeline 1' }, - { key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }] - ) - end + it 'returns job variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] + ) end end - context 'when the pipeline has not a variable' do - let(:root_variables_inheritance) { true } + context 'when root_variables_inheritance is an array' do + let(:root_variables_inheritance) { %w(VAR1 VAR2 VAR3) } - it 'returns seed yaml variables' do + it 'returns calculated yaml variables' do expect(subject[:yaml_variables]).to match_array( - [{ key: 'VAR2', value: 'var 2' }, - { key: 'VAR3', value: 'var 3' }]) + [{ key: 'VAR1', value: 'var overridden pipeline 1' }, + { key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }] + ) end end end - context 'when the job rule depends on variables' do - let(:attributes) do - { name: 'rspec', - ref: 'master', - yaml_variables: [{ key: 'VAR1', value: 'var 1' }], - job_variables: [{ key: 'VAR1', value: 'var 1' }], - root_variables_inheritance: root_variables_inheritance, - rules: rules } + context 'when the pipeline has not a variable' do + let(:root_variables_inheritance) { true } + + it 'returns seed yaml variables' do + expect(subject[:yaml_variables]).to match_array( + [{ key: 'VAR2', value: 'var 2' }, + { key: 'VAR3', value: 'var 3' }]) end + end + end - let(:root_variables_inheritance) { true } + context 'when the job rule depends on variables' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + yaml_variables: [{ key: 'VAR1', value: 'var 1' }], + job_variables: [{ key: 'VAR1', value: 'var 1' }], + root_variables_inheritance: root_variables_inheritance, + rules: rules } + end - context 'when the rules use job variables' do - let(:rules) do - [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }] - end + let(:root_variables_inheritance) { true } - it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, - { key: 'VAR2', value: 'new var 2' }) - end + context 'when the rules use job variables' do + let(:rules) do + [{ if: '$VAR1 == "var 1"', variables: { VAR1: 'overridden var 1', VAR2: 'new var 2' } }] end - context 'when the rules use root variables' do - let(:root_variables) do - [{ key: 'VAR2', value: 'var pipeline 2' }] - end + it 'recalculates the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'new var 2' }) + end + end - let(:rules) do - [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }] - end + context 'when the rules use root variables' do + let(:root_variables) do + [{ key: 'VAR2', value: 'var pipeline 2' }] + end - it 'recalculates the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, - { key: 'VAR2', value: 'overridden var 2' }) - end + let(:rules) do + [{ if: '$VAR2 == "var pipeline 2"', variables: { VAR1: 'overridden var 1', VAR2: 'overridden var 2' } }] + end - context 'when the root_variables_inheritance is false' do - let(:root_variables_inheritance) { false } + it 'recalculates the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'overridden var 1' }, + { key: 'VAR2', value: 'overridden var 2' }) + end - it 'does not recalculate the variables' do - expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) - end + context 'when the root_variables_inheritance is false' do + let(:root_variables_inheritance) { false } + + it 'does not recalculate the variables' do + expect(subject[:yaml_variables]).to contain_exactly({ key: 'VAR1', value: 'var 1' }) end end end 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 - describe '#bridge?' do - subject { seed_build.bridge? } + it { is_expected.to be_truthy } - context 'when job is a downstream bridge' do + context 'when trigger definition is empty' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } + { name: 'rspec', ref: 'master', options: { trigger: '' } } 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 - it { is_expected.to be_falsey } - end + context 'when job is an upstream bridge' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } end - context 'when job is an upstream bridge' do + it { is_expected.to be_truthy } + + context 'when upstream definition is empty' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } end - it { is_expected.to be_truthy } + it { is_expected.to be_falsey } + end + end - context 'when upstream definition is empty' do - let(:attributes) do - { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } - end + context 'when job is not a bridge' do + it { is_expected.to be_falsey } + end + end - 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_falsey } - end + it 'memoizes a resource object' do + expect(subject.object_id).to eq seed_build.to_resource.object_id end - describe '#to_resource' do - subject { seed_build.to_resource } + it 'can not be persisted without explicit assignment' do + pipeline.save! - it 'memoizes a resource object' do - expect(subject.object_id).to eq seed_build.to_resource.object_id - end + expect(subject).not_to be_persisted + end + end - it 'can not be persisted without explicit assignment' do - pipeline.save! + describe 'applying job inclusion policies' do + subject { seed_build } - expect(subject).not_to be_persisted + context 'when no branch policy is specified' do + let(:attributes) do + { name: 'rspec' } end - end - describe 'applying job inclusion policies' do - subject { seed_build } + it { is_expected.to be_included } + end - context 'when no branch policy is specified' do + context 'when branch policy does not match' do + context 'when using only' do let(:attributes) do - { name: 'rspec' } + { name: 'rspec', only: { refs: ['deploy'] } } end - it { is_expected.to be_included } + it { is_expected.not_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 } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: ['deploy'] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: ['deploy'] } } - end + it { is_expected.to be_included } + end - it { is_expected.to be_included } + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[deploy] }, + except: { refs: %w[deploy] } + } 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 + 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 } + context 'when branch regexp policy does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[/^deploy$/] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[/^deploy$/] } } - end + it { is_expected.not_to be_included } + end - it { is_expected.to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[/^deploy$/] } } 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.to be_included } + end - it { is_expected.not_to be_included } + context 'with both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[/^deploy$/] }, + except: { refs: %w[/^deploy$/] } + } 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.not_to be_included } + end + end - it { is_expected.to be_included } + context 'when branch policy matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[deploy master] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[deploy master] } } - end + it { is_expected.to be_included } + end - it { is_expected.not_to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[deploy master] } } 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 - it { is_expected.not_to be_included } + 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 - 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.not_to be_included } + end + end - it { is_expected.to be_included } + context 'when keyword policy matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[branches] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[branches] } } - end + it { is_expected.to be_included } + end - it { is_expected.not_to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[branches] } } 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 - it { is_expected.not_to be_included } + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[branches] }, + except: { refs: %w[branches] } + } 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 + end - it { is_expected.not_to be_included } + context 'when keyword policy does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[tags] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[tags] } } - end + it { is_expected.not_to be_included } + end - it { is_expected.to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[tags] } } 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.to be_included } + end - it { is_expected.not_to be_included } + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: %w[tags] }, + except: { refs: %w[tags] } + } end - end - context 'with source-keyword policy' do - using RSpec::Parameterized + it { is_expected.not_to be_included } + end + end - let(:pipeline) do - build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project) - end + context 'with source-keyword policy' do + using RSpec::Parameterized - 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 + let(:pipeline) do + build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source, project: project) + end - with_them do - context 'using an only policy' do - let(:attributes) do - { name: 'rspec', only: { refs: [keyword] } } - 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 - it { is_expected.to be_included } + with_them do + context 'using an only policy' do + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } end - context 'using an except policy' do - let(:attributes) do - { name: 'rspec', except: { refs: [keyword] } } - end + it { is_expected.to be_included } + end - it { is_expected.not_to be_included } + context 'using an except policy' do + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } 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 - it { is_expected.not_to be_included } + context 'using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { refs: [keyword] }, + except: { refs: [keyword] } + } 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] } + it { is_expected.not_to be_included } end + end + end - with_them do - context 'using an only policy' do - let(:attributes) do - { name: 'rspec', only: { refs: [keyword] } } - 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 - it { is_expected.not_to be_included } + with_them do + context 'using an only policy' do + let(:attributes) do + { name: 'rspec', only: { refs: [keyword] } } end - context 'using an except policy' do - let(:attributes) do - { name: 'rspec', except: { refs: [keyword] } } - end + it { is_expected.not_to be_included } + end - it { is_expected.to be_included } + context 'using an except policy' do + let(:attributes) do + { name: 'rspec', except: { refs: [keyword] } } end - context 'using both only and except policies' do - let(:attributes) do - { - name: 'rspec', - only: { refs: [keyword] }, - except: { refs: [keyword] } - } - end + it { is_expected.to be_included } + end - it { is_expected.not_to be_included } + 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 } + context 'when repository path matches' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } } end - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } } - end + it { is_expected.to be_included } + end - it { is_expected.not_to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } } 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 - it { is_expected.not_to be_included } + 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 - 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 + it { is_expected.not_to be_included } 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 } + 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 - context 'when using except' do - let(:attributes) do - { name: 'rspec', except: { refs: %w[branches@fork] } } - end + it { is_expected.not_to be_included } + end + end - it { is_expected.to be_included } + context 'when repository path does not match' do + context 'when using only' do + let(:attributes) do + { name: 'rspec', only: { refs: %w[branches@fork] } } 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 - it { is_expected.not_to be_included } + context 'when using except' do + let(:attributes) do + { name: 'rspec', except: { refs: %w[branches@fork] } } end + + it { is_expected.to be_included } end - context 'using rules:' do - using RSpec::Parameterized + 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 - let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } } + it { is_expected.not_to be_included } + end + end - 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 + context 'using rules:' do + using RSpec::Parameterized - with_them do - it { is_expected.not_to be_included } + let(:attributes) { { name: 'rspec', rules: rule_set, when: 'on_success' } } - it 'still correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') - end - end + 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 - 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 } + with_them do + it { is_expected.not_to be_included } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'always') - end + it 'still correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') 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 + 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 - 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 } + 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 + 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 - [ - [[{ if: '$VARIABLE == null' }]], - [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]], - [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]] - ] - 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 } + with_them do + it { is_expected.to be_included } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'on_success') - end + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_failure') end end end - context 'with a matching changes: rule' do - let(:pipeline) do - build(: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 + 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 - context 'with an explicit `when: never`' do - where(:rule_set) do - [ - [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]], - [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]], - [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]] - ] - end - - with_them do - it { is_expected.not_to be_included } + with_them do + it { is_expected.to be_included } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') - end + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'delayed', options: { start_in: '1 day' }) end end + end - context 'with an explicit `when: always`' do - where(:rule_set) do - [ - [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]], - [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]], - [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]] - ] - 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 } + with_them do + it { is_expected.to be_included } - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'always') - end + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') end end + end + end - context 'without an explicit when: value' do - where(:rule_set) do - [ - [[{ changes: { paths: %w[*/**/*.rb] } }]], - [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]], - [[{ changes: { paths: %w[spec/**/*.rb] } }]], - [[{ changes: { paths: %w[*.yml] } }]], - [[{ changes: { paths: %w[.*.yml] } }]], - [[{ changes: { paths: %w[**/*] } }]], - [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]], - [[{ changes: { paths: %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 + context 'with a matching changes: rule' do + let(:pipeline) do + build(: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 no matching 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' }]], - [[{ 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' }]] + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'never' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'always' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'never' }, { changes: { paths: %w[*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'never' }, { changes: { paths: %w[.*.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[**/*] }, when: 'never' }, { changes: { paths: %w[**/*] }, when: 'always' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'never' }, { changes: { paths: %w[.*.yml **/*] }, when: 'always' }]] ] end @@ -971,249 +878,291 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au end end - context 'with a rule using CI_ENVIRONMENT_NAME variable' do - let(:rule_set) do - [{ if: '$CI_ENVIRONMENT_NAME == "test"' }] + context 'with an explicit `when: always`' do + where(:rule_set) do + [ + [[{ changes: { paths: %w[*/**/*.rb] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'always' }, { changes: { paths: %w[app/models/ci/pipeline.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[spec/**/*.rb] }, when: 'always' }, { changes: { paths: %w[spec/**/*.rb] }, when: 'never' }]], + [[{ changes: { paths: %w[*.yml] }, when: 'always' }, { changes: { paths: %w[*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml] }, when: 'always' }, { changes: { paths: %w[.*.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[**/*] }, when: 'always' }, { changes: { paths: %w[**/*] }, when: 'never' }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] }, when: 'always' }, { changes: { paths: %w[*/**/*.rb *.yml] }, when: 'never' }]], + [[{ changes: { paths: %w[.*.yml **/*] }, when: 'always' }, { changes: { paths: %w[.*.yml **/*] }, when: 'never' }]] + ] end - context 'when environment:name satisfies the rule' do - let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success' } } - + with_them do it { is_expected.to be_included } it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'on_success') + expect(seed_build.attributes).to include(when: 'always') end end + end - context 'when environment:name does not satisfy rule' do - let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'dev', when: 'on_success' } } - - it { is_expected.not_to be_included } - - it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') - end + context 'without an explicit when: value' do + where(:rule_set) do + [ + [[{ changes: { paths: %w[*/**/*.rb] } }]], + [[{ changes: { paths: %w[app/models/ci/pipeline.rb] } }]], + [[{ changes: { paths: %w[spec/**/*.rb] } }]], + [[{ changes: { paths: %w[*.yml] } }]], + [[{ changes: { paths: %w[.*.yml] } }]], + [[{ changes: { paths: %w[**/*] } }]], + [[{ changes: { paths: %w[*/**/*.rb *.yml] } }]], + [[{ changes: { paths: %w[.*.yml **/*] } }]] + ] end - context 'when environment:name is not set' do - it { is_expected.not_to be_included } + with_them do + it { is_expected.to be_included } it 'correctly populates when:' do - expect(seed_build.attributes).to include(when: 'never') + expect(seed_build.attributes).to include(when: 'on_success') end end end + end - context 'with no rules' do - let(:rule_set) { [] } + 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 a rule using CI_ENVIRONMENT_NAME variable' do + let(:rule_set) do + [{ if: '$CI_ENVIRONMENT_NAME == "test"' }] + end - context 'with invalid rules raising error' do - let(:rule_set) do - [ - { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } - ] + context 'when environment:name satisfies the rule' do + let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success' } } + + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') end + end + + context 'when environment:name does not satisfy rule' do + let(:attributes) { { name: 'rspec', rules: rule_set, environment: 'dev', when: 'on_success' } } it { is_expected.not_to be_included } it 'correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end + end - it 'returns an error' do - expect(seed_build.errors).to contain_exactly( - 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' - ) + context 'when environment:name is not set' do + 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 } + context 'with no rules' do + let(:rule_set) { [] } - let(:needs_attributes) do - Array.new(needs_count, name: 'build') - end + it { is_expected.not_to be_included } - let(:attributes) do - { - name: 'rspec', - needs_attributes: needs_attributes - } + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end end - context 'when build job is not present in prior stages' do - it "is included" do - is_expected.to be_included + context 'with invalid rules raising error' do + let(:rule_set) do + [ + { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } + ] end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "'rspec' job needs 'build' job, but 'build' is not in any previous stage") - end + it { is_expected.not_to be_included } - context 'when the needed job is optional' do - let(:needs_attributes) { [{ name: 'build', optional: true }] } + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end - it "does not return an error" do - expect(subject.errors).to be_empty - end + it 'returns an error' do + expect(seed_build.errors).to contain_exactly( + 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' + ) end end + end + end - context 'when build job is part of prior stages' do - let(:stage_attributes) do - { - name: 'build', - index: 0, - builds: [{ name: 'build' }] - } - end + describe 'applying needs: dependency' do + subject { seed_build } - let(:stage_seed) do - Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, []) - end + let(:needs_count) { 1 } - let(:previous_stages) { [stage_seed] } + let(:needs_attributes) do + Array.new(needs_count, name: 'build') + end - it "is included" do - is_expected.to be_included - end + let(:attributes) do + { + name: 'rspec', + needs_attributes: needs_attributes + } + end - it "does not have errors" do - expect(subject.errors).to be_empty - end + context 'when build job is not present in prior stages' do + it "is included" do + is_expected.to be_included end - context 'when build job is part of the same stage' do - let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) } + it "returns an error" do + expect(subject.errors).to contain_exactly( + "'rspec' job needs 'build' job, but 'build' is not in any previous stage") + end - it 'is included' do - is_expected.to be_included - end + context 'when the needed job is optional' do + let(:needs_attributes) { [{ name: 'build', optional: true }] } - it 'does not have errors' do + it "does not return an error" do expect(subject.errors).to be_empty end end + end - context 'when using 101 needs' do - let(:needs_count) { 101 } - - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details") - end + context 'when build job is part of prior stages' do + let(:stage_attributes) do + { + name: 'build', + index: 0, + builds: [{ name: 'build' }] + } + end - context 'when ci_needs_size_limit is set to 100' do - before do - project.actual_limits.update!(ci_needs_size_limit: 100) - end + let(:stage_seed) do + Gitlab::Ci::Pipeline::Seed::Stage.new(seed_context, stage_attributes, []) + end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details") - end - end + let(:previous_stages) { [stage_seed] } - context 'when ci_needs_size_limit is set to 0' do - before do - project.actual_limits.update!(ci_needs_size_limit: 0) - end + it "is included" do + is_expected.to be_included + end - it "returns an error" do - expect(subject.errors).to contain_exactly( - "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details") - end - end + it "does not have errors" do + expect(subject.errors).to be_empty end end - describe 'applying pipeline variables' do - subject { seed_build } + context 'when build job is part of the same stage' do + let(:current_stage) { double(seeds_names: [attributes[:name], 'build']) } - let(:pipeline_variables) { [] } - let(:pipeline) do - build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables) + it 'is included' do + is_expected.to be_included end - context 'containing variable references' do - let(:pipeline_variables) do - [ - build(:ci_pipeline_variable, key: 'A', value: '$B'), - build(:ci_pipeline_variable, key: 'B', value: '$C') - ] - end + it 'does not have errors' do + expect(subject.errors).to be_empty + end + end - it "does not have errors" do - expect(subject.errors).to be_empty - end + context 'when using 101 needs' do + let(:needs_count) { 101 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 101. See needs keyword documentation for more details") end - context 'containing cyclic reference' do - let(:pipeline_variables) do - [ - build(:ci_pipeline_variable, key: 'A', value: '$B'), - build(:ci_pipeline_variable, key: 'B', value: '$C'), - build(:ci_pipeline_variable, key: 'C', value: '$A') - ] + context 'when ci_needs_size_limit is set to 100' do + before do + project.actual_limits.update!(ci_needs_size_limit: 100) end it "returns an error" do expect(subject.errors).to contain_exactly( - 'rspec: circular variable reference detected: ["A", "B", "C"]') + "rspec: one job can only need 100 others, but you have listed 101. See needs keyword documentation for more details") end + end - context 'with job:rules:[if:]' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } - - it "included? does not raise" do - expect { subject.included? }.not_to raise_error - end + context 'when ci_needs_size_limit is set to 0' do + before do + project.actual_limits.update!(ci_needs_size_limit: 0) + end - it "included? returns true" do - expect(subject.included?).to eq(true) - end + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 0 others, but you have listed 101. See needs keyword documentation for more details") end end end end - describe 'feature flag ci_reuse_build_in_seed_context' do - let(:attributes) do - { name: 'rspec', rules: [{ if: '$VARIABLE == null' }], when: 'on_success' } + describe 'applying pipeline variables' do + subject { seed_build } + + let(:pipeline_variables) { [] } + let(:pipeline) do + build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables) end - context 'when enabled' do - it_behaves_like 'build seed' + context 'containing variable references' do + let(:pipeline_variables) do + [ + build(:ci_pipeline_variable, key: 'A', value: '$B'), + build(:ci_pipeline_variable, key: 'B', value: '$C') + ] + end - it 'initializes the build once' do - expect(Ci::Build).to receive(:new).once.and_call_original - seed_build.to_resource + it "does not have errors" do + expect(subject.errors).to be_empty end end - context 'when disabled' do - before do - stub_feature_flags(ci_reuse_build_in_seed_context: false) + context 'containing cyclic reference' do + let(:pipeline_variables) do + [ + build(:ci_pipeline_variable, key: 'A', value: '$B'), + build(:ci_pipeline_variable, key: 'B', value: '$C'), + build(:ci_pipeline_variable, key: 'C', value: '$A') + ] + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + 'rspec: circular variable reference detected: ["A", "B", "C"]') end - it_behaves_like 'build seed' + context 'with job:rules:[if:]' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } + + it "included? does not raise" do + expect { subject.included? }.not_to raise_error + end - it 'initializes the build twice' do - expect(Ci::Build).to receive(:new).twice.and_call_original - seed_build.to_resource + it "included? returns true" do + expect(subject.included?).to eq(true) + end end end end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb index 68e70525c55..93644aa1497 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -4,19 +4,25 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Reports::CodequalityReports do let(:codequality_report) { described_class.new } - let(:degradation_1) { build(:codequality_degradation_1) } - let(:degradation_2) { build(:codequality_degradation_2) } + let(:degradation_major) { build(:codequality_degradation, :major) } + let(:degradation_minor) { build(:codequality_degradation, :minor) } + let(:degradation_blocker) { build(:codequality_degradation, :blocker) } + let(:degradation_info) { build(:codequality_degradation, :info) } + let(:degradation_major_2) { build(:codequality_degradation, :major) } + let(:degradation_critical) { build(:codequality_degradation, :critical) } + let(:degradation_uppercase_major) { build(:codequality_degradation, severity: 'MAJOR') } + let(:degradation_unknown) { build(:codequality_degradation, severity: 'unknown') } it { expect(codequality_report.degradations).to eq({}) } describe '#add_degradation' do context 'when there is a degradation' do before do - codequality_report.add_degradation(degradation_1) + codequality_report.add_degradation(degradation_major) end it 'adds degradation to codequality report' do - expect(codequality_report.degradations.keys).to eq([degradation_1[:fingerprint]]) + expect(codequality_report.degradations.keys).to match_array([degradation_major[:fingerprint]]) expect(codequality_report.degradations.values.size).to eq(1) end end @@ -53,8 +59,8 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do context 'when there are many degradations' do before do - codequality_report.add_degradation(degradation_1) - codequality_report.add_degradation(degradation_2) + codequality_report.add_degradation(degradation_major) + codequality_report.add_degradation(degradation_minor) end it 'returns the number of degradations' do @@ -68,36 +74,25 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do context 'when there are many degradations' do before do - codequality_report.add_degradation(degradation_1) - codequality_report.add_degradation(degradation_2) + codequality_report.add_degradation(degradation_major) + codequality_report.add_degradation(degradation_minor) end it 'returns all degradations' do - expect(all_degradations).to contain_exactly(degradation_1, degradation_2) + expect(all_degradations).to contain_exactly(degradation_major, degradation_minor) end end end describe '#sort_degradations!' do - let(:major) { build(:codequality_degradation, :major) } - let(:minor) { build(:codequality_degradation, :minor) } - let(:blocker) { build(:codequality_degradation, :blocker) } - let(:info) { build(:codequality_degradation, :info) } - let(:major_2) { build(:codequality_degradation, :major) } - let(:critical) { build(:codequality_degradation, :critical) } - let(:uppercase_major) { build(:codequality_degradation, severity: 'MAJOR') } - let(:unknown) { build(:codequality_degradation, severity: 'unknown') } - - let(:codequality_report) { described_class.new } - before do - codequality_report.add_degradation(major) - codequality_report.add_degradation(minor) - codequality_report.add_degradation(blocker) - codequality_report.add_degradation(major_2) - codequality_report.add_degradation(info) - codequality_report.add_degradation(critical) - codequality_report.add_degradation(unknown) + codequality_report.add_degradation(degradation_major) + codequality_report.add_degradation(degradation_minor) + codequality_report.add_degradation(degradation_blocker) + codequality_report.add_degradation(degradation_major_2) + codequality_report.add_degradation(degradation_info) + codequality_report.add_degradation(degradation_critical) + codequality_report.add_degradation(degradation_unknown) codequality_report.sort_degradations! end @@ -105,36 +100,70 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do it 'sorts degradations based on severity' do expect(codequality_report.degradations.values).to eq( [ - blocker, - critical, - major, - major_2, - minor, - info, - unknown + degradation_blocker, + degradation_critical, + degradation_major, + degradation_major_2, + degradation_minor, + degradation_info, + degradation_unknown ]) end context 'with non-existence and uppercase severities' do let(:other_report) { described_class.new } - let(:non_existent) { build(:codequality_degradation, severity: 'non-existent') } + let(:degradation_non_existent) { build(:codequality_degradation, severity: 'non-existent') } before do - other_report.add_degradation(blocker) - other_report.add_degradation(uppercase_major) - other_report.add_degradation(minor) - other_report.add_degradation(non_existent) + other_report.add_degradation(degradation_blocker) + other_report.add_degradation(degradation_uppercase_major) + other_report.add_degradation(degradation_minor) + other_report.add_degradation(degradation_non_existent) end it 'sorts unknown last' do expect(other_report.degradations.values).to eq( [ - blocker, - uppercase_major, - minor, - non_existent + degradation_blocker, + degradation_uppercase_major, + degradation_minor, + degradation_non_existent ]) end end end + + describe '#code_quality_report_summary' do + context "when there is no degradation" do + it 'return nil' do + expect(codequality_report.code_quality_report_summary).to eq(nil) + end + end + + context "when there are degradations" do + before do + codequality_report.add_degradation(degradation_major) + codequality_report.add_degradation(degradation_major_2) + codequality_report.add_degradation(degradation_minor) + codequality_report.add_degradation(degradation_blocker) + codequality_report.add_degradation(degradation_info) + codequality_report.add_degradation(degradation_critical) + codequality_report.add_degradation(degradation_unknown) + end + + it 'returns the summary of the code quality report' do + expect(codequality_report.code_quality_report_summary).to eq( + { + 'major' => 2, + 'minor' => 1, + 'blocker' => 1, + 'info' => 1, + 'critical' => 1, + 'unknown' => 1, + 'count' => 7 + } + ) + end + end + end end diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb index 56f69720b87..31c53d4a030 100644 --- a/spec/lib/gitlab/ci/runner_instructions_spec.rb +++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerInstructions do +RSpec.describe Gitlab::Ci::RunnerInstructions, feature_category: :runner_fleet do using RSpec::Parameterized::TableSyntax let(:params) { {} } @@ -29,7 +29,6 @@ RSpec.describe Gitlab::Ci::RunnerInstructions do context name do it 'has the required fields' do expect(subject).to have_key(:human_readable_name) - expect(subject).to have_key(:installation_instructions_url) end end end diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index ad1e9b12b8a..14f3c95ec79 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerReleases do +RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do subject { described_class.instance } let(:runner_releases_url) { 'http://testurl.com/runner_public_releases' } diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index 55c3834bfa7..526d6cba657 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do +RSpec.describe Gitlab::Ci::RunnerUpgradeCheck, feature_category: :runner_fleet do using RSpec::Parameterized::TableSyntax subject(:instance) { described_class.new(gitlab_version, runner_releases) } @@ -51,8 +51,8 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with runner_version from last minor release' do let(:runner_version) { 'v14.0.1' } - it 'returns :not_available' do - is_expected.to eq([parsed_runner_version, :not_available]) + it 'returns :unavailable' do + is_expected.to eq([parsed_runner_version, :unavailable]) end end end @@ -85,8 +85,8 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with a runner_version that is too recent' do let(:runner_version) { 'v14.2.0' } - it 'returns :not_available' do - is_expected.to eq([parsed_runner_version, :not_available]) + it 'returns :unavailable' do + is_expected.to eq([parsed_runner_version, :unavailable]) end end end @@ -96,14 +96,14 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with valid params' do where(:runner_version, :expected_status, :expected_suggested_version) do - 'v15.0.0' | :not_available | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available + 'v15.0.0' | :unavailable | '15.0.0' # not available since the GitLab instance is still on 14.x, a major version might be incompatible, and a patch upgrade is not available 'v14.1.0-rc3' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes 'v14.1.0~beta.1574.gf6ea9389' | :recommended | '14.1.1' # suffixes are correctly handled 'v14.1.0/1.1.0' | :recommended | '14.1.1' # suffixes are correctly handled 'v14.1.0' | :recommended | '14.1.1' # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes 'v14.0.1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available 'v14.0.2-rc1' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate - 'v14.0.2' | :not_available | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version + 'v14.0.2' | :unavailable | '14.0.2' # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version 'v13.10.1' | :available | '14.0.2' # available upgrade: 14.0.2 'v13.10.1~beta.1574.gf6ea9389' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available 'v13.10.1/1.1.0' | :recommended | '13.10.1' # suffixes are correctly handled, official 13.10.1 is available @@ -125,13 +125,13 @@ RSpec.describe Gitlab::Ci::RunnerUpgradeCheck do context 'with valid params' do where(:runner_version, :expected_status, :expected_suggested_version) do - 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible - 'v13.10.1' | :not_available | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x - 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available - 'v13.9.2' | :not_available | '13.9.2' # not_available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version - 'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version - 'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) - 'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) + 'v14.0.0' | :recommended | '14.0.2' # recommended upgrade since 14.0.2 is available, even though the GitLab instance is still on 13.x and a major version might be incompatible + 'v13.10.1' | :unavailable | '13.10.1' # not available since 13.10.1 is already ahead of GitLab instance version and is the latest patch update for 13.10.x + 'v13.10.0' | :recommended | '13.10.1' # recommended upgrade since 13.10.1 is available + 'v13.9.2' | :unavailable | '13.9.2' # not available even though backports are no longer released for this version because the runner is already on the same version as the GitLab version + 'v13.9.0' | :recommended | '13.9.2' # recommended upgrade since backports are no longer released for this version + 'v13.8.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) + 'v11.4.1' | :recommended | '13.9.2' # recommended upgrade since build is too old (missing in records) end with_them do diff --git a/spec/lib/gitlab/ci/status/bridge/common_spec.rb b/spec/lib/gitlab/ci/status/bridge/common_spec.rb index 37524afc83d..fef97c73a91 100644 --- a/spec/lib/gitlab/ci/status/bridge/common_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/common_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Bridge::Common do +RSpec.describe Gitlab::Ci::Status::Bridge::Common, feature_category: :continuous_integration do let_it_be(:user) { create(:user) } let_it_be(:bridge) { create(:ci_bridge) } let_it_be(:downstream_pipeline) { create(:ci_pipeline) } @@ -37,4 +37,35 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Common do it { expect(subject.details_path).to be_nil } end end + + describe '#label' do + let(:description) { 'my description' } + let(:bridge) { create(:ci_bridge, description: description) } + + subject do + Gitlab::Ci::Status::Created + .new(bridge, user) + .extend(described_class) + end + + it 'returns description' do + expect(subject.label).to eq description + end + + context 'when description is nil' do + let(:description) { nil } + + it 'returns core status label' do + expect(subject.label).to eq('created') + end + end + + context 'when description is empty string' do + let(:description) { '' } + + it 'returns core status label' do + expect(subject.label).to eq('created') + end + end + end end diff --git a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb index c13901a4776..040c3ec7f6e 100644 --- a/spec/lib/gitlab/ci/status/bridge/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/bridge/factory_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou expect(status.text).to eq s_('CiStatusText|created') expect(status.icon).to eq 'status_created' expect(status.favicon).to eq 'favicon_status_created' - expect(status.label).to be_nil + expect(status.label).to eq 'created' expect(status).not_to have_details expect(status).not_to have_action end @@ -40,7 +40,8 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Bridge::Failed] + .to eq [Gitlab::Ci::Status::Bridge::Retryable, + Gitlab::Ci::Status::Bridge::Failed] end it 'fabricates a failed bridge status' do @@ -51,10 +52,10 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou expect(status.text).to eq s_('CiStatusText|failed') expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' - expect(status.label).to be_nil + expect(status.label).to eq 'failed' expect(status.status_tooltip).to eq "#{s_('CiStatusText|failed')} - (unknown failure)" expect(status).not_to have_details - expect(status).not_to have_action + expect(status).to have_action end context 'failed with downstream_pipeline_creation_failed' do @@ -130,12 +131,36 @@ RSpec.describe Gitlab::Ci::Status::Bridge::Factory, feature_category: :continuou expect(status.text).to eq 'waiting' expect(status.group).to eq 'waiting-for-resource' expect(status.icon).to eq 'status_pending' - expect(status.favicon).to eq 'favicon_pending' + expect(status.favicon).to eq 'favicon_status_pending' expect(status.illustration).to include(:image, :size, :title) expect(status).not_to have_details end end + context 'when the bridge is successful and therefore retryable' do + let(:bridge) { create(:ci_bridge, :success) } + + it 'matches correct core status' do + expect(factory.core_status).to be_a Gitlab::Ci::Status::Success + end + + it 'matches correct extended statuses' do + expect(factory.extended_statuses) + .to eq [Gitlab::Ci::Status::Bridge::Retryable] + end + + it 'fabricates a retryable build status' do + expect(status).to be_a Gitlab::Ci::Status::Bridge::Retryable + end + + it 'fabricates status with correct details' do + expect(status.text).to eq s_('CiStatusText|passed') + expect(status.icon).to eq 'status_success' + expect(status.favicon).to eq 'favicon_status_success' + expect(status).to have_action + end + end + private def create_bridge(*traits) diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index ade07a54877..2c93f842a30 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -75,7 +75,7 @@ RSpec.describe Gitlab::Ci::Status::Build::Play do end describe '#action_button_title' do - it { expect(subject.action_button_title).to eq 'Trigger this manual action' } + it { expect(subject.action_button_title).to eq 'Run job' } end describe '.matches?' do diff --git a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb index bb6139accaf..6f5ab77a358 100644 --- a/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb +++ b/spec/lib/gitlab/ci/status/waiting_for_resource_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::Ci::Status::WaitingForResource do end describe '#favicon' do - it { expect(subject.favicon).to eq 'favicon_pending' } + it { expect(subject.favicon).to eq 'favicon_status_pending' } end describe '#group' do diff --git a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb index 43deb465025..e4cee379f40 100644 --- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb @@ -2,16 +2,16 @@ require 'spec_helper' -RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do +RSpec.describe '5-Minute-Production-App.gitlab-ci.yml', feature_category: :five_minute_production_app do subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('5-Minute-Production-App') } describe 'the created pipeline' do - let_it_be(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } + let_it_be_with_refind(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.first_owner } let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } - let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) } let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } @@ -24,24 +24,27 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do end context 'when AWS variables are set' do + def create_ci_variable(key, value) + create(:ci_variable, project: project, key: key, value: value) + end + before do - create(:ci_variable, project: project, key: 'AWS_ACCESS_KEY_ID', value: 'AKIAIOSFODNN7EXAMPLE') - create(:ci_variable, project: project, key: 'AWS_SECRET_ACCESS_KEY', value: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY') - create(:ci_variable, project: project, key: 'AWS_DEFAULT_REGION', value: 'us-west-2') + create_ci_variable('AWS_ACCESS_KEY_ID', 'AKIAIOSFODNN7EXAMPLE') + create_ci_variable('AWS_SECRET_ACCESS_KEY', 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY') + create_ci_variable('AWS_DEFAULT_REGION', 'us-west-2') end it 'creates all jobs' do - expect(build_names).to match_array(%w(build terraform_apply deploy terraform_destroy)) + expect(build_names).to match_array(%w[build terraform_apply deploy terraform_destroy]) end - context 'pipeline branch is protected' do + context 'when pipeline branch is protected' do before do create(:protected_branch, project: project, name: pipeline_branch) - project.reload end it 'does not create a destroy job' do - expect(build_names).to match_array(%w(build terraform_apply deploy)) + expect(build_names).to match_array(%w[build terraform_apply deploy]) end end end diff --git a/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..9f4f6b02b0b --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Terraform/module_base_gitlab_ci_yaml_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform/Module-Base.gitlab-ci.yml', feature_category: :continuous_integration do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Module-Base') } + + describe 'the created pipeline' do + let(:default_branch) { 'main' } + let(:pipeline_branch) { default_branch } + let_it_be(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.first_owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch) } + let(:pipeline) { service.execute(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + it 'does not create any jobs' do + expect(build_names).to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..7c3c1776111 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/terraform_module_gitlab_ci_yaml_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform-Module.gitlab-ci.yml', feature_category: :continuous_integration do + before do + allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([]) + end + + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform-Module') } + + shared_examples 'on any branch' do + it 'creates fmt and kics job', :aggregate_failures do + expect(pipeline.errors).to be_empty + expect(build_names).to include('fmt', 'kics-iac-sast') + end + + it 'does not create a deploy job', :aggregate_failures do + expect(pipeline.errors).to be_empty + expect(build_names).not_to include('deploy') + end + end + + let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1', create_tag: '1.0.0') } + let_it_be(:user) { project.first_owner } + + describe 'the created pipeline' do + let(:default_branch) { project.default_branch_or_main } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| + allow(instance).to receive(:perform).and_return(true) + end + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'when on default branch' do + let(:pipeline_ref) { default_branch } + + it_behaves_like 'on any branch' + end + + context 'when outside the default branch' do + let(:pipeline_ref) { 'patch-1' } + + it_behaves_like 'on any branch' + end + + context 'when on tag' do + let(:pipeline_ref) { '1.0.0' } + + it 'creates fmt and deploy job', :aggregate_failures do + expect(pipeline.errors).to be_empty + expect(build_names).to include('fmt', 'deploy') + end + end + end +end diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb index 582c4ad343f..cce6477b91e 100644 --- a/spec/lib/gitlab/ci/trace/archive_spec.rb +++ b/spec/lib/gitlab/ci/trace/archive_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Trace::Archive do +RSpec.describe Gitlab::Ci::Trace::Archive, feature_category: :scalability do context 'with transactional fixtures' do let_it_be_with_reload(:job) { create(:ci_build, :success, :trace_live) } let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) } diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb new file mode 100644 index 00000000000..a5365ae53b8 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -0,0 +1,336 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipeline_authoring do + let_it_be(:project) { create_default(:project, :repository, create_tag: 'test').freeze } + let_it_be(:user) { create(:user) } + + let(:pipeline) { build(:ci_empty_pipeline, :created, project: project) } + + describe '#predefined_variables' do + subject { described_class.new(pipeline).predefined_variables } + + it 'includes all predefined variables in a valid order' do + keys = subject.pluck(:key) + + expect(keys).to contain_exactly(*%w[ + CI_PIPELINE_IID + CI_PIPELINE_SOURCE + CI_PIPELINE_CREATED_AT + CI_COMMIT_SHA + CI_COMMIT_SHORT_SHA + CI_COMMIT_BEFORE_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_COMMIT_BRANCH + CI_COMMIT_MESSAGE + CI_COMMIT_TITLE + CI_COMMIT_DESCRIPTION + CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP + CI_COMMIT_AUTHOR + CI_BUILD_REF + CI_BUILD_BEFORE_SHA + CI_BUILD_REF_NAME + CI_BUILD_REF_SLUG + ]) + end + + context 'when the pipeline is running for a tag' do + let(:pipeline) { build(:ci_empty_pipeline, :created, project: project, ref: 'test', tag: true) } + + it 'includes all predefined variables in a valid order' do + keys = subject.pluck(:key) + + expect(keys).to contain_exactly(*%w[ + CI_PIPELINE_IID + CI_PIPELINE_SOURCE + CI_PIPELINE_CREATED_AT + CI_COMMIT_SHA + CI_COMMIT_SHORT_SHA + CI_COMMIT_BEFORE_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_COMMIT_MESSAGE + CI_COMMIT_TITLE + CI_COMMIT_DESCRIPTION + CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP + CI_COMMIT_AUTHOR + CI_BUILD_REF + CI_BUILD_BEFORE_SHA + CI_BUILD_REF_NAME + CI_BUILD_REF_SLUG + CI_COMMIT_TAG + CI_COMMIT_TAG_MESSAGE + CI_BUILD_TAG + ]) + end + end + + context 'when merge request is present' do + let_it_be(:assignees) { create_list(:user, 2) } + let_it_be(:milestone) { create(:milestone, project: project) } + let_it_be(:labels) { create_list(:label, 2) } + + let(:merge_request) do + create(:merge_request, :simple, + source_project: project, + target_project: project, + assignees: assignees, + milestone: milestone, + labels: labels) + end + + context 'when pipeline for merge request is created' do + let(:pipeline) do + create(:ci_pipeline, :detached_merge_request_pipeline, + ci_ref_presence: false, + user: user, + merge_request: merge_request) + end + + before do + project.add_developer(user) + end + + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?( + merge_request.target_project, + merge_request.target_branch + ).to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '', + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '', + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached', + 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)) + end + + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) + end + + context 'without assignee' do + let(:assignees) { [] } + + it 'does not expose assignee variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') + end + end + + context 'without milestone' do + let(:milestone) { nil } + + it 'does not expose milestone variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE') + end + end + + context 'without labels' do + let(:labels) { [] } + + it 'does not expose labels variable' do + expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') + end + end + end + + context 'when pipeline on branch is created' do + let(:pipeline) do + create(:ci_pipeline, project: project, user: user, ref: 'feature') + end + + context 'when a merge request is created' do + before do + merge_request + end + + context 'when user has access to project' do + before do + project.add_developer(user) + end + + it 'merge request references are returned matching the pipeline' do + expect(subject.to_hash).to include( + 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)) + end + end + + context 'when user does not have access to project' do + it 'CI_OPEN_MERGE_REQUESTS is not returned' do + expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS') + end + end + end + + context 'when no a merge request is created' do + it 'CI_OPEN_MERGE_REQUESTS is not returned' do + expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS') + end + end + end + + context 'with merged results' do + let(:pipeline) do + create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request) + end + + it 'exposes merge request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, + 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, + 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, + 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, + 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path, + 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, + 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?( + merge_request.target_project, + merge_request.target_branch + ).to_s, + 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, + 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, + 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha, + 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, + 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, + 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), + 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result') + end + + it 'exposes diff variables' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, + 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) + end + end + end + + context 'when source is external pull request' do + let(:pipeline) do + create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request) + end + + let(:pull_request) { create(:external_pull_request, project: project) } + + it 'exposes external pull request pipeline variables' do + expect(subject.to_hash) + .to include( + 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s, + 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository, + 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository, + 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha, + 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha, + 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch, + 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch + ) + end + end + + describe 'variable CI_KUBERNETES_ACTIVE' do + context 'when pipeline.has_kubernetes_active? is true' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(true) + end + + it "is included with value 'true'" do + expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true') + end + end + + context 'when pipeline.has_kubernetes_active? is false' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(false) + end + + it 'is not included' do + expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE') + end + end + end + + describe 'variable CI_GITLAB_FIPS_MODE' do + context 'when FIPS flag is enabled' do + before do + allow(Gitlab::FIPS).to receive(:enabled?).and_return(true) + end + + it "is included with value 'true'" do + expect(subject.to_hash).to include('CI_GITLAB_FIPS_MODE' => 'true') + end + end + + context 'when FIPS flag is disabled' do + before do + allow(Gitlab::FIPS).to receive(:enabled?).and_return(false) + end + + it 'is not included' do + expect(subject.to_hash).not_to have_key('CI_GITLAB_FIPS_MODE') + end + end + end + + context 'when tag is not found' do + let(:pipeline) do + create(:ci_pipeline, project: project, ref: 'not_found_tag', tag: true) + end + + it 'does not expose tag variables' do + expect(subject.to_hash.keys) + .not_to include( + 'CI_COMMIT_TAG', + 'CI_COMMIT_TAG_MESSAGE', + 'CI_BUILD_TAG' + ) + end + end + + context 'without a commit' do + let(:pipeline) { build(:ci_empty_pipeline, :created, sha: nil) } + + it 'does not expose commit variables' do + expect(subject.to_hash.keys) + .not_to include( + 'CI_COMMIT_SHA', + 'CI_COMMIT_SHORT_SHA', + 'CI_COMMIT_BEFORE_SHA', + 'CI_COMMIT_REF_NAME', + 'CI_COMMIT_REF_SLUG', + 'CI_COMMIT_BRANCH', + 'CI_COMMIT_TAG', + 'CI_COMMIT_MESSAGE', + 'CI_COMMIT_TITLE', + 'CI_COMMIT_DESCRIPTION', + 'CI_COMMIT_REF_PROTECTED', + 'CI_COMMIT_TIMESTAMP', + 'CI_COMMIT_AUTHOR') + end + end + end +end diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 5aa752ee429..bbd3dc54e6a 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -166,8 +166,14 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur end before do + pipeline_variables_builder = double( + ::Gitlab::Ci::Variables::Builder::Pipeline, + predefined_variables: [var('C', 3), var('D', 3)] + ) + allow(builder).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] } allow(pipeline.project).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] } + allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder } allow(pipeline).to receive(:predefined_variables) { [var('C', 3), var('D', 3)] } allow(job).to receive(:runner) { double(predefined_variables: [var('D', 4), var('E', 4)]) } allow(builder).to receive(:kubernetes_variables) { [var('E', 5), var('F', 5)] } @@ -635,8 +641,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur end before do + pipeline_variables_builder = double( + ::Gitlab::Ci::Variables::Builder::Pipeline, + predefined_variables: [var('B', 2), var('C', 2)] + ) + allow(pipeline.project).to receive(:predefined_variables) { [var('A', 1), var('B', 1)] } - allow(pipeline).to receive(:predefined_variables) { [var('B', 2), var('C', 2)] } + allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder } allow(builder).to receive(:secret_instance_variables) { [var('C', 3), var('D', 3)] } allow(builder).to receive(:secret_group_variables) { [var('D', 4), var('E', 4)] } allow(builder).to receive(:secret_project_variables) { [var('E', 5), var('F', 5)] } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index b9f65ff749d..360686ce65c 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1503,7 +1503,7 @@ module Gitlab end context "when the included internal file is not present" do - it_behaves_like 'returns errors', "Local file `/local.gitlab-ci.yml` does not exist!" + it_behaves_like 'returns errors', "Local file `local.gitlab-ci.yml` does not exist!" end end end |