diff options
Diffstat (limited to 'spec/services/ci')
27 files changed, 1640 insertions, 986 deletions
diff --git a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb index 9add096d782..7c698242921 100644 --- a/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb +++ b/spec/services/ci/create_pipeline_service/evaluate_runner_tags_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Ci::CreatePipelineService do let_it_be(:group) { create(:group, :private) } - let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group')} + let_it_be(:group_variable) { create(:ci_group_variable, group: group, key: 'RUNNER_TAG', value: 'group') } let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:user) { create(:user) } diff --git a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb index 4326fa5533f..cc808b7e61c 100644 --- a/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb +++ b/spec/services/ci/create_pipeline_service/parent_child_pipeline_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Ci::CreatePipelineService, '#execute' do expect(pipeline.statuses).to match_array [test, bridge] expect(bridge.options).to eq(expected_bridge_options) expect(bridge.yaml_variables) - .to include(key: 'CROSS', value: 'downstream', public: true) + .to include(key: 'CROSS', value: 'downstream') end end diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb index d0ce1c5aba8..6e48141226d 100644 --- a/spec/services/ci/create_pipeline_service/rules_spec.rb +++ b/spec/services/ci/create_pipeline_service/rules_spec.rb @@ -7,10 +7,38 @@ RSpec.describe Ci::CreatePipelineService do let(:ref) { 'refs/heads/master' } let(:source) { :push } let(:service) { described_class.new(project, user, { ref: ref }) } - let(:pipeline) { service.execute(source).payload } + let(:response) { execute_service } + let(:pipeline) { response.payload } let(:build_names) { pipeline.builds.pluck(:name) } + def execute_service(before: '00000000', variables_attributes: nil) + params = { ref: ref, before: before, after: project.commit(ref).sha, variables_attributes: variables_attributes } + + described_class + .new(project, user, params) + .execute(source) do |pipeline| + yield(pipeline) if block_given? + end + end + context 'job:rules' do + let(:regular_job) { find_job('regular-job') } + let(:rules_job) { find_job('rules-job') } + let(:delayed_job) { find_job('delayed-job') } + + def find_job(name) + pipeline.builds.find_by(name: name) + end + + shared_examples 'rules jobs are excluded' do + it 'only persists the job without rules' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_nil + expect(delayed_job).to be_nil + end + end + before do stub_ci_pipeline_yaml_file(config) allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| @@ -95,10 +123,6 @@ RSpec.describe Ci::CreatePipelineService do end context 'with allow_failure and exit_codes', :aggregate_failures do - def find_job(name) - pipeline.builds.find_by(name: name) - end - let(:config) do <<-EOY job-1: @@ -280,6 +304,773 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with simple if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + master-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" + when: never + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + negligible-job: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + delayed-job: + script: "echo See you later, World!" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: delayed + start_in: 1 hour + + never-job: + script: "echo Goodbye, World!" + rules: + - if: $CI_COMMIT_REF_NAME + when: never + EOY + end + + context 'with matches' do + it 'creates a pipeline with the vanilla and manual jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'delayed-job', 'master-job', 'negligible-job' + ) + end + + it 'assigns job:when values to the builds' do + expect(find_job('regular-job').when).to eq('on_success') + expect(find_job('master-job').when).to eq('manual') + expect(find_job('negligible-job').when).to eq('on_success') + expect(find_job('delayed-job').when).to eq('delayed') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('regular-job').allow_failure).to eq(false) + expect(find_job('master-job').allow_failure).to eq(false) + expect(find_job('negligible-job').allow_failure).to eq(true) + expect(find_job('delayed-job').allow_failure).to eq(false) + end + + it 'assigns start_in for delayed jobs' do + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'with complex if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + rules: + - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME + when: manual + allow_failure: true + EOY + end + + it 'matches the first rule' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + expect(regular_job.when).to eq('manual') + expect(regular_job.allow_failure).to eq(true) + end + end + end + + context 'changes:' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - changes: + - README.md + when: manual + - changes: + - app.rb + when: on_success + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + + negligible-job: + script: "can be failed sometimes" + rules: + - changes: + - README.md + allow_failure: true + + README: + script: "I use variables for changes!" + rules: + - changes: + - $CI_JOB_NAME* + + changes-paths: + script: "I am using a new syntax!" + rules: + - changes: + paths: [README.md] + EOY + end + + context 'and matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates five jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' + ) + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for negligible job' do + expect(find_job('negligible-job').allow_failure).to eq(true) + end + end + + context 'and matches the second rule' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + end + end + + context 'and does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[useless_script.rb]) + end + end + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + + context 'with paths and compare_to' do + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:user) { project.first_owner } + + before_all do + project.repository.add_branch(user, 'feature_1', 'master') + + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + + project.repository.add_branch(user, 'feature_2', 'feature_1') + + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2' + ) + end + + let(:changed_file) { 'file2.txt' } + let(:ref) { 'feature_2' } + + let(:response) { execute_service(before: nil) } + + context 'for jobs rules' do + let(:config) do + <<-EOY + job1: + script: exit 0 + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job2: + script: exit 0 + EOY + end + + context 'when there is no such compare_to ref' do + let(:compare_to) { 'invalid-branch' } + + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' + ]) + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the compare_to ref exists' do + let(:compare_to) { 'feature_1' } + + context 'when the rule matches' do + it 'creates job1 and job2' do + expect(build_names).to contain_exactly('job1', 'job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(build_names).to contain_exactly('job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + end + end + + context 'for workflow rules' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job1: + script: exit 0 + EOY + end + + let(:compare_to) { 'feature_1' } + + context 'when the rule matches' do + it 'creates job1' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + end + end + end + end + + context 'mixed if: and changes: rules' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + allow_failure: true + rules: + - changes: + - README.md + when: manual + - if: $CI_COMMIT_REF_NAME == "master" + when: on_success + allow_failure: false + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + allow_failure: true + - if: $CI_COMMIT_REF_NAME == "master" + when: delayed + start_in: 1 hour + EOY + end + + context 'and changes: matches before if' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates two jobs' do + expect(pipeline).to be_persisted + expect(build_names) + .to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for all jobs' do + expect(regular_job.allow_failure).to eq(false) + expect(rules_job.allow_failure).to eq(true) + expect(delayed_job.allow_failure).to eq(true) + end + end + + context 'and if: matches after changes' do + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'and does not match' do + let(:ref) { 'refs/heads/wip' } + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + end + + context 'mixed if: and changes: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [README.md] + when: on_success + allow_failure: true + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [app.rb] + when: manual + EOY + end + + context 'with if matches and changes matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'persists all jobs' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_persisted + expect(rules_job.when).to eq('manual') + expect(rules_job.allow_failure).to eq(false) + end + end + + context 'with if matches and no change matches' do + it_behaves_like 'rules jobs are excluded' + end + + context 'with change matches and no if matches' do + let(:ref) { 'refs/heads/feature' } + + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it_behaves_like 'rules jobs are excluded' + end + + context 'and no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'complex if: allow_failure usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-2: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + + job-3: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: true + + job-4: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-5: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + job-6: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + - allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(true) + end + end + + context 'complex if: allow_failure & when usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-2: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: true + + job-3: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-4: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: false + + job-5: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + allow_failure: false + - when: always + allow_failure: true + + job-6: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-7: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + - when: :on_failure + allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' + ) + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-2').allow_failure).to eq(true) + expect(find_job('job-3').allow_failure).to eq(true) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(false) + expect(find_job('job-7').allow_failure).to eq(true) + end + + it 'assigns job:when values to the builds' do + expect(find_job('job-1').when).to eq('manual') + expect(find_job('job-2').when).to eq('manual') + expect(find_job('job-3').when).to eq('manual') + expect(find_job('job-4').when).to eq('manual') + expect(find_job('job-5').when).to eq('always') + expect(find_job('job-6').when).to eq('manual') + expect(find_job('job-7').when).to eq('on_failure') + end + end + + context 'deploy freeze period `if:` clause' do + # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" + let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } + + context 'with 2 jobs' do + let(:config) do + <<-EOY + stages: + - test + - deploy + + test-job: + script: + - echo 'running TEST stage' + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job', 'deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'creates one job' do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job') + end + end + end + end + + context 'with 1 job' do + let(:config) do + <<-EOY + stages: + - deploy + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'does not create the pipeline', :aggregate_failures do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + end + + context 'with when:manual' do + let(:config) do + <<-EOY + job-with-rules: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules-when: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: on_success + + job-with-rules-when: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-without-rules: + script: 'echo this is a job with NO rules' + EOY + end + + let(:job_with_rules) { find_job('job-with-rules') } + let(:job_when_with_rules) { find_job('job-when-with-rules') } + let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } + let(:job_with_rules_when) { find_job('job-with-rules-when') } + let(:job_without_rules) { find_job('job-without-rules') } + + context 'when matching the rules' do + let(:ref) { 'refs/heads/master' } + + it 'adds the job-with-rules with a when:manual' do + expect(job_with_rules).to be_persisted + expect(job_when_with_rules).to be_persisted + expect(job_when_with_rules_when).to be_persisted + expect(job_with_rules_when).to be_persisted + expect(job_without_rules).to be_persisted + + expect(job_with_rules.when).to eq('on_success') + expect(job_when_with_rules.when).to eq('manual') + expect(job_when_with_rules_when.when).to eq('on_success') + expect(job_with_rules_when.when).to eq('manual') + expect(job_without_rules.when).to eq('on_success') + end + end + + context 'when there is no match to the rule' do + let(:ref) { 'refs/heads/wip' } + + it 'does not add job_with_rules' do + expect(job_with_rules).to be_nil + expect(job_when_with_rules).to be_nil + expect(job_when_with_rules_when).to be_nil + expect(job_with_rules_when).to be_nil + expect(job_without_rules).to be_persisted + end + end end end @@ -447,5 +1238,232 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with persisted variables' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $CI_COMMIT_REF_NAME == "master" + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with pipeline variables' do + let(:pipeline) do + execute_service(variables_attributes: variables_attributes).payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables_attributes) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:variables_attributes) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with trigger variables' do + let(:pipeline) do + execute_service do |pipeline| + pipeline.variables.build(variables) + end.payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('build', 'test1', 'test2') + end + end + end + + context 'with no matches' do + let(:variables) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + + context 'changes' do + shared_examples 'comparing file changes with workflow rules' do + context 'when matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[file1.md]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job') + end + end + + context 'when does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[unknown]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline.errors.full_messages).to eq(['Pipeline filtered out by workflow rules.']) + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'changes is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + + context 'changes:paths is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 9cef7f7dadb..a9442b0dc68 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -18,6 +18,7 @@ RSpec.describe Ci::CreatePipelineService do # rubocop:disable Metrics/ParameterLists def execute_service( source: :push, + before: '00000000', after: project.commit.id, ref: ref_name, trigger_request: nil, @@ -29,7 +30,7 @@ RSpec.describe Ci::CreatePipelineService do target_sha: nil, save_on_errors: true) params = { ref: ref, - before: '00000000', + before: before, after: after, variables_attributes: variables_attributes, push_options: push_options, @@ -1865,818 +1866,6 @@ RSpec.describe Ci::CreatePipelineService do end end end - - context 'when rules are used' do - let(:ref_name) { 'refs/heads/master' } - let(:response) { execute_service } - let(:pipeline) { response.payload } - let(:build_names) { pipeline.builds.pluck(:name) } - let(:regular_job) { find_job('regular-job') } - let(:rules_job) { find_job('rules-job') } - let(:delayed_job) { find_job('delayed-job') } - - context 'with when:manual' do - let(:config) do - <<-EOY - job-with-rules: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules-when: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: on_success - - job-with-rules-when: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-without-rules: - script: 'echo this is a job with NO rules' - EOY - end - - let(:job_with_rules) { find_job('job-with-rules') } - let(:job_when_with_rules) { find_job('job-when-with-rules') } - let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } - let(:job_with_rules_when) { find_job('job-with-rules-when') } - let(:job_without_rules) { find_job('job-without-rules') } - - context 'when matching the rules' do - let(:ref_name) { 'refs/heads/master' } - - it 'adds the job-with-rules with a when:manual' do - expect(job_with_rules).to be_persisted - expect(job_when_with_rules).to be_persisted - expect(job_when_with_rules_when).to be_persisted - expect(job_with_rules_when).to be_persisted - expect(job_without_rules).to be_persisted - - expect(job_with_rules.when).to eq('on_success') - expect(job_when_with_rules.when).to eq('manual') - expect(job_when_with_rules_when.when).to eq('on_success') - expect(job_with_rules_when.when).to eq('manual') - expect(job_without_rules.when).to eq('on_success') - end - end - - context 'when there is no match to the rule' do - let(:ref_name) { 'refs/heads/wip' } - - it 'does not add job_with_rules' do - expect(job_with_rules).to be_nil - expect(job_when_with_rules).to be_nil - expect(job_when_with_rules_when).to be_nil - expect(job_with_rules_when).to be_nil - expect(job_without_rules).to be_persisted - end - end - end - - shared_examples 'rules jobs are excluded' do - it 'only persists the job without rules' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_nil - expect(delayed_job).to be_nil - end - end - - def find_job(name) - pipeline.builds.find_by(name: name) - end - - before do - stub_ci_pipeline_yaml_file(config) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - end - - context 'with simple if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - master-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" - when: never - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - negligible-job: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - delayed-job: - script: "echo See you later, World!" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: delayed - start_in: 1 hour - - never-job: - script: "echo Goodbye, World!" - rules: - - if: $CI_COMMIT_REF_NAME - when: never - EOY - end - - context 'with matches' do - it 'creates a pipeline with the vanilla and manual jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'delayed-job', 'master-job', 'negligible-job' - ) - end - - it 'assigns job:when values to the builds' do - expect(find_job('regular-job').when).to eq('on_success') - expect(find_job('master-job').when).to eq('manual') - expect(find_job('negligible-job').when).to eq('on_success') - expect(find_job('delayed-job').when).to eq('delayed') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('regular-job').allow_failure).to eq(false) - expect(find_job('master-job').allow_failure).to eq(false) - expect(find_job('negligible-job').allow_failure).to eq(true) - expect(find_job('delayed-job').allow_failure).to eq(false) - end - - it 'assigns start_in for delayed jobs' do - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - rules: - - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME - when: manual - allow_failure: true - EOY - end - - it 'matches the first rule' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - expect(regular_job.when).to eq('manual') - expect(regular_job.allow_failure).to eq(true) - end - end - - context 'with changes:' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - changes: - - README.md - when: manual - - changes: - - app.rb - when: on_success - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - - negligible-job: - script: "can be failed sometimes" - rules: - - changes: - - README.md - allow_failure: true - - README: - script: "I use variables for changes!" - rules: - - changes: - - $CI_JOB_NAME* - - changes-paths: - script: "I am using a new syntax!" - rules: - - changes: - paths: [README.md] - EOY - end - - context 'and matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates five jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' - ) - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for negligible job' do - expect(find_job('negligible-job').allow_failure).to eq(true) - end - end - - context 'and matches the second rule' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - end - end - - context 'and does not match' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[useless_script.rb]) - end - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with mixed if: and changes: rules' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - allow_failure: true - rules: - - changes: - - README.md - when: manual - - if: $CI_COMMIT_REF_NAME == "master" - when: on_success - allow_failure: false - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - allow_failure: true - - if: $CI_COMMIT_REF_NAME == "master" - when: delayed - start_in: 1 hour - EOY - end - - context 'and changes: matches before if' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates two jobs' do - expect(pipeline).to be_persisted - expect(build_names) - .to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for all jobs' do - expect(regular_job.allow_failure).to eq(false) - expect(rules_job.allow_failure).to eq(true) - expect(delayed_job.allow_failure).to eq(true) - end - end - - context 'and if: matches after changes' do - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'and does not match' do - let(:ref_name) { 'refs/heads/wip' } - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with mixed if: and changes: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [README.md] - when: on_success - allow_failure: true - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [app.rb] - when: manual - EOY - end - - context 'with if matches and changes matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'persists all jobs' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_persisted - expect(rules_job.when).to eq('manual') - expect(rules_job.allow_failure).to eq(false) - end - end - - context 'with if matches and no change matches' do - it_behaves_like 'rules jobs are excluded' - end - - context 'with change matches and no if matches' do - let(:ref_name) { 'refs/heads/feature' } - - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it_behaves_like 'rules jobs are excluded' - end - - context 'and no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: allow_failure usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-2: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - job-3: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: true - - job-4: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-5: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - job-6: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(true) - end - end - - context 'with complex if: allow_failure & when usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-2: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: true - - job-3: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-4: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: false - - job-5: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - allow_failure: false - - when: always - allow_failure: true - - job-6: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-7: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - - when: :on_failure - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' - ) - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-2').allow_failure).to eq(true) - expect(find_job('job-3').allow_failure).to eq(true) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(false) - expect(find_job('job-7').allow_failure).to eq(true) - end - - it 'assigns job:when values to the builds' do - expect(find_job('job-1').when).to eq('manual') - expect(find_job('job-2').when).to eq('manual') - expect(find_job('job-3').when).to eq('manual') - expect(find_job('job-4').when).to eq('manual') - expect(find_job('job-5').when).to eq('always') - expect(find_job('job-6').when).to eq('manual') - expect(find_job('job-7').when).to eq('on_failure') - end - end - - context 'with deploy freeze period `if:` clause' do - # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } - - context 'with 2 jobs' do - let(:config) do - <<-EOY - stages: - - test - - deploy - - test-job: - script: - - echo 'running TEST stage' - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job', 'deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'creates one job' do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job') - end - end - end - end - - context 'with 1 job' do - let(:config) do - <<-EOY - stages: - - deploy - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'does not create the pipeline', :aggregate_failures do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - end - - context 'with workflow rules with persisted variables' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $CI_COMMIT_REF_NAME == "master" - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with pipeline variables' do - let(:pipeline) do - execute_service(variables_attributes: variables_attributes).payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables_attributes) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:variables_attributes) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with trigger variables' do - let(:pipeline) do - execute_service do |pipeline| - pipeline.variables.build(variables) - end.payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('build', 'test1', 'test2') - end - end - end - - context 'with no matches' do - let(:variables) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - end end describe '#execute!' do diff --git a/spec/services/ci/deployments/destroy_service_spec.rb b/spec/services/ci/deployments/destroy_service_spec.rb new file mode 100644 index 00000000000..60a57c05728 --- /dev/null +++ b/spec/services/ci/deployments/destroy_service_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Deployments::DestroyService do + let_it_be(:project) { create(:project, :repository) } + + let(:environment) { create(:environment, project: project) } + let(:commits) { project.repository.commits(nil, { limit: 3 }) } + let!(:deploy) do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: nil, + sha: commits[2].sha + ) + end + + let!(:running_deploy) do + create( + :deployment, + :running, + project: project, + environment: environment, + deployable: nil, + sha: commits[1].sha + ) + end + + let!(:old_deploy) do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: nil, + sha: commits[0].sha, + finished_at: 1.year.ago + ) + end + + let(:user) { project.first_owner } + + subject { described_class.new(project, user) } + + context 'when deleting a deployment' do + it 'delete is accepted for old deployment' do + expect(subject.execute(old_deploy)).to be_success + end + + it 'does not delete a running deployment' do + response = subject.execute(running_deploy) + expect(response).to be_an_error + expect(response.message).to eq("Cannot destroy running deployment") + end + + it 'does not delete the last deployment' do + response = subject.execute(deploy) + expect(response).to be_an_error + expect(response.message).to eq("Deployment currently deployed to environment") + end + end +end diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index 045051c7152..6bd7fe7559c 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -90,15 +90,23 @@ RSpec.describe ::Ci::DestroyPipelineService do end end - context 'when pipeline is in cancelable state' do - before do - allow(pipeline).to receive(:cancelable?).and_return(true) - end + context 'when pipeline is in cancelable state', :sidekiq_inline do + let!(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:child_pipeline) { create(:ci_pipeline, :running, child_of: pipeline) } + let!(:child_build) { create(:ci_build, :running, pipeline: child_pipeline) } + + it 'cancels the pipelines sync' do + # turn off deletion for all instances of pipeline to allow for testing cancellation + allow(pipeline).to receive_message_chain(:reset, :destroy!) + allow_next_found_instance_of(Ci::Pipeline) { |p| allow(p).to receive_message_chain(:reset, :destroy!) } - it 'cancels the pipeline' do - expect(pipeline).to receive(:cancel_running) + # ensure cancellation happens sync so we accumulate minutes + expect(::Ci::CancelPipelineWorker).not_to receive(:perform) subject + + expect(build.reload.status).to eq('canceled') + expect(child_build.reload.status).to eq('canceled') end end end diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index b7a810ce47e..7b3f67b192f 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -34,6 +34,14 @@ RSpec.describe Ci::JobArtifacts::CreateService do subject { service.execute(artifacts_file, params, metadata_file: metadata_file) } context 'when artifacts file is uploaded' do + it 'logs the created artifact' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_created) + .with(an_instance_of(Ci::JobArtifact)) + + subject + end + it 'returns artifact in the response' do response = subject new_artifact = job.job_artifacts.last diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 05069054483..9ca39d4d32e 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -40,7 +40,14 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do expect { execute }.not_to change { artifact_with_file.file.exists? } end - it 'deletes the artifact records' do + it 'deletes the artifact records and logs them' do + expect(Gitlab::Ci::Artifacts::Logger) + .to receive(:log_deleted) + .with( + match_array([artifact_with_file, artifact_without_file]), + 'Ci::JobArtifacts::DestroyBatchService#execute' + ) + expect { subject }.to change { Ci::JobArtifact.count }.by(-2) end diff --git a/spec/services/ci/list_config_variables_service_spec.rb b/spec/services/ci/list_config_variables_service_spec.rb index 1735f4cfc97..4953b18bfcc 100644 --- a/spec/services/ci/list_config_variables_service_spec.rb +++ b/spec/services/ci/list_config_variables_service_spec.rb @@ -40,8 +40,8 @@ RSpec.describe Ci::ListConfigVariablesService, :use_clean_rails_memory_store_cac it 'returns variable list' do expect(subject['KEY1']).to eq({ value: 'val 1', description: 'description 1' }) expect(subject['KEY2']).to eq({ value: 'val 2', description: '' }) - expect(subject['KEY3']).to eq({ value: 'val 3', description: nil }) - expect(subject['KEY4']).to eq({ value: 'val 4', description: nil }) + expect(subject['KEY3']).to eq({ value: 'val 3' }) + expect(subject['KEY4']).to eq({ value: 'val 4' }) end end diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index aaab849cd93..7b3af33ac72 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -292,7 +292,7 @@ RSpec.describe Ci::ParseDotenvArtifactService do end context 'when build does not have a dotenv artifact' do - let!(:artifact) { } + let!(:artifact) {} it 'raises an error' do expect { subject }.to raise_error(ArgumentError) diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb index 7868629d34d..289e004fcce 100644 --- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb +++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb @@ -87,7 +87,7 @@ RSpec.describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection describe '#processing_processables' do it 'returns processables marked as processing' do - expect(collection.processing_processables.map { |processable| processable[:id]} ) + expect(collection.processing_processables.map { |processable| processable[:id] } ) .to contain_exactly(build_a.id, build_b.id, test_a.id, test_b.id, deploy.id) end end diff --git a/spec/services/ci/process_build_service_spec.rb b/spec/services/ci/process_build_service_spec.rb index b54fc45d36a..2fcb4ce73ff 100644 --- a/spec/services/ci/process_build_service_spec.rb +++ b/spec/services/ci/process_build_service_spec.rb @@ -101,7 +101,7 @@ RSpec.describe Ci::ProcessBuildService, '#execute' do context 'when build has delayed option' do before do - allow(Ci::BuildScheduleWorker).to receive(:perform_at) { } + allow(Ci::BuildScheduleWorker).to receive(:perform_at) {} end let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) } diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 2316575f164..cabd60a22d1 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -129,6 +129,12 @@ module Ci let!(:build2_project2) { create(:ci_build, :pending, :queued, pipeline: pipeline2) } let!(:build1_project3) { create(:ci_build, :pending, :queued, pipeline: pipeline3) } + it 'picks builds one-by-one' do + expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original + + expect(execute(shared_runner)).to eq(build1_project1) + end + context 'when using fair scheduling' do context 'when all builds are pending' do it 'prefers projects without builds first' do @@ -485,6 +491,48 @@ module Ci end context 'when "dependencies" keyword is specified' do + let!(:pre_stage_job) do + create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'test', stage_idx: 0) + end + + let!(:pending_job) do + create(:ci_build, :pending, :queued, + pipeline: pipeline, stage_idx: 1, + options: { script: ["bash"], dependencies: dependencies }) + end + + let(:dependencies) { %w[test] } + + subject { execute(specific_runner) } + + it 'picks a build with a dependency' do + picked_build = execute(specific_runner) + + expect(picked_build).to be_present + end + + context 'when there are multiple dependencies with artifacts' do + let!(:pre_stage_job_second) do + create(:ci_build, :success, :artifacts, pipeline: pipeline, name: 'deploy', stage_idx: 0) + end + + let(:dependencies) { %w[test deploy] } + + it 'logs build artifacts size' do + execute(specific_runner) + + artifacts_size = [pre_stage_job, pre_stage_job_second].sum do |job| + job.job_artifacts_archive.size + end + + expect(artifacts_size).to eq 107464 * 2 + expect(Gitlab::ApplicationContext.current).to include({ + 'meta.artifacts_dependencies_size' => artifacts_size, + 'meta.artifacts_dependencies_count' => 2 + }) + end + end + shared_examples 'not pick' do it 'does not pick the build and drops the build' do expect(subject).to be_nil @@ -572,16 +620,6 @@ module Ci end end - let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } - - let!(:pending_job) do - create(:ci_build, :pending, :queued, - pipeline: pipeline, stage_idx: 1, - options: { script: ["bash"], dependencies: ['test'] }) - end - - subject { execute(specific_runner) } - it_behaves_like 'validation is active' end @@ -739,16 +777,6 @@ module Ci end end - context 'when a long queue is created' do - it 'picks builds one-by-one' do - expect(Ci::Build).to receive(:find).with(pending_job.id).and_call_original - - expect(execute(specific_runner)).to eq(pending_job) - end - - include_examples 'handles runner assignment' - end - context 'when using pending builds table' do include_examples 'handles runner assignment' diff --git a/spec/services/ci/retry_job_service_spec.rb b/spec/services/ci/retry_job_service_spec.rb index f042471bd1f..b14e4187c7a 100644 --- a/spec/services/ci/retry_job_service_spec.rb +++ b/spec/services/ci/retry_job_service_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Ci::RetryJobService do name: 'test') end + let(:job_variables_attributes) { [{ key: 'MANUAL_VAR', value: 'manual test var' }] } let(:user) { developer } let(:service) { described_class.new(project, user) } @@ -206,6 +207,14 @@ RSpec.describe Ci::RetryJobService do include_context 'retryable bridge' it_behaves_like 'clones the job' + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + it 'does not give variables to the new bridge' do + expect { new_job }.not_to raise_error + end + end end context 'when the job to be cloned is a build' do @@ -250,6 +259,28 @@ RSpec.describe Ci::RetryJobService do expect { new_job }.not_to change { Environment.count } end end + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + context 'when the build is actionable' do + let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) } + + it 'gives variables to the new build' do + expect(new_job.job_variables.count).to be(1) + expect(new_job.job_variables.first.key).to eq('MANUAL_VAR') + expect(new_job.job_variables.first.value).to eq('manual test var') + end + end + + context 'when the build is not actionable' do + let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) } + + it 'does not give variables to the new build' do + expect(new_job.job_variables.count).to be_zero + end + end + end end end @@ -260,6 +291,14 @@ RSpec.describe Ci::RetryJobService do include_context 'retryable bridge' it_behaves_like 'retries the job' + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + it 'does not give variables to the new bridge' do + expect { new_job }.not_to raise_error + end + end end context 'when the job to be retried is a build' do @@ -288,6 +327,28 @@ RSpec.describe Ci::RetryJobService do expect { service.execute(job) }.not_to exceed_all_query_limit(control_count) end end + + context 'when given variables' do + let(:new_job) { service.clone!(job, variables: job_variables_attributes) } + + context 'when the build is actionable' do + let_it_be_with_refind(:job) { create(:ci_build, :actionable, pipeline: pipeline) } + + it 'gives variables to the new build' do + expect(new_job.job_variables.count).to be(1) + expect(new_job.job_variables.first.key).to eq('MANUAL_VAR') + expect(new_job.job_variables.first.value).to eq('manual test var') + end + end + + context 'when the build is not actionable' do + let_it_be_with_refind(:job) { create(:ci_build, pipeline: pipeline) } + + it 'does not give variables to the new build' do + expect(new_job.job_variables.count).to be_zero + end + end + end end end end diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb index 00b176bb759..08bb99830fb 100644 --- a/spec/services/ci/runners/assign_runner_service_spec.rb +++ b/spec/services/ci/runners/assign_runner_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do - subject { described_class.new(runner, project, user).execute } + subject(:execute) { described_class.new(runner, project, user).execute } let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let_it_be(:project) { create(:project) } @@ -11,30 +11,32 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute' do context 'without user' do let(:user) { nil } - it 'does not call assign_to on runner and returns false' do + it 'does not call assign_to on runner and returns error response', :aggregate_failures do expect(runner).not_to receive(:assign_to) - is_expected.to eq(false) + is_expected.to be_error + expect(execute.message).to eq('user not allowed to assign runner') end end context 'with unauthorized user' do let(:user) { build(:user) } - it 'does not call assign_to on runner and returns false' do + it 'does not call assign_to on runner and returns error message' do expect(runner).not_to receive(:assign_to) - is_expected.to eq(false) + is_expected.to be_error + expect(execute.message).to eq('user not allowed to assign runner') end end context 'with admin user', :enable_admin_mode do let(:user) { create_default(:user, :admin) } - it 'calls assign_to on runner and returns value unchanged' do - expect(runner).to receive(:assign_to).with(project, user).once.and_return('assign_to return value') + it 'calls assign_to on runner and returns success response' do + expect(runner).to receive(:assign_to).with(project, user).once.and_call_original - is_expected.to eq('assign_to return value') + is_expected.to be_success end end end diff --git a/spec/services/ci/runners/bulk_delete_runners_service_spec.rb b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb new file mode 100644 index 00000000000..8e9fc4e3012 --- /dev/null +++ b/spec/services/ci/runners/bulk_delete_runners_service_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Runners::BulkDeleteRunnersService, '#execute' do + subject(:execute) { described_class.new(**service_args).execute } + + let(:service_args) { { runners: runners_arg } } + let(:runners_arg) {} + + context 'with runners specified' do + let!(:instance_runner) { create(:ci_runner) } + let!(:group_runner) { create(:ci_runner, :group) } + let!(:project_runner) { create(:ci_runner, :project) } + + shared_examples 'a service deleting runners in bulk' do + it 'destroys runners', :aggregate_failures do + expect { subject }.to change { Ci::Runner.count }.by(-2) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 2, deleted_ids: [instance_runner.id, project_runner.id] }) + expect(instance_runner[:errors]).to be_nil + expect(project_runner[:errors]).to be_nil + expect { project_runner.runner_projects.first.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { group_runner.reload }.not_to raise_error + expect { instance_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + context 'with some runners already deleted' do + before do + instance_runner.destroy! + end + + let(:runners_arg) { [instance_runner.id, project_runner.id] } + + it 'destroys runners and returns only deleted runners', :aggregate_failures do + expect { subject }.to change { Ci::Runner.count }.by(-1) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [project_runner.id] }) + expect(instance_runner[:errors]).to be_nil + expect(project_runner[:errors]).to be_nil + expect { project_runner.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'with too many runners specified' do + before do + stub_const("#{described_class}::RUNNER_LIMIT", 1) + end + + it 'deletes only first RUNNER_LIMIT runners' do + expect { subject }.to change { Ci::Runner.count }.by(-1) + + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 1, deleted_ids: [instance_runner.id] }) + end + end + end + + context 'with runners specified as relation' do + let(:runners_arg) { Ci::Runner.not_group_type } + + include_examples 'a service deleting runners in bulk' + end + + context 'with runners specified as array of IDs' do + let(:runners_arg) { Ci::Runner.not_group_type.ids } + + include_examples 'a service deleting runners in bulk' + end + + context 'with no arguments specified' do + let(:runners_arg) { nil } + + it 'returns 0 deleted runners' do + is_expected.to be_success + expect(execute.payload).to eq({ deleted_count: 0, deleted_ids: [] }) + end + end + end +end diff --git a/spec/services/ci/runners/process_runner_version_update_service_spec.rb b/spec/services/ci/runners/process_runner_version_update_service_spec.rb new file mode 100644 index 00000000000..b885138fc7a --- /dev/null +++ b/spec/services/ci/runners/process_runner_version_update_service_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Runners::ProcessRunnerVersionUpdateService do + subject(:service) { described_class.new(version) } + + let(:version) { '1.0.0' } + let(:available_runner_releases) { %w[1.0.0 1.0.1] } + + describe '#execute' do + subject(:execute) { service.execute } + + context 'with upgrade check returning error' do + let(:service_double) { instance_double(Gitlab::Ci::RunnerUpgradeCheck) } + + before do + allow(service_double).to receive(:check_runner_upgrade_suggestion).with(version) + .and_return([version, :error]) + allow(service).to receive(:upgrade_check_service).and_return(service_double) + end + + it 'does not update ci_runner_versions records', :aggregate_failures do + expect do + expect(execute).to be_error + expect(execute.message).to eq 'upgrade version check failed' + end.not_to change(Ci::RunnerVersion, :count).from(0) + expect(service_double).to have_received(:check_runner_upgrade_suggestion).with(version).once + end + end + + context 'with successful result from upgrade check' do + before do + url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url + + WebMock.stub_request(:get, url).to_return( + body: available_runner_releases.map { |v| { name: v } }.to_json, + status: 200, + headers: { 'Content-Type' => 'application/json' } + ) + end + + context 'with no existing ci_runner_version record' do + it 'creates ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.to change(Ci::RunnerVersion, :all).to contain_exactly( + an_object_having_attributes(version: version, status: 'recommended') + ) + end + end + + context 'with existing ci_runner_version record' do + let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :not_available) } + + it 'updates ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.to change { runner_version.reload.status }.from('not_available').to('recommended') + end + end + + context 'with up-to-date ci_runner_version record' do + let!(:runner_version) { create(:ci_runner_version, version: '1.0.0', status: :recommended) } + + it 'does not update ci_runner_versions record', :aggregate_failures do + expect do + expect(execute).to be_success + expect(execute.http_status).to eq :ok + expect(execute.payload).to eq({ upgrade_status: 'recommended' }) + end.not_to change { runner_version.reload.status } + end + end + end + end +end diff --git a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb index f8313eaab90..1690190320a 100644 --- a/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb +++ b/spec/services/ci/runners/reconcile_existing_runner_versions_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' do + include RunnerReleasesHelper + subject(:execute) { described_class.new.execute } let_it_be(:runner_14_0_1) { create(:ci_runner, version: '14.0.1') } @@ -11,12 +13,12 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end context 'with RunnerUpgradeCheck recommending 14.0.2' do + let(:upgrade_check) { instance_double(::Gitlab::Ci::RunnerUpgradeCheck) } + before do stub_const('Ci::Runners::ReconcileExistingRunnerVersionsService::VERSION_BATCH_SIZE', 1) - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ recommended: ::Gitlab::VersionInfo.new(14, 0, 2) }) + allow(::Gitlab::Ci::RunnerUpgradeCheck).to receive(:new).and_return(upgrade_check).once end context 'with runner with new version' do @@ -25,10 +27,11 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' let!(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') } before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :recommended]) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) .with('14.0.2') - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) }) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) .once end @@ -39,14 +42,13 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' .once .and_call_original - result = nil - expect { result = execute } + expect { execute } .to change { runner_version_14_0_0.reload.status }.from('not_available').to('recommended') .and change { runner_version_14_0_1.reload.status }.from('not_available').to('recommended') .and change { ::Ci::RunnerVersion.find_by(version: '14.0.2')&.status }.from(nil).to('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 1, # 14.0.2 is inserted total_updated: 3, # 14.0.0, 14.0.1 are updated, and newly inserted 14.0.2's status is calculated total_deleted: 0 @@ -58,19 +60,17 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' let!(:runner_version_14_0_2) { create(:ci_runner_version, version: '14.0.2', status: :not_available) } before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 2) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 2), :not_available]) end it 'deletes orphan ci_runner_versions entry', :aggregate_failures do - result = nil - expect { result = execute } + expect { execute } .to change { ::Ci::RunnerVersion.find_by_version('14.0.2')&.status }.from('not_available').to(nil) .and not_change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 1 # 14.0.2 is deleted @@ -80,17 +80,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with no runner version changes' do before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ not_available: ::Gitlab::VersionInfo.new(14, 0, 1) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :not_available]) end it 'does not modify ci_runner_versions entries', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 @@ -100,17 +98,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' context 'with failing version check' do before do - allow(::Gitlab::Ci::RunnerUpgradeCheck.instance) - .to receive(:check_runner_upgrade_status) - .and_return({ error: ::Gitlab::VersionInfo.new(14, 0, 1) }) + allow(upgrade_check).to receive(:check_runner_upgrade_suggestion) + .and_return([::Gitlab::VersionInfo.new(14, 0, 1), :error]) end it 'makes no changes to ci_runner_versions', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 @@ -120,26 +116,15 @@ RSpec.describe ::Ci::Runners::ReconcileExistingRunnerVersionsService, '#execute' end context 'integration testing with Gitlab::Ci::RunnerUpgradeCheck' do - let(:available_runner_releases) do - %w[14.0.0 14.0.1] - end - before do - url = ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url - - WebMock.stub_request(:get, url).to_return( - body: available_runner_releases.map { |v| { name: v } }.to_json, - status: 200, - headers: { 'Content-Type' => 'application/json' } - ) + stub_runner_releases(%w[14.0.0 14.0.1]) end it 'does not modify ci_runner_versions entries', :aggregate_failures do - result = nil - expect { result = execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') + expect { execute }.not_to change { runner_version_14_0_1.reload.status }.from('not_available') - expect(result).to eq({ - status: :success, + expect(execute).to be_success + expect(execute.payload).to eq({ total_inserted: 0, total_updated: 0, total_deleted: 0 diff --git a/spec/services/ci/runners/register_runner_service_spec.rb b/spec/services/ci/runners/register_runner_service_spec.rb index 03dcf851e53..6d7b39de21e 100644 --- a/spec/services/ci/runners/register_runner_service_spec.rb +++ b/spec/services/ci/runners/register_runner_service_spec.rb @@ -4,8 +4,9 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:registration_token) { 'abcdefg123456' } - let(:token) { } + let(:token) {} let(:args) { {} } + let(:runner) { execute.payload[:runner] } before do stub_feature_flags(runner_registration_control: false) @@ -13,21 +14,25 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do stub_application_setting(valid_runner_registrars: ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES) end - subject(:runner) { described_class.new.execute(token, args) } + subject(:execute) { described_class.new.execute(token, args) } context 'when no token is provided' do let(:token) { '' } - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + expect(execute).to be_error + expect(execute.message).to eq 'invalid token supplied' + expect(execute.http_status).to eq :forbidden end end context 'when invalid token is provided' do let(:token) { 'invalid' } - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + expect(execute).to be_error + expect(execute.message).to eq 'invalid token supplied' + expect(execute.http_status).to eq :forbidden end end @@ -36,12 +41,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { registration_token } it 'creates runner with default values' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_truthy - expect(subject.run_untagged).to be true - expect(subject.active).to be true - expect(subject.token).not_to eq(registration_token) - expect(subject).to be_instance_type + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_truthy + expect(runner.run_untagged).to be true + expect(runner.active).to be true + expect(runner.token).not_to eq(registration_token) + expect(runner).to be_instance_type end context 'with non-default arguments' do @@ -67,25 +74,27 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner with specified values', :aggregate_failures do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.active).to eq args[:active] - expect(subject.locked).to eq args[:locked] - expect(subject.run_untagged).to eq args[:run_untagged] - expect(subject.tags).to contain_exactly( + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.active).to eq args[:active] + expect(runner.locked).to eq args[:locked] + expect(runner.run_untagged).to eq args[:run_untagged] + expect(runner.tags).to contain_exactly( an_object_having_attributes(name: 'tag1'), an_object_having_attributes(name: 'tag2') ) - expect(subject.access_level).to eq args[:access_level] - expect(subject.maximum_timeout).to eq args[:maximum_timeout] - expect(subject.name).to eq args[:name] - expect(subject.version).to eq args[:version] - expect(subject.revision).to eq args[:revision] - expect(subject.platform).to eq args[:platform] - expect(subject.architecture).to eq args[:architecture] - expect(subject.ip_address).to eq args[:ip_address] - - expect(Ci::Runner.tagged_with('tag1')).to include(subject) - expect(Ci::Runner.tagged_with('tag2')).to include(subject) + expect(runner.access_level).to eq args[:access_level] + expect(runner.maximum_timeout).to eq args[:maximum_timeout] + expect(runner.name).to eq args[:name] + expect(runner.version).to eq args[:version] + expect(runner.revision).to eq args[:revision] + expect(runner.platform).to eq args[:platform] + expect(runner.architecture).to eq args[:architecture] + expect(runner.ip_address).to eq args[:ip_address] + + expect(Ci::Runner.tagged_with('tag1')).to include(runner) + expect(Ci::Runner.tagged_with('tag2')).to include(runner) end end @@ -95,8 +104,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner with token expiration' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.token_expires_at).to eq(5.days.from_now) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.token_expires_at).to eq(5.days.from_now) end end end @@ -106,12 +117,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { project.runners_token } it 'creates project runner' do - is_expected.to be_an_instance_of(::Ci::Runner) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) expect(project.runners.size).to eq(1) - is_expected.to eq(project.runners.first) - expect(subject.token).not_to eq(registration_token) - expect(subject.token).not_to eq(project.runners_token) - expect(subject).to be_project_type + expect(runner).to eq(project.runners.first) + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(project.runners_token) + expect(runner).to be_project_type end context 'when it exceeds the application limits' do @@ -121,9 +134,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'does not create runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_falsey - expect(subject.errors.messages).to eq('runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded']) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_falsey + expect(runner.errors.messages).to eq( + 'runner_projects.base': ['Maximum number of ci registered project runners (1) exceeded'] + ) expect(project.runners.reload.size).to eq(1) end end @@ -135,8 +152,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(project.runners.reload.size).to eq(2) expect(project.runners.recent.size).to eq(1) end @@ -153,15 +172,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'returns 403 error' do - is_expected.to be_nil + expect(execute).to be_error + expect(execute.http_status).to eq :forbidden end end context 'when feature flag is disabled' do it 'registers the runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty - expect(subject.active).to be true + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty + expect(runner.active).to be true end end end @@ -172,12 +194,14 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do let(:token) { group.runners_token } it 'creates a group runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(group.runners.reload.size).to eq(1) - expect(subject.token).not_to eq(registration_token) - expect(subject.token).not_to eq(group.runners_token) - expect(subject).to be_group_type + expect(runner.token).not_to eq(registration_token) + expect(runner.token).not_to eq(group.runners_token) + expect(runner).to be_group_type end context 'when it exceeds the application limits' do @@ -187,9 +211,13 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'does not create runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.persisted?).to be_falsey - expect(subject.errors.messages).to eq('runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded']) + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_falsey + expect(runner.errors.messages).to eq( + 'runner_namespaces.base': ['Maximum number of ci registered group runners (1) exceeded'] + ) expect(group.runners.reload.size).to eq(1) end end @@ -202,8 +230,10 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do end it 'creates runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty expect(group.runners.reload.size).to eq(3) expect(group.runners.recent.size).to eq(1) end @@ -219,16 +249,18 @@ RSpec.describe ::Ci::Runners::RegisterRunnerService, '#execute' do stub_feature_flags(runner_registration_control: true) end - it 'returns nil' do - is_expected.to be_nil + it 'returns error response' do + is_expected.to be_error end end context 'when feature flag is disabled' do it 'registers the runner' do - is_expected.to be_an_instance_of(::Ci::Runner) - expect(subject.errors).to be_empty - expect(subject.active).to be true + expect(execute).to be_success + + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.errors).to be_empty + expect(runner.active).to be true end end end diff --git a/spec/services/ci/runners/reset_registration_token_service_spec.rb b/spec/services/ci/runners/reset_registration_token_service_spec.rb index c4bfff51cc8..79059712032 100644 --- a/spec/services/ci/runners/reset_registration_token_service_spec.rb +++ b/spec/services/ci/runners/reset_registration_token_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do - subject { described_class.new(scope, current_user).execute } + subject(:execute) { described_class.new(scope, current_user).execute } let_it_be(:user) { build(:user) } let_it_be(:admin_user) { create(:user, :admin) } @@ -12,20 +12,20 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do context 'without user' do let(:current_user) { nil } - it 'does not reset registration token and returns nil' do + it 'does not reset registration token and returns error response' do expect(scope).not_to receive(token_reset_method_name) - is_expected.to be_nil + expect(execute).to be_error end end context 'with unauthorized user' do let(:current_user) { user } - it 'does not reset registration token and returns nil' do + it 'does not reset registration token and returns error response' do expect(scope).not_to receive(token_reset_method_name) - is_expected.to be_nil + expect(execute).to be_error end end @@ -37,7 +37,8 @@ RSpec.describe ::Ci::Runners::ResetRegistrationTokenService, '#execute' do expect(scope).to receive(token_method_name).once.and_return("#{token_method_name} return value") end - is_expected.to eq("#{token_method_name} return value") + expect(execute).to be_success + expect(execute.payload[:new_registration_token]).to eq("#{token_method_name} return value") end end end diff --git a/spec/services/ci/runners/unassign_runner_service_spec.rb b/spec/services/ci/runners/unassign_runner_service_spec.rb index 3fb6925f4bd..cf710cf6893 100644 --- a/spec/services/ci/runners/unassign_runner_service_spec.rb +++ b/spec/services/ci/runners/unassign_runner_service_spec.rb @@ -3,21 +3,21 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do - subject(:service) { described_class.new(runner_project, user).execute } - - let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let_it_be(:project) { create(:project) } + let_it_be(:runner) { create(:ci_runner, :project, projects: [project]) } let(:runner_project) { runner.runner_projects.last } + subject(:execute) { described_class.new(runner_project, user).execute } + context 'without user' do let(:user) { nil } it 'does not destroy runner_project', :aggregate_failures do expect(runner_project).not_to receive(:destroy) - expect { service }.not_to change { runner.runner_projects.count }.from(1) + expect { execute }.not_to change { runner.runner_projects.count }.from(1) - is_expected.to eq(false) + is_expected.to be_error end end @@ -27,17 +27,27 @@ RSpec.describe ::Ci::Runners::UnassignRunnerService, '#execute' do it 'does not call destroy on runner_project' do expect(runner).not_to receive(:destroy) - service + is_expected.to be_error end end context 'with admin user', :enable_admin_mode do let(:user) { create_default(:user, :admin) } - it 'destroys runner_project' do - expect(runner_project).to receive(:destroy).once + context 'with destroy returning false' do + it 'returns error response' do + expect(runner_project).to receive(:destroy).once.and_return(false) + + is_expected.to be_error + end + end + + context 'with destroy returning true' do + it 'returns success response' do + expect(runner_project).to receive(:destroy).once.and_return(true) - service + is_expected.to be_success + end end end end diff --git a/spec/services/ci/runners/unregister_runner_service_spec.rb b/spec/services/ci/runners/unregister_runner_service_spec.rb index df1a0a90067..77fc299e4e1 100644 --- a/spec/services/ci/runners/unregister_runner_service_spec.rb +++ b/spec/services/ci/runners/unregister_runner_service_spec.rb @@ -3,13 +3,16 @@ require 'spec_helper' RSpec.describe ::Ci::Runners::UnregisterRunnerService, '#execute' do - subject { described_class.new(runner, 'some_token').execute } + subject(:execute) { described_class.new(runner, 'some_token').execute } let(:runner) { create(:ci_runner) } it 'destroys runner' do expect(runner).to receive(:destroy).once.and_call_original - expect { subject }.to change { Ci::Runner.count }.by(-1) + + expect do + expect(execute).to be_success + end.to change { Ci::Runner.count }.by(-1) expect(runner[:errors]).to be_nil end end diff --git a/spec/services/ci/runners/update_runner_service_spec.rb b/spec/services/ci/runners/update_runner_service_spec.rb index b02ea8f58b0..e008fde9982 100644 --- a/spec/services/ci/runners/update_runner_service_spec.rb +++ b/spec/services/ci/runners/update_runner_service_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Ci::Runners::UpdateRunnerService do end context 'with cost factor params' do - let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 }} + let(:params) { { public_projects_minutes_cost_factor: 1.1, private_projects_minutes_cost_factor: 2.2 } } it 'updates the runner cost factors' do expect(update).to be_truthy diff --git a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb index ebc57af77a0..a452a65829a 100644 --- a/spec/services/ci/stuck_builds/drop_pending_service_spec.rb +++ b/spec/services/ci/stuck_builds/drop_pending_service_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Ci::StuckBuilds::DropPendingService do create(:ci_build, pipeline: pipeline, runner: runner) end - let(:created_at) { } - let(:updated_at) { } + let(:created_at) {} + let(:updated_at) {} subject(:service) { described_class.new } diff --git a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb index 1416fab3d25..a4f9f97fffc 100644 --- a/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb +++ b/spec/services/ci/stuck_builds/drop_scheduled_service_spec.rb @@ -44,7 +44,7 @@ RSpec.describe Ci::StuckBuilds::DropScheduledService do end context 'when there are no stale scheduled builds' do - let(:job) { } + let(:job) {} it 'does not drop the stale scheduled build yet' do expect { service.execute }.not_to raise_error diff --git a/spec/services/ci/track_failed_build_service_spec.rb b/spec/services/ci/track_failed_build_service_spec.rb new file mode 100644 index 00000000000..d83e56f0669 --- /dev/null +++ b/spec/services/ci/track_failed_build_service_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::TrackFailedBuildService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + + let_it_be(:exit_code) { 42 } + let_it_be(:failure_reason) { "script_failure" } + + describe '#execute' do + context 'when a build has failed' do + let_it_be(:build) { create(:ci_build, :failed, :sast_report, pipeline: pipeline, user: user) } + + subject { described_class.new(build: build, exit_code: exit_code, failure_reason: failure_reason) } + + it 'tracks the build failed event', :snowplow do + response = subject.execute + + expect(response.success?).to be true + + expect_snowplow_event( + category: 'ci::build', + action: 'failed', + context: [{ + schema: described_class::SCHEMA_URL, + data: { + build_id: build.id, + build_name: build.name, + build_artifact_types: ["sast"], + exit_code: exit_code, + failure_reason: failure_reason + } + }], + user: user, + project: project.id) + end + end + + context 'when a build has not failed' do + let_it_be(:build) { create(:ci_build, :success, :sast_report, pipeline: pipeline, user: user) } + + subject { described_class.new(build: build, exit_code: nil, failure_reason: nil) } + + it 'does not track the build failed event', :snowplow do + response = subject.execute + + expect(response.error?).to be true + + expect_no_snowplow_event + end + end + end +end diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 937b19beff5..90a86e7ae59 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -33,6 +33,24 @@ RSpec.describe Ci::UpdateBuildStateService do end end + context 'when build has failed' do + let(:params) do + { + output: { checksum: 'crc32:12345678', bytesize: 123 }, + state: 'failed', + failure_reason: 'script_failure', + exit_code: 7 + } + end + + it 'sends a build failed event to Snowplow' do + expect(::Ci::TrackFailedBuildWorker) + .to receive(:perform_async).with(build.id, params[:exit_code], params[:failure_reason]) + + subject.execute + end + end + context 'when build does not have checksum' do context 'when state has changed' do let(:params) { { state: 'success' } } |