Commit 51ec4902 authored by drew's avatar drew Committed by Kamil Trzciński
Browse files

Created Workflow::Rules configuration

- Added Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules
- Basic E2E spec for skipping Pipelines via workflow:rules
- CI config validation for workflow:rules
parent cb39aebc
require 'spec_helper'
describe Gitlab::Ci::Build::Context::Build do
let(:pipeline) { create(:ci_pipeline) }
let(:seed_attributes) { { 'name' => 'some-job' } }
let(:context) { described_class.new(pipeline, seed_attributes) }
describe '#variables' do
subject { context.variables }
it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') }
it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) }
it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
it { is_expected.to include('CI_JOB_NAME' => 'some-job') }
it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') }
context 'without passed build-specific attributes' do
let(:context) { described_class.new(pipeline) }
it { is_expected.to include('CI_JOB_NAME' => nil) }
it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') }
it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Context::Global do
let(:pipeline) { create(:ci_pipeline) }
let(:yaml_variables) { {} }
let(:context) { described_class.new(pipeline, yaml_variables: yaml_variables) }
describe '#variables' do
subject { context.variables }
it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') }
it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) }
it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) }
it { is_expected.not_to have_key('CI_JOB_NAME') }
it { is_expected.not_to have_key('CI_BUILD_REF_NAME') }
context 'with passed yaml variables' do
let(:yaml_variables) { [{ key: 'SUPPORTED', value: 'parsed', public: true }] }
it { is_expected.to include('SUPPORTED' => 'parsed') }
end
end
end
......@@ -16,7 +16,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do
double('build seed',
to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash
variables: ci_build.scoped_variables_hash
)
end
 
......@@ -91,7 +91,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do
double('bridge seed',
to_resource: bridge,
scoped_variables_hash: ci_build.scoped_variables_hash
variables: ci_build.scoped_variables_hash
)
end
 
......
......@@ -6,7 +6,7 @@ describe Gitlab::Ci::Build::Rules::Rule do
let(:seed) do
double('build seed',
to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash
variables: ci_build.scoped_variables_hash
)
end
 
......
......@@ -9,11 +9,11 @@ describe Gitlab::Ci::Build::Rules do
let(:seed) do
double('build seed',
to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash
variables: ci_build.scoped_variables_hash
)
end
 
let(:rules) { described_class.new(rule_list) }
let(:rules) { described_class.new(rule_list, default_when: 'on_success') }
 
describe '.new' do
let(:rules_ivar) { rules.instance_variable_get :@rule_list }
......@@ -62,7 +62,7 @@ describe Gitlab::Ci::Build::Rules do
 
context 'with a specified default when:' do
let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] }
let(:rules) { described_class.new(rule_list, 'manual') }
let(:rules) { described_class.new(rule_list, default_when: 'manual') }
 
it 'sets @rule_list to an array of a single rule' do
expect(rules_ivar).to be_an(Array)
......@@ -83,7 +83,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('on_success')) }
 
context 'and when:manual set as the default' do
let(:rules) { described_class.new(rule_list, 'manual') }
let(:rules) { described_class.new(rule_list, default_when: 'manual') }
 
it { is_expected.to eq(described_class::Result.new('manual')) }
end
......@@ -95,7 +95,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) }
 
context 'and when:manual set as the default' do
let(:rules) { described_class.new(rule_list, 'manual') }
let(:rules) { described_class.new(rule_list, default_when: 'manual') }
 
it { is_expected.to eq(described_class::Result.new('never')) }
end
......@@ -159,7 +159,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) }
 
context 'and when:manual set as the default' do
let(:rules) { described_class.new(rule_list, 'manual') }
let(:rules) { described_class.new(rule_list, default_when: 'manual') }
 
it 'does not return the default when:' do
expect(subject).to eq(described_class::Result.new('never'))
......
......@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Default do
# that we know that we don't want to inherit
# as they do not have sense in context of Default
let(:ignored_inheritable_columns) do
%i[default include variables stages types]
%i[default include variables stages types workflow]
end
end
 
......
......@@ -18,9 +18,8 @@ describe Gitlab::Ci::Config::Entry::Root do
#
# The purpose of `Root` is have only globally defined configuration.
expect(described_class.nodes.keys)
.to match_array(%i[before_script image services
after_script variables cache
stages types include default])
.to match_array(%i[before_script image services after_script
variables cache stages types include default workflow])
end
end
end
......@@ -50,7 +49,7 @@ describe Gitlab::Ci::Config::Entry::Root do
end
 
it 'creates node object for each entry' do
expect(root.descendants.count).to eq 10
expect(root.descendants.count).to eq 11
end
 
it 'creates node object using valid class' do
......@@ -203,7 +202,7 @@ describe Gitlab::Ci::Config::Entry::Root do
 
describe '#nodes' do
it 'instantizes all nodes' do
expect(root.descendants.count).to eq 10
expect(root.descendants.count).to eq 11
end
 
it 'contains unspecified nodes' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Workflow do
let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(rules_hash) }
let(:config) { factory.create! }
describe 'validations' do
context 'when work config value is a string' do
let(:rules_hash) { 'build' }
describe '#valid?' do
it 'is invalid' do
expect(config).not_to be_valid
end
it 'attaches an error specifying that workflow should point to a hash' do
expect(config.errors).to include('workflow config should be a hash')
end
end
describe '#value' do
it 'returns the invalid configuration' do
expect(config.value).to eq(rules_hash)
end
end
end
context 'when work config value is a hash' do
let(:rules_hash) { { rules: [{ if: '$VAR' }] } }
describe '#valid?' do
it 'is valid' do
expect(config).to be_valid
end
it 'attaches no errors' do
expect(config.errors).to be_empty
end
end
describe '#value' do
it 'returns the config' do
expect(config.value).to eq(rules_hash)
end
end
context 'with an invalid key' do
let(:rules_hash) { { trash: [{ if: '$VAR' }] } }
describe '#valid?' do
it 'is invalid' do
expect(config).not_to be_valid
end
it 'attaches an error specifying the unknown key' do
expect(config.errors).to include('workflow config contains unknown keys: trash')
end
end
describe '#value' do
it 'returns the invalid configuration' do
expect(config.value).to eq(rules_hash)
end
end
end
end
end
describe '.default' do
it 'is nil' do
expect(described_class.default).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
describe '#perform!' do
context 'when pipeline has been skipped by workflow configuration' do
before do
allow(step).to receive(:workflow_passed?)
.and_return(false)
step.perform!
end
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'attaches an error to the pipeline' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
end
context 'when pipeline has not been skipped by workflow configuration' do
before do
allow(step).to receive(:workflow_passed?)
.and_return(true)
step.perform!
end
it 'continues the pipeline processing chain' do
expect(step.break?).to be false
end
it 'does not skip the pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline).not_to be_skipped
end
it 'attaches no errors' do
expect(pipeline.errors).to be_empty
end
end
end
end
......@@ -869,10 +869,4 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
end
describe '#scoped_variables_hash' do
subject { seed_build.scoped_variables_hash }
it { is_expected.to eq(seed_build.to_resource.scoped_variables_hash) }
end
end
......@@ -268,6 +268,108 @@ module Gitlab
end
end
 
describe '#workflow_attributes' do
context 'with disallowed workflow:variables' do
let(:config) do
<<-EOYML
workflow:
rules:
- if: $VAR == "value"
variables:
UNSUPPORTED: "unparsed"
EOYML
end
it 'parses the workflow:rules configuration' do
expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'workflow config contains unknown keys: variables')
end
end
context 'with rules and variables' do
let(:config) do
<<-EOYML
variables:
SUPPORTED: "parsed"
workflow:
rules:
- if: $VAR == "value"
hello:
script: echo world
EOYML
end
it 'parses the workflow:rules configuration' do
expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' })
end
it 'parses the root:variables as yaml_variables:' do
expect(subject.workflow_attributes[:yaml_variables])
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
end
end
context 'with rules and no variables' do
let(:config) do
<<-EOYML
workflow:
rules:
- if: $VAR == "value"
hello:
script: echo world
EOYML
end
it 'parses the workflow:rules configuration' do
expect(subject.workflow_attributes[:rules]).to contain_exactly({ if: '$VAR == "value"' })
end
it 'parses the root:variables as yaml_variables:' do
expect(subject.workflow_attributes[:yaml_variables]).to eq([])
end
end
context 'with variables and no rules' do
let(:config) do
<<-EOYML
variables:
SUPPORTED: "parsed"
hello:
script: echo world
EOYML
end
it 'parses the workflow:rules configuration' do
expect(subject.workflow_attributes[:rules]).to be_nil
end
it 'parses the root:variables as yaml_variables:' do
expect(subject.workflow_attributes[:yaml_variables])
.to contain_exactly({ key: 'SUPPORTED', value: 'parsed', public: true })
end
end
context 'with no rules and no variables' do
let(:config) do
<<-EOYML
hello:
script: echo world
EOYML
end
it 'parses the workflow:rules configuration' do
expect(subject.workflow_attributes[:rules]).to be_nil
end
it 'parses the root:variables as yaml_variables:' do
expect(subject.workflow_attributes[:yaml_variables]).to eq([])
end
end
end
describe 'only / except policies validations' do
context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
......
# frozen_string_literal: true
require 'spec_helper'
 
describe Ci::CreatePipelineService do
context 'rules' do
let(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
let(:build_names) { pipeline.builds.pluck(:name) }
let(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
let(:build_names) { pipeline.builds.pluck(:name) }
 
context 'job:rules' do
before do
stub_ci_pipeline_yaml_file(config)
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
......@@ -41,6 +41,7 @@ describe Ci::CreatePipelineService do
start_in: 4 hours
EOY
end
let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
......@@ -91,4 +92,259 @@ describe Ci::CreatePipelineService do
end
end
end
context 'when workflow:rules are used' do
before do
stub_ci_pipeline_yaml_file(config)
end
context 'with a single regex-matching if: clause' do
let(:config) do
<<-EOY
workflow:
rules:
- if: $CI_COMMIT_REF_NAME =~ /master/
- if: $CI_COMMIT_REF_NAME =~ /wip$/
when: never
- if: $CI_COMMIT_REF_NAME =~ /feature/
regular-job:
script: 'echo Hello, World!'
EOY
end
context 'matching the first rule in the list' do
it 'saves the pipeline' do
expect(pipeline).to be_persisted
end
it 'sets the pipeline state to pending' do
expect(pipeline).to be_pending
end
end
context 'matching the last rule in the list' do
let(:ref) { 'refs/heads/feature' }
it 'saves the pipeline' do
expect(pipeline).to be_persisted
end
it 'sets the pipeline state to pending' do
expect(pipeline).to be_pending
end
end
context 'matching the when:never rule' do
let(:ref) { 'refs/heads/wip' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches errors' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
end
context 'matching no rules in the list' do
let(:ref) { 'refs/heads/fix' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches errors' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
end
end
context 'when root variables are used' do
let(:config) do
<<-EOY
variables:
VARIABLE: value
workflow:
rules:
- if: $VARIABLE
regular-job:
script: 'echo Hello, World!'
EOY
end
context 'matching the first rule in the list' do
it 'saves the pipeline' do
expect(pipeline).to be_persisted
end
it 'sets the pipeline state to pending' do
expect(pipeline).to be_pending
end
end
end
context 'with a multiple regex-matching if: clause' do
let(:config) do
<<-EOY
workflow:
rules:
- if: $CI_COMMIT_REF_NAME =~ /master/
- if: $CI_COMMIT_REF_NAME =~ /^feature/ && $CI_COMMIT_REF_NAME =~ /conflict$/
when: never
- if: $CI_COMMIT_REF_NAME =~ /feature/
regular-job:
script: 'echo Hello, World!'
EOY
end
context 'with partial match' do
let(:ref) { 'refs/heads/feature' }
it 'saves the pipeline' do
expect(pipeline).to be_persisted
end
it 'sets the pipeline state to pending' do
expect(pipeline).to be_pending
end
end
context 'with complete match' do
let(:ref) { 'refs/heads/feature_conflict' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches errors' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
end
end