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

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
...@@ -12,6 +12,7 @@ module Ci ...@@ -12,6 +12,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository, Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config, Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create, Gitlab::Ci::Pipeline::Chain::Create,
......
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Base
attr_reader :pipeline
def initialize(pipeline)
@pipeline = pipeline
end
def variables
raise NotImplementedError
end
protected
def pipeline_attributes
{
pipeline: pipeline,
project: pipeline.project,
user: pipeline.user,
ref: pipeline.ref,
tag: pipeline.tag,
trigger_request: pipeline.legacy_trigger,
protected: pipeline.protected_ref?
}
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Build < Base
include Gitlab::Utils::StrongMemoize
attr_reader :attributes
def initialize(pipeline, attributes = {})
super(pipeline)
@attributes = attributes
end
def variables
strong_memoize(:variables) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate rules before we persist a Build
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
stub_build.scoped_variables_hash
end
end
private
def stub_build
::Ci::Build.new(build_attributes)
end
def build_attributes
attributes.merge(pipeline_attributes)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Build
module Context
class Global < Base
include Gitlab::Utils::StrongMemoize
def initialize(pipeline, yaml_variables:)
super(pipeline)
@yaml_variables = yaml_variables.to_a
end
def variables
strong_memoize(:variables) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate workflow:rules
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
stub_build.scoped_variables_hash
.reject { |key, _value| key =~ /\ACI_(JOB|BUILD)/ }
end
end
private
def stub_build
::Ci::Build.new(build_attributes)
end
def build_attributes
pipeline_attributes.merge(
yaml_variables: @yaml_variables)
end
end
end
end
end
end
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
@globs = Array(globs) @globs = Array(globs)
end end
def satisfied_by?(pipeline, seed) def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil? return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path| pipeline.modified_paths.any? do |path|
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
end end
end end
def satisfied_by?(pipeline, seed = nil) def satisfied_by?(pipeline, context = nil)
pipeline.has_kubernetes_active? pipeline.has_kubernetes_active?
end end
end end
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
@patterns = Array(refs) @patterns = Array(refs)
end end
def satisfied_by?(pipeline, seed = nil) def satisfied_by?(pipeline, context = nil)
@patterns.any? do |pattern| @patterns.any? do |pattern|
pattern, path = pattern.split('@', 2) pattern, path = pattern.split('@', 2)
......
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
@spec = spec @spec = spec
end end
def satisfied_by?(pipeline, seed = nil) def satisfied_by?(pipeline, context = nil)
raise NotImplementedError raise NotImplementedError
end end
end end
......
...@@ -9,8 +9,8 @@ module Gitlab ...@@ -9,8 +9,8 @@ module Gitlab
@expressions = Array(expressions) @expressions = Array(expressions)
end end
def satisfied_by?(pipeline, seed) def satisfied_by?(pipeline, context)
variables = seed.scoped_variables_hash variables = context.variables
statements = @expressions.map do |statement| statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement ::Gitlab::Ci::Pipeline::Expression::Statement
......
...@@ -13,17 +13,21 @@ module Gitlab ...@@ -13,17 +13,21 @@ module Gitlab
options: { start_in: start_in }.compact options: { start_in: start_in }.compact
}.compact }.compact
end end
def pass?
self.when != 'never'
end
end end
def initialize(rule_hashes, default_when = 'on_success') def initialize(rule_hashes, default_when:)
@rule_list = Rule.fabricate_list(rule_hashes) @rule_list = Rule.fabricate_list(rule_hashes)
@default_when = default_when @default_when = default_when
end end
def evaluate(pipeline, build) def evaluate(pipeline, context)
if @rule_list.nil? if @rule_list.nil?
Result.new(@default_when) Result.new(@default_when)
elsif matched_rule = match_rule(pipeline, build) 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]
...@@ -35,8 +39,8 @@ module Gitlab ...@@ -35,8 +39,8 @@ module Gitlab
private private
def match_rule(pipeline, build) def match_rule(pipeline, context)
@rule_list.find { |rule| rule.matches?(pipeline, build) } @rule_list.find { |rule| rule.matches?(pipeline, context) }
end end
end end
end end
......
...@@ -23,8 +23,8 @@ module Gitlab ...@@ -23,8 +23,8 @@ module Gitlab
end end
end end
def matches?(pipeline, build) def matches?(pipeline, context)
@clauses.all? { |clause| clause.satisfied_by?(pipeline, build) } @clauses.all? { |clause| clause.satisfied_by?(pipeline, context) }
end end
end end
end end
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
@spec = spec @spec = spec
end end
def satisfied_by?(pipeline, seed = nil) def satisfied_by?(pipeline, context = nil)
raise NotImplementedError raise NotImplementedError
end end
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
@globs = Array(globs) @globs = Array(globs)
end end
def satisfied_by?(pipeline, seed) def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil? return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path| pipeline.modified_paths.any? do |path|
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?)) @exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end end
def satisfied_by?(pipeline, seed) def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline) paths = worktree_paths(pipeline)
exact_matches?(paths) || pattern_matches?(paths) exact_matches?(paths) || pattern_matches?(paths)
......
...@@ -8,10 +8,9 @@ module Gitlab ...@@ -8,10 +8,9 @@ module Gitlab
@expression = expression @expression = expression
end end
def satisfied_by?(pipeline, seed) def satisfied_by?(pipeline, context)
variables = seed.scoped_variables_hash ::Gitlab::Ci::Pipeline::Expression::Statement.new(
@expression, context.variables).truthful?
::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
end end
end end
end end
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[default include before_script image services ALLOWED_KEYS = %i[default include before_script image services
after_script variables stages types cache].freeze after_script variables stages types cache workflow].freeze
validations do validations do
validates :config, allowed_keys: ALLOWED_KEYS validates :config, allowed_keys: ALLOWED_KEYS
...@@ -64,6 +64,9 @@ module Gitlab ...@@ -64,6 +64,9 @@ module Gitlab
description: 'Configure caching between build jobs.', description: 'Configure caching between build jobs.',
reserved: true reserved: true
entry :workflow, Entry::Workflow,
description: 'List of evaluable rules to determine Pipeline status'
helpers :default, :jobs, :stages, :types, :variables helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value, delegate :before_script_value,
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
class Workflow < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[rules].freeze
validations do
validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
validates :config, presence: true
end
entry :rules, Entry::Rules,
description: 'List of evaluable Rules to determine Pipeline status.',
metadata: { allowed_when: %w[always never] }
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
class EvaluateWorkflowRules < Chain::Base
include ::Gitlab::Utils::StrongMemoize
include Chain::Helpers
def perform!
return unless Feature.enabled?(:workflow_rules, @pipeline.project)
unless workflow_passed?
error('Pipeline filtered out by workflow rules.')
end
end
def break?
return false unless Feature.enabled?(:workflow_rules, @pipeline.project)
!workflow_passed?
end
private
def workflow_passed?
strong_memoize(:workflow_passed) do
workflow_rules.evaluate(@pipeline, global_context).pass?
end
end
def workflow_rules
Gitlab::Ci::Build::Rules.new(
workflow_config[:rules], default_when: 'always')
end
def global_context
Gitlab::Ci::Build::Context::Global.new(
@pipeline, yaml_variables: workflow_config[:yaml_variables])
end
def workflow_config
@pipeline.config_processor.workflow_attributes || {}
end
end
end
end
end
end
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
@except = Gitlab::Ci::Build::Policy @except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except)) .fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules @rules = Gitlab::Ci::Build::Rules
.new(attributes.delete(:rules)) .new(attributes.delete(:rules), default_when: 'on_success')
@cache = Seed::Build::Cache @cache = Seed::Build::Cache
.new(pipeline, attributes.delete(:cache)) .new(pipeline, attributes.delete(:cache))
end end
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
def included? def included?
strong_memoize(:inclusion) do strong_memoize(:inclusion) do
if @using_rules if @using_rules
included_by_rules? rules_result.pass?
elsif @using_only || @using_except elsif @using_only || @using_except
all_of_only? && none_of_except? all_of_only? && none_of_except?
else else
...@@ -83,26 +83,14 @@ module Gitlab ...@@ -83,26 +83,14 @@ module Gitlab
end end
end end
def scoped_variables_hash
strong_memoize(:scoped_variables_hash) do
# This is a temporary piece of technical debt to allow us access
# to the CI variables to evaluate rules before we persist a Build
# with the result. We should refactor away the extra Build.new,
# but be able to get CI Variables directly from the Seed::Build.
::Ci::Build.new(
@seed_attributes.merge(pipeline_attributes)
).scoped_variables_hash
end
end
private private
def all_of_only? def all_of_only?
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) } @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end end
def none_of_except? def none_of_except?
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) } @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end end
def needs_errors def needs_errors
...@@ -144,13 +132,21 @@ module Gitlab ...@@ -144,13 +132,21 @@ module Gitlab
} }
end end
def included_by_rules? def rules_attributes
rules_attributes[:when] != 'never' return {} unless @using_rules
rules_result.build_attributes
end end
def rules_attributes def rules_result
strong_memoize(:rules_attributes) do strong_memoize(:rules_result) do
@using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {} @rules.evaluate(@pipeline, evaluate_context)
end
end
def evaluate_context
strong_memoize(:evaluate_context) do
Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes)
end end
end end
......
...@@ -39,7 +39,7 @@ module Gitlab ...@@ -39,7 +39,7 @@ module Gitlab
when: job[:when] || 'on_success', when: job[:when] || 'on_success',
environment: job[:environment_name], environment: job[:environment_name],
coverage_regex: job[:coverage], coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name), yaml_variables: transform_to_yaml_variables(job_variables(name)),
needs_attributes: job.dig(:needs, :job), needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible], interruptible: job[:interruptible],
rules: job[:rules], rules: job[:rules],
...@@ -83,6 +83,13 @@ module Gitlab ...@@ -83,6 +83,13 @@ module Gitlab
end end
end end
def workflow_attributes
{
rules: @config.dig(:workflow, :rules),
yaml_variables: transform_to_yaml_variables(@variables)
}
end
def self.validation_message(content, opts = {}) def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank? return 'Please provide content of .gitlab-ci.yml' if content.blank?
...@@ -118,20 +125,17 @@ module Gitlab ...@@ -118,20 +125,17 @@ module Gitlab
end end
end end
def yaml_variables(name) def job_variables(name)
variables = (@variables || {}) job_variables = @jobs.dig(name.to_sym, :variables)
.merge(job_variables(name))
variables.map do |key, value| @variables.to_h
{ key: key.to_s, value: value, public: true } .merge(job_variables.to_h)
end
end end
def job_variables(name) def transform_to_yaml_variables(variables)
job = @jobs[name.to_sym] variables.to_h.map do |key, value|
return {} unless job { key: key.to_s, value: value, public: true }
end
job[:variables] || {}
end end
def validate_job_stage!(name, job) def validate_job_stage!(name, job)
......
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 ...@@ -16,7 +16,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do let(:seed) do
double('build seed', double('build seed',
to_resource: ci_build, to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash variables: ci_build.scoped_variables_hash
) )
end end
...@@ -91,7 +91,7 @@ describe Gitlab::Ci::Build::Policy::Variables do ...@@ -91,7 +91,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
let(:seed) do let(:seed) do
double('bridge seed', double('bridge seed',
to_resource: bridge, to_resource: bridge,
scoped_variables_hash: ci_build.scoped_variables_hash variables: ci_build.scoped_variables_hash
) )
end end
......
...@@ -6,7 +6,7 @@ describe Gitlab::Ci::Build::Rules::Rule do ...@@ -6,7 +6,7 @@ describe Gitlab::Ci::Build::Rules::Rule do
let(:seed) do let(:seed) do
double('build seed', double('build seed',
to_resource: ci_build, to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash variables: ci_build.scoped_variables_hash
) )
end end
......
...@@ -9,11 +9,11 @@ describe Gitlab::Ci::Build::Rules do ...@@ -9,11 +9,11 @@ describe Gitlab::Ci::Build::Rules do
let(:seed) do let(:seed) do
double('build seed', double('build seed',
to_resource: ci_build, to_resource: ci_build,
scoped_variables_hash: ci_build.scoped_variables_hash variables: ci_build.scoped_variables_hash
) )
end end
let(:rules) { described_class.new(rule_list) } let(:rules) { described_class.new(rule_list, default_when: 'on_success') }
describe '.new' do describe '.new' do
let(:rules_ivar) { rules.instance_variable_get :@rule_list } let(:rules_ivar) { rules.instance_variable_get :@rule_list }
...@@ -62,7 +62,7 @@ describe Gitlab::Ci::Build::Rules do ...@@ -62,7 +62,7 @@ describe Gitlab::Ci::Build::Rules do
context 'with a specified default when:' do context 'with a specified default when:' do
let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] } 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 it 'sets @rule_list to an array of a single rule' do
expect(rules_ivar).to be_an(Array) expect(rules_ivar).to be_an(Array)
...@@ -83,7 +83,7 @@ describe Gitlab::Ci::Build::Rules do ...@@ -83,7 +83,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('on_success')) } it { is_expected.to eq(described_class::Result.new('on_success')) }
context 'and when:manual set as the default' do 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')) } it { is_expected.to eq(described_class::Result.new('manual')) }
end end
...@@ -95,7 +95,7 @@ describe Gitlab::Ci::Build::Rules do ...@@ -95,7 +95,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) } it { is_expected.to eq(described_class::Result.new('never')) }
context 'and when:manual set as the default' do 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')) } it { is_expected.to eq(described_class::Result.new('never')) }
end end
...@@ -159,7 +159,7 @@ describe Gitlab::Ci::Build::Rules do ...@@ -159,7 +159,7 @@ describe Gitlab::Ci::Build::Rules do
it { is_expected.to eq(described_class::Result.new('never')) } it { is_expected.to eq(described_class::Result.new('never')) }
context 'and when:manual set as the default' do 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 it 'does not return the default when:' do
expect(subject).to eq(described_class::Result.new('never')) expect(subject).to eq(described_class::Result.new('never'))
......
...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Default do ...@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Default do
# that we know that we don't want to inherit # that we know that we don't want to inherit
# as they do not have sense in context of Default # as they do not have sense in context of Default
let(:ignored_inheritable_columns) do let(:ignored_inheritable_columns) do
%i[default include variables stages types] %i[default include variables stages types workflow]
end end
end end
......
...@@ -18,9 +18,8 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -18,9 +18,8 @@ describe Gitlab::Ci::Config::Entry::Root do
# #
# The purpose of `Root` is have only globally defined configuration. # The purpose of `Root` is have only globally defined configuration.
expect(described_class.nodes.keys) expect(described_class.nodes.keys)
.to match_array(%i[before_script image services .to match_array(%i[before_script image services after_script
after_script variables cache variables cache stages types include default workflow])
stages types include default])
end end
end end
end end
...@@ -50,7 +49,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -50,7 +49,7 @@ describe Gitlab::Ci::Config::Entry::Root do
end end
it 'creates node object for each entry' do it 'creates node object for each entry' do
expect(root.descendants.count).to eq 10 expect(root.descendants.count).to eq 11
end end
it 'creates node object using valid class' do it 'creates node object using valid class' do
...@@ -203,7 +202,7 @@ describe Gitlab::Ci::Config::Entry::Root do ...@@ -203,7 +202,7 @@ describe Gitlab::Ci::Config::Entry::Root do
describe '#nodes' do describe '#nodes' do
it 'instantizes all nodes' do it 'instantizes all nodes' do
expect(root.descendants.count).to eq 10 expect(root.descendants.count).to eq 11
end end
it 'contains unspecified nodes' do 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 ...@@ -869,10 +869,4 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end end
end 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 end
...@@ -268,6 +268,108 @@ module Gitlab ...@@ -268,6 +268,108 @@ module Gitlab
end end
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 describe 'only / except policies validations' do
context 'when `only` has an invalid value' do context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
......
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Ci::CreatePipelineService do describe Ci::CreatePipelineService do
context 'rules' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
let(:ref) { 'refs/heads/master' } let(:ref) { 'refs/heads/master' }
let(:source) { :push } let(:source) { :push }
let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project, user, { ref: ref }) } let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) } let(:pipeline) { service.execute(source) }
let(:build_names) { pipeline.builds.pluck(:name) } let(:build_names) { pipeline.builds.pluck(:name) }
context 'job:rules' do
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)
...@@ -41,6 +41,7 @@ describe Ci::CreatePipelineService do ...@@ -41,6 +41,7 @@ describe Ci::CreatePipelineService do
start_in: 4 hours start_in: 4 hours
EOY EOY
end end
let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') } let(:regular_job) { pipeline.builds.find_by(name: 'regular-job') }
let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') } let(:rules_job) { pipeline.builds.find_by(name: 'rules-job') }
let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') } let(:delayed_job) { pipeline.builds.find_by(name: 'delayed-job') }
...@@ -91,4 +92,259 @@ describe Ci::CreatePipelineService do ...@@ -91,4 +92,259 @@ describe Ci::CreatePipelineService do
end end
end 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
context 'with job rules' do
let(:config) do
<<-EOY
workflow:
rules:
- if: $CI_COMMIT_REF_NAME =~ /master/
- if: $CI_COMMIT_REF_NAME =~ /feature/
regular-job:
script: 'echo Hello, World!'
rules:
- if: $CI_COMMIT_REF_NAME =~ /wip/
- if: $CI_COMMIT_REF_NAME =~ /feature/
EOY
end
context 'where workflow passes and the job fails' do
let(:ref) { 'refs/heads/master' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches an error about no job in the pipeline' do
expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
end
context 'with workflow:rules shut off' do
before do
stub_feature_flags(workflow_rules: false)
end
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches an error about no job in the pipeline' do
expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
end
end
end
context 'where workflow passes and the job passes' 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
context 'with workflow:rules shut off' do
before do
stub_feature_flags(workflow_rules: false)
end
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 'where workflow fails and the job fails' do
let(:ref) { 'refs/heads/fix' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches an error about workflow rules' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
context 'with workflow:rules shut off' do
before do
stub_feature_flags(workflow_rules: false)
end
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches an error about job rules' do
expect(pipeline.errors[:base]).to include('No stages / jobs for this pipeline.')
end
end
end
context 'where workflow fails and the job passes' do
let(:ref) { 'refs/heads/wip' }
it 'does not save the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'attaches an error about workflow rules' do
expect(pipeline.errors[:base]).to include('Pipeline filtered out by workflow rules.')
end
context 'with workflow:rules shut off' do
before do
stub_feature_flags(workflow_rules: false)
end
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
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