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:
- Matches, the job will be given the `when:always` attribute.
- 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:
- `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
rule will always match when reached because it has no conditional clauses.
......@@ -937,6 +937,25 @@ NOTE: **Note:**
For performance reasons, using `exists` with patterns is limited to 10000
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
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:
- `when`.
- `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
except `never`.
......
......@@ -6,11 +6,12 @@ module Gitlab
class Rules
include ::Gitlab::Utils::StrongMemoize
Result = Struct.new(:when, :start_in) do
Result = Struct.new(:when, :start_in, :allow_failure) do
def build_attributes
{
when: self.when,
options: { start_in: start_in }.compact
options: { start_in: start_in }.compact,
allow_failure: allow_failure
}.compact
end
......@@ -30,7 +31,8 @@ module Gitlab
elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in]
matched_rule.attributes[:start_in],
matched_rule.attributes[:allow_failure]
)
else
Result.new('never')
......
......@@ -9,10 +9,10 @@ module Gitlab
include ::Gitlab::Config::Entry::Attributable
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
attributes :if, :changes, :exists, :when, :start_in
attributes :if, :changes, :exists, :when, :start_in, :allow_failure
validations do
validates :config, presence: true
......@@ -26,6 +26,7 @@ module Gitlab
validates :if, expression: true
validates :changes, :exists, array_of_strings: true, length: { maximum: 50 }
validates :when, allowed_values: { in: ALLOWABLE_WHEN }
validates :allow_failure, boolean: true
end
validate do
......
......@@ -102,9 +102,9 @@ describe Gitlab::Ci::Build::Rules do
end
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
context 'with one matching rule' do
......@@ -166,5 +166,51 @@ describe Gitlab::Ci::Build::Rules do
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
......@@ -27,8 +27,14 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
it { is_expected.to be_valid }
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
let(:config) { { if: '$THIS || $THAT', when: 'manual' } }
let(:config) { { if: '$THIS || $THAT', when: 'manual', allow_failure: true } }
it { is_expected.to be_valid }
......@@ -37,6 +43,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
it { is_expected.to eq('manual') }
end
describe '#allow_failure' do
subject { entry.allow_failure }
it { is_expected.to eq(true) }
end
end
context 'using a list of multiple expressions' do
......@@ -328,16 +340,43 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do
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
describe '#value' do
subject { entry.value }
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
expect(subject).to eq(if: '$THIS || $THAT', when: 'manual')
expect(subject).to eq(if: '$THIS || $THAT', when: 'manual', allow_failure: true)
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