diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index fcbe0b350a3f6315d6c1a230a10fb32d022605c4..279d3faa110a6c44793144bcf7c940b4dd866ab4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -1031,6 +1031,13 @@ def debug_mode? variables.any? { |variable| variable[:key] == 'CI_DEBUG_TRACE' && variable[:value].casecmp('true') == 0 } end + def drop_with_exit_code!(failure_reason, exit_code) + transaction do + conditionally_allow_failure!(exit_code) + drop!(failure_reason) + end + end + protected def run_status_commit_hooks! @@ -1114,6 +1121,22 @@ def job_jwt_variables Gitlab::ErrorTracking.track_exception(e) end end + + def conditionally_allow_failure!(exit_code) + return unless ::Gitlab::Ci::Features.allow_failure_with_exit_codes_enabled? + return unless exit_code + + if allowed_to_fail_with_code?(exit_code) + update_columns(allow_failure: true) + end + end + + def allowed_to_fail_with_code?(exit_code) + options + .dig(:allow_failure_criteria, :exit_codes) + .to_a + .include?(exit_code) + end end end diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb index f01d41d94141af20b472b0d559fee19fe9466d08..874f4bf459a0e6d3930bd3b5469beb5776c5dfc1 100644 --- a/app/services/ci/update_build_state_service.rb +++ b/app/services/ci/update_build_state_service.rb @@ -111,7 +111,7 @@ def update_build_state! Result.new(status: 200) when 'failed' - build.drop!(params[:failure_reason] || :unknown_failure) + build.drop_with_exit_code!(params[:failure_reason] || :unknown_failure, params[:exit_code]) Result.new(status: 200) else diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 86e1a939df1cd00f466c2a7aa1e65f4f0b3148ad..5cfb65e1fbb5f6b243f64382f9d3aeeb1094a62f 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -180,6 +180,7 @@ class Runner < ::API::Base optional :checksum, type: String, desc: %q(Job's trace CRC32 checksum) optional :bytesize, type: Integer, desc: %q(Job's trace size in bytes) end + optional :exit_code, type: Integer, desc: %q(Job's exit code) end put '/:id' do job = authenticate_job! diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7e52b897dbc4d5e21206e1fb73651d02b81cff2b..b2473b01cf43c630aa5275a1bc5a6753a0b3b41d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4823,4 +4823,107 @@ def run_job_without_exception it { is_expected.to eq false } end end + + describe '#drop_with_exit_code!' do + let(:exit_code) { 1 } + let(:options) { {} } + + before do + build.options.merge!(options) + build.save! + end + + subject(:drop_with_exit_code) do + build.drop_with_exit_code!(:unknown_failure, exit_code) + end + + shared_examples 'drops the build without changing allow_failure' do + it 'does not change allow_failure' do + expect { drop_with_exit_code } + .not_to change { build.reload.allow_failure } + end + + it 'drops the build' do + expect { drop_with_exit_code } + .to change { build.reload.failed? } + end + end + + context 'when exit_codes are not defined' do + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when allow_failure_criteria is nil' do + let(:options) { { allow_failure_criteria: nil } } + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when exit_codes is nil' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: nil + } + } + end + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when exit_codes do not match' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [2, 3, 4] + } + } + end + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'with matching exit codes' do + let(:options) do + { allow_failure_criteria: { exit_codes: [1, 2, 3] } } + end + + it 'changes allow_failure' do + expect { drop_with_exit_code } + .to change { build.reload.allow_failure } + end + + it 'drops the build' do + expect { drop_with_exit_code } + .to change { build.reload.failed? } + end + + it 'is executed inside a transaction' do + expect(build).to receive(:drop!) + .with(:unknown_failure) + .and_raise(ActiveRecord::Rollback) + + expect(build).to receive(:conditionally_allow_failure!) + .with(1) + .and_call_original + + expect { drop_with_exit_code } + .not_to change { build.reload.allow_failure } + end + + context 'when exit_code is nil' do + let(:exit_code) {} + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when ci_allow_failure_with_exit_codes is disabled' do + before do + stub_feature_flags(ci_allow_failure_with_exit_codes: false) + end + + it_behaves_like 'drops the build without changing allow_failure' + end + end + end end diff --git a/spec/requests/api/ci/runner/jobs_put_spec.rb b/spec/requests/api/ci/runner/jobs_put_spec.rb index e9d793d5a22f9fb34567168779d3331c6567f7dd..f4c99307b1ab757f60b16a8f8026a97973d4510e 100644 --- a/spec/requests/api/ci/runner/jobs_put_spec.rb +++ b/spec/requests/api/ci/runner/jobs_put_spec.rb @@ -78,6 +78,33 @@ end end + context 'when an exit_code is provided' do + context 'when the exit_codes are acceptable' do + before do + job.options[:allow_failure_criteria] = { exit_codes: [1] } + job.save! + end + + it 'accepts an exit code' do + update_job(state: 'failed', exit_code: 1) + + expect(job.reload).to be_failed + expect(job.allow_failure).to be_truthy + expect(job).to be_unknown_failure + end + end + + context 'when the exit_codes are not defined' do + it 'ignore the exit code' do + update_job(state: 'failed', exit_code: 1) + + expect(job.reload).to be_failed + expect(job.allow_failure).to be_falsy + expect(job).to be_unknown_failure + end + end + end + context 'when failure_reason is script_failure' do before do update_job(state: 'failed', failure_reason: 'script_failure') diff --git a/spec/services/ci/update_build_state_service_spec.rb b/spec/services/ci/update_build_state_service_spec.rb index 3112e5dda1b6a90e7877d93d91ea5a2b3875c012..63190cc5d49a7a89aa88a7ce451d05240f3fb686 100644 --- a/spec/services/ci/update_build_state_service_spec.rb +++ b/spec/services/ci/update_build_state_service_spec.rb @@ -82,8 +82,9 @@ let(:params) do { output: { checksum: 'crc32:12345678', bytesize: 123 }, + state: 'failed', failure_reason: 'script_failure', - state: 'failed' + exit_code: 42 } end @@ -95,6 +96,15 @@ expect(result.status).to eq 200 end + it 'updates the allow_failure flag' do + expect(build) + .to receive(:drop_with_exit_code!) + .with('script_failure', 42) + .and_call_original + + subject.execute + end + it 'does not increment invalid trace metric' do execute_with_stubbed_metrics! @@ -115,6 +125,15 @@ expect(build).to be_failed end + it 'updates the allow_failure flag' do + expect(build) + .to receive(:drop_with_exit_code!) + .with('script_failure', 42) + .and_call_original + + subject.execute + end + it 'responds with 200 OK status' do result = subject.execute