Commit 708d0e0d authored by Furkan Ayhan's avatar Furkan Ayhan Committed by Ash McKenzie

Add support of allow_failure keyword for CI rules

With this, we can set allow_failure to jobs
depending on conditions.
parent 214c605d
---
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
......
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