Commit 17c4db13 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch '30235-support-allow-failure-for-ci-rules' into 'master'

Implement support of allow_failure keyword for CI rules

See merge request gitlab-org/gitlab!24605
parents 24137480 708d0e0d
---
title: Implement support of allow_failure keyword for CI rules
merge_request: 24605
author:
type: added
...@@ -851,7 +851,7 @@ In this example, if the first rule: ...@@ -851,7 +851,7 @@ In this example, if the first rule:
- Matches, the job will be given the `when:always` attribute. - Matches, the job will be given the `when:always` attribute.
- Does not match, the second and third rules will be evaluated sequentially - Does not match, the second and third rules will be evaluated sequentially
until a match is found. That is, the job will be given either the: until a match is found. That is, the job will be given either the:
- `when: manual` attribute if the second rule matches. - `when: manual` attribute if the second rule matches. **The stage will not complete until this manual job is triggered and completes successfully.**
- `when: on_success` attribute if the second rule does not match. The third - `when: on_success` attribute if the second rule does not match. The third
rule will always match when reached because it has no conditional clauses. rule will always match when reached because it has no conditional clauses.
...@@ -937,6 +937,25 @@ NOTE: **Note:** ...@@ -937,6 +937,25 @@ NOTE: **Note:**
For performance reasons, using `exists` with patterns is limited to 10000 For performance reasons, using `exists` with patterns is limited to 10000
checks. After the 10000th check, rules with patterned globs will always match. checks. After the 10000th check, rules with patterned globs will always match.
#### `rules:allow_failure`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30235) in GitLab 12.8.
You can use [`allow_failure: true`](#allow_failure) within `rules:` to allow a job to fail, or a manual job to
wait for action, without stopping the pipeline itself. All jobs using `rules:` default to `allow_failure: false`
if `allow_failure:` is not defined.
```yaml
job:
script: "echo Hello, Rules!"
rules:
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
when: manual
allow_failure: true
```
In this example, if the first rule matches, then the job will have `when: manual` and `allow_failure: true`.
#### Complex rule clauses #### Complex rule clauses
To conjoin `if`, `changes`, and `exists` clauses with an AND, use them in the To conjoin `if`, `changes`, and `exists` clauses with an AND, use them in the
...@@ -976,6 +995,7 @@ The only job attributes currently set by `rules` are: ...@@ -976,6 +995,7 @@ The only job attributes currently set by `rules` are:
- `when`. - `when`.
- `start_in`, if `when` is set to `delayed`. - `start_in`, if `when` is set to `delayed`.
- `allow_failure`.
A job will be included in a pipeline if `when` is evaluated to any value A job will be included in a pipeline if `when` is evaluated to any value
except `never`. except `never`.
......
...@@ -6,11 +6,12 @@ module Gitlab ...@@ -6,11 +6,12 @@ module Gitlab
class Rules class Rules
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
Result = Struct.new(:when, :start_in) do Result = Struct.new(:when, :start_in, :allow_failure) do
def build_attributes def build_attributes
{ {
when: self.when, when: self.when,
options: { start_in: start_in }.compact options: { start_in: start_in }.compact,
allow_failure: allow_failure
}.compact }.compact
end end
...@@ -30,7 +31,8 @@ module Gitlab ...@@ -30,7 +31,8 @@ module Gitlab
elsif matched_rule = match_rule(pipeline, context) elsif matched_rule = match_rule(pipeline, context)
Result.new( Result.new(
matched_rule.attributes[:when] || @default_when, matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in] matched_rule.attributes[:start_in],
matched_rule.attributes[:allow_failure]
) )
else else
Result.new('never') Result.new('never')
......
...@@ -9,10 +9,10 @@ module Gitlab ...@@ -9,10 +9,10 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable include ::Gitlab::Config::Entry::Attributable
CLAUSES = %i[if changes exists].freeze CLAUSES = %i[if changes exists].freeze
ALLOWED_KEYS = %i[if changes exists when start_in].freeze ALLOWED_KEYS = %i[if changes exists when start_in allow_failure].freeze
ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze ALLOWABLE_WHEN = %w[on_success on_failure always never manual delayed].freeze
attributes :if, :changes, :exists, :when, :start_in attributes :if, :changes, :exists, :when, :start_in, :allow_failure
validations do validations do
validates :config, presence: true validates :config, presence: true
...@@ -26,6 +26,7 @@ module Gitlab ...@@ -26,6 +26,7 @@ module Gitlab
validates :if, expression: true validates :if, expression: true
validates :changes, :exists, array_of_strings: true, length: { maximum: 50 } validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
validates :when, allowed_values: { in: ALLOWABLE_WHEN } validates :when, allowed_values: { in: ALLOWABLE_WHEN }
validates :allow_failure, boolean: true
end end
validate do validate do
......
...@@ -102,9 +102,9 @@ describe Gitlab::Ci::Build::Rules do ...@@ -102,9 +102,9 @@ describe Gitlab::Ci::Build::Rules do
end end
context 'with one rule without any clauses' do context 'with one rule without any clauses' do
let(:rule_list) { [{ when: 'manual' }] } let(:rule_list) { [{ when: 'manual', allow_failure: true }] }
it { is_expected.to eq(described_class::Result.new('manual')) } it { is_expected.to eq(described_class::Result.new('manual', nil, true)) }
end end
context 'with one matching rule' do context 'with one matching rule' do
...@@ -166,5 +166,51 @@ describe Gitlab::Ci::Build::Rules do ...@@ -166,5 +166,51 @@ describe Gitlab::Ci::Build::Rules do
end end
end end
end end
context 'with only allow_failure' do
context 'with matching rule' do
let(:rule_list) { [{ if: '$VAR == null', allow_failure: true }] }
it { is_expected.to eq(described_class::Result.new('on_success', nil, true)) }
end
context 'with non-matching rule' do
let(:rule_list) { [{ if: '$VAR != null', allow_failure: true }] }
it { is_expected.to eq(described_class::Result.new('never')) }
end
end
end
describe 'Gitlab::Ci::Build::Rules::Result' do
let(:when_value) { 'on_success' }
let(:start_in) { nil }
let(:allow_failure) { nil }
subject { Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure) }
describe '#build_attributes' do
it 'compacts nil values' do
expect(subject.build_attributes).to eq(options: {}, when: 'on_success')
end
end
describe '#pass?' do
context "'when' is 'never'" do
let!(:when_value) { 'never' }
it 'returns false' do
expect(subject.pass?).to eq(false)
end
end
context "'when' is 'on_success'" do
let!(:when_value) { 'on_success' }
it 'returns true' do
expect(subject.pass?).to eq(true)
end
end
end
end end
end end
...@@ -27,8 +27,14 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do ...@@ -27,8 +27,14 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
it { is_expected.to be_valid } it { is_expected.to be_valid }
end end
context 'with an allow_failure: value but no clauses' do
let(:config) { { allow_failure: true } }
it { is_expected.to be_valid }
end
context 'when specifying an if: clause' do context 'when specifying an if: clause' do
let(:config) { { if: '$THIS || $THAT', when: 'manual' } } let(:config) { { if: '$THIS || $THAT', when: 'manual', allow_failure: true } }
it { is_expected.to be_valid } it { is_expected.to be_valid }
...@@ -37,6 +43,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do ...@@ -37,6 +43,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
it { is_expected.to eq('manual') } it { is_expected.to eq('manual') }
end end
describe '#allow_failure' do
subject { entry.allow_failure }
it { is_expected.to eq(true) }
end
end end
context 'using a list of multiple expressions' do context 'using a list of multiple expressions' do
...@@ -328,16 +340,43 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do ...@@ -328,16 +340,43 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
end end
end end
end end
context 'allow_failure: validation' do
context 'with an invalid string allow_failure:' do
let(:config) do
{ if: '$THIS == "that"', allow_failure: 'always' }
end
it { is_expected.to be_a(described_class) }
it { is_expected.not_to be_valid }
it 'returns an error about invalid allow_failure:' do
expect(subject.errors).to include(/rule allow failure should be a boolean value/)
end
context 'when composed' do
before do
subject.compose!
end
it { is_expected.not_to be_valid }
it 'returns an error about invalid allow_failure:' do
expect(subject.errors).to include(/rule allow failure should be a boolean value/)
end
end
end
end
end end
describe '#value' do describe '#value' do
subject { entry.value } subject { entry.value }
context 'when specifying an if: clause' do context 'when specifying an if: clause' do
let(:config) { { if: '$THIS || $THAT', when: 'manual' } } let(:config) { { if: '$THIS || $THAT', when: 'manual', allow_failure: true } }
it 'stores the expression as "if"' do it 'stores the expression as "if"' do
expect(subject).to eq(if: '$THIS || $THAT', when: 'manual') expect(subject).to eq(if: '$THIS || $THAT', when: 'manual', allow_failure: true)
end end
end end
......
...@@ -1750,9 +1750,9 @@ describe Ci::CreatePipelineService do ...@@ -1750,9 +1750,9 @@ describe Ci::CreatePipelineService do
let(:ref_name) { 'refs/heads/master' } let(:ref_name) { 'refs/heads/master' }
let(:pipeline) { execute_service } let(:pipeline) { execute_service }
let(:build_names) { pipeline.builds.pluck(:name) } let(:build_names) { pipeline.builds.pluck(:name) }
let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') } let(:regular_job) { find_job('regular-job') }
let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') } let(:rules_job) { find_job('rules-job') }
let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') } let(:delayed_job) { find_job('delayed-job') }
shared_examples 'rules jobs are excluded' do shared_examples 'rules jobs are excluded' do
it 'only persists the job without rules' do it 'only persists the job without rules' do
...@@ -1763,6 +1763,10 @@ describe Ci::CreatePipelineService do ...@@ -1763,6 +1763,10 @@ describe Ci::CreatePipelineService do
end end
end end
def find_job(name)
pipeline.builds.find_by(name: name)
end
before do before do
stub_ci_pipeline_yaml_file(config) stub_ci_pipeline_yaml_file(config)
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
...@@ -1782,6 +1786,12 @@ describe Ci::CreatePipelineService do ...@@ -1782,6 +1786,12 @@ describe Ci::CreatePipelineService do
- if: $CI_COMMIT_REF_NAME =~ /master/ - if: $CI_COMMIT_REF_NAME =~ /master/
when: manual when: manual
negligible-job:
script: "exit 1"
rules:
- if: $CI_COMMIT_REF_NAME =~ /master/
allow_failure: true
delayed-job: delayed-job:
script: "echo See you later, World!" script: "echo See you later, World!"
rules: rules:
...@@ -1800,11 +1810,23 @@ describe Ci::CreatePipelineService do ...@@ -1800,11 +1810,23 @@ describe Ci::CreatePipelineService do
context 'with matches' do context 'with matches' do
it 'creates a pipeline with the vanilla and manual jobs' do it 'creates a pipeline with the vanilla and manual jobs' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('regular-job', 'delayed-job', 'master-job') expect(build_names).to contain_exactly(
'regular-job', 'delayed-job', 'master-job', 'negligible-job'
)
end end
it 'assigns job:when values to the builds' do it 'assigns job:when values to the builds' do
expect(pipeline.builds.pluck(:when)).to contain_exactly('on_success', 'delayed', 'manual') 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 end
it 'assigns start_in for delayed jobs' do it 'assigns start_in for delayed jobs' do
...@@ -1827,6 +1849,7 @@ describe Ci::CreatePipelineService do ...@@ -1827,6 +1849,7 @@ describe Ci::CreatePipelineService do
rules: rules:
- if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME
when: manual when: manual
allow_failure: true
EOY EOY
end end
...@@ -1834,6 +1857,7 @@ describe Ci::CreatePipelineService do ...@@ -1834,6 +1857,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(build_names).to contain_exactly('regular-job') expect(build_names).to contain_exactly('regular-job')
expect(regular_job.when).to eq('manual') expect(regular_job.when).to eq('manual')
expect(regular_job.allow_failure).to eq(true)
end end
end end
...@@ -1860,6 +1884,13 @@ describe Ci::CreatePipelineService do ...@@ -1860,6 +1884,13 @@ describe Ci::CreatePipelineService do
- README.md - README.md
when: delayed when: delayed
start_in: 4 hours start_in: 4 hours
negligible-job:
script: "can be failed sometimes"
rules:
- changes:
- README.md
allow_failure: true
EOY EOY
end end
...@@ -1872,7 +1903,7 @@ describe Ci::CreatePipelineService do ...@@ -1872,7 +1903,7 @@ describe Ci::CreatePipelineService do
it 'creates two jobs' do it 'creates two jobs' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
expect(build_names) expect(build_names)
.to contain_exactly('regular-job', 'rules-job', 'delayed-job') .to contain_exactly('regular-job', 'rules-job', 'delayed-job', 'negligible-job')
end end
it 'sets when: for all jobs' do it 'sets when: for all jobs' do
...@@ -1881,6 +1912,10 @@ describe Ci::CreatePipelineService do ...@@ -1881,6 +1912,10 @@ describe Ci::CreatePipelineService do
expect(delayed_job.when).to eq('delayed') expect(delayed_job.when).to eq('delayed')
expect(delayed_job.options[:start_in]).to eq('4 hours') expect(delayed_job.options[:start_in]).to eq('4 hours')
end end
it 'sets allow_failure: for negligible job' do
expect(find_job('negligible-job').allow_failure).to eq(true)
end
end end
context 'and matches the second rule' do context 'and matches the second rule' do
...@@ -1922,12 +1957,14 @@ describe Ci::CreatePipelineService do ...@@ -1922,12 +1957,14 @@ describe Ci::CreatePipelineService do
rules-job: rules-job:
script: "echo hello world, $CI_COMMIT_REF_NAME" script: "echo hello world, $CI_COMMIT_REF_NAME"
allow_failure: true
rules: rules:
- changes: - changes:
- README.md - README.md
when: manual when: manual
- if: $CI_COMMIT_REF_NAME == "master" - if: $CI_COMMIT_REF_NAME == "master"
when: on_success when: on_success
allow_failure: false
delayed-job: delayed-job:
script: "echo See you later, World!" script: "echo See you later, World!"
...@@ -1936,6 +1973,7 @@ describe Ci::CreatePipelineService do ...@@ -1936,6 +1973,7 @@ describe Ci::CreatePipelineService do
- README.md - README.md
when: delayed when: delayed
start_in: 4 hours start_in: 4 hours
allow_failure: true
- if: $CI_COMMIT_REF_NAME == "master" - if: $CI_COMMIT_REF_NAME == "master"
when: delayed when: delayed
start_in: 1 hour start_in: 1 hour
...@@ -1960,6 +1998,12 @@ describe Ci::CreatePipelineService do ...@@ -1960,6 +1998,12 @@ describe Ci::CreatePipelineService do
expect(delayed_job.when).to eq('delayed') expect(delayed_job.when).to eq('delayed')
expect(delayed_job.options[:start_in]).to eq('4 hours') expect(delayed_job.options[:start_in]).to eq('4 hours')
end 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 end
context 'and if: matches after changes' do context 'and if: matches after changes' do
...@@ -1999,6 +2043,7 @@ describe Ci::CreatePipelineService do ...@@ -1999,6 +2043,7 @@ describe Ci::CreatePipelineService do
- if: $CI_COMMIT_REF_NAME =~ /master/ - if: $CI_COMMIT_REF_NAME =~ /master/
changes: [README.md] changes: [README.md]
when: on_success when: on_success
allow_failure: true
- if: $CI_COMMIT_REF_NAME =~ /master/ - if: $CI_COMMIT_REF_NAME =~ /master/
changes: [app.rb] changes: [app.rb]
when: manual when: manual
...@@ -2016,6 +2061,7 @@ describe Ci::CreatePipelineService do ...@@ -2016,6 +2061,7 @@ describe Ci::CreatePipelineService do
expect(regular_job).to be_persisted expect(regular_job).to be_persisted
expect(rules_job).to be_persisted expect(rules_job).to be_persisted
expect(rules_job.when).to eq('manual') expect(rules_job.when).to eq('manual')
expect(rules_job.allow_failure).to eq(false)
end end
end end
...@@ -2040,6 +2086,150 @@ describe Ci::CreatePipelineService do ...@@ -2040,6 +2086,150 @@ describe Ci::CreatePipelineService do
it_behaves_like 'rules jobs are excluded' it_behaves_like 'rules jobs are excluded'
end end
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
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment