summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-11-17 21:06:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-11-17 21:06:14 +0000
commit77fc73217e022d796ce25c0e684268b1efc680f4 (patch)
treec5f65b059e3b80037f251e7511533fda9fa478f0
parent0a358b68c5a6f3d17c7435714e21fd827fa3cfa8 (diff)
downloadgitlab-ce-77fc73217e022d796ce25c0e684268b1efc680f4.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/services/ci/create_pipeline_service.rb1
-rw-r--r--lib/gitlab/ci/build/context/base.rb35
-rw-r--r--lib/gitlab/ci/build/context/build.rb41
-rw-r--r--lib/gitlab/ci/build/context/global.rb41
-rw-r--r--lib/gitlab/ci/build/policy/changes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb2
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb2
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb2
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb4
-rw-r--r--lib/gitlab/ci/build/rules.rb14
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb4
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/exists.rb2
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb7
-rw-r--r--lib/gitlab/ci/config/entry/root.rb5
-rw-r--r--lib/gitlab/ci/config/entry/workflow.rb25
-rw-r--r--lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb50
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb38
-rw-r--r--lib/gitlab/ci/yaml_processor.rb28
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/build/context/global_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/rules/rule_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/config/entry/default_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/config/entry/workflow_spec.rb76
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb102
-rw-r--r--spec/services/ci/create_pipeline_service/rules_spec.rb272
32 files changed, 820 insertions, 83 deletions
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index eb4176035d3..74c188c39d5 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -12,6 +12,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
+ Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules,
Gitlab::Ci::Pipeline::Chain::Limit::Size,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create,
diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb
new file mode 100644
index 00000000000..02b97ea76e9
--- /dev/null
+++ b/lib/gitlab/ci/build/context/base.rb
@@ -0,0 +1,35 @@
+# 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
diff --git a/lib/gitlab/ci/build/context/build.rb b/lib/gitlab/ci/build/context/build.rb
new file mode 100644
index 00000000000..dfd86d3ad72
--- /dev/null
+++ b/lib/gitlab/ci/build/context/build.rb
@@ -0,0 +1,41 @@
+# 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
diff --git a/lib/gitlab/ci/build/context/global.rb b/lib/gitlab/ci/build/context/global.rb
new file mode 100644
index 00000000000..fdd3ac358d5
--- /dev/null
+++ b/lib/gitlab/ci/build/context/global.rb
@@ -0,0 +1,41 @@
+# 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
diff --git a/lib/gitlab/ci/build/policy/changes.rb b/lib/gitlab/ci/build/policy/changes.rb
index 9c705a1cd3e..9ae4198bbf7 100644
--- a/lib/gitlab/ci/build/policy/changes.rb
+++ b/lib/gitlab/ci/build/policy/changes.rb
@@ -9,7 +9,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index 4c7dc947cd0..4e8693724e5 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -11,7 +11,7 @@ module Gitlab
end
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
pipeline.has_kubernetes_active?
end
end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index c3005303fd8..afe0ccb361e 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -9,7 +9,7 @@ module Gitlab
@patterns = Array(refs)
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index ceb5210cfb5..1394340ce1f 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -17,7 +17,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index e9c8864123f..7b1ce6330f0 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -9,8 +9,8 @@ module Gitlab
@expressions = Array(expressions)
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
+ def satisfied_by?(pipeline, context)
+ variables = context.variables
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
index 43399c74457..c705b6f86c7 100644
--- a/lib/gitlab/ci/build/rules.rb
+++ b/lib/gitlab/ci/build/rules.rb
@@ -13,17 +13,21 @@ module Gitlab
options: { start_in: start_in }.compact
}.compact
end
+
+ def pass?
+ self.when != 'never'
+ end
end
- def initialize(rule_hashes, default_when = 'on_success')
+ def initialize(rule_hashes, default_when:)
@rule_list = Rule.fabricate_list(rule_hashes)
@default_when = default_when
end
- def evaluate(pipeline, build)
+ def evaluate(pipeline, context)
if @rule_list.nil?
Result.new(@default_when)
- elsif matched_rule = match_rule(pipeline, build)
+ elsif matched_rule = match_rule(pipeline, context)
Result.new(
matched_rule.attributes[:when] || @default_when,
matched_rule.attributes[:start_in]
@@ -35,8 +39,8 @@ module Gitlab
private
- def match_rule(pipeline, build)
- @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ def match_rule(pipeline, context)
+ @rule_list.find { |rule| rule.matches?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
index 8d52158c8d2..077e4d150fb 100644
--- a/lib/gitlab/ci/build/rules/rule.rb
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -23,8 +23,8 @@ module Gitlab
end
end
- def matches?(pipeline, build)
- @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ def matches?(pipeline, context)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, context) }
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
index bf787fe95a6..6d4bbbb8c21 100644
--- a/lib/gitlab/ci/build/rules/rule/clause.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -20,7 +20,7 @@ module Gitlab
@spec = spec
end
- def satisfied_by?(pipeline, seed = nil)
+ def satisfied_by?(pipeline, context = nil)
raise NotImplementedError
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
index 81d2ee6c24c..728a66ca87f 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -8,7 +8,7 @@ module Gitlab
@globs = Array(globs)
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
return true if pipeline.modified_paths.nil?
pipeline.modified_paths.any? do |path|
diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
index 62f8371283f..85e77438f51 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
@@ -15,7 +15,7 @@ module Gitlab
@exact_globs, @pattern_globs = globs.partition(&method(:exact_glob?))
end
- def satisfied_by?(pipeline, seed)
+ def satisfied_by?(pipeline, context)
paths = worktree_paths(pipeline)
exact_matches?(paths) || pattern_matches?(paths)
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
index 18c3b450f95..6143a736ca6 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/if.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -8,10 +8,9 @@ module Gitlab
@expression = expression
end
- def satisfied_by?(pipeline, seed)
- variables = seed.scoped_variables_hash
-
- ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ def satisfied_by?(pipeline, context)
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(
+ @expression, context.variables).truthful?
end
end
end
diff --git a/lib/gitlab/ci/config/entry/root.rb b/lib/gitlab/ci/config/entry/root.rb
index 07022ff7b54..25fb278d9b8 100644
--- a/lib/gitlab/ci/config/entry/root.rb
+++ b/lib/gitlab/ci/config/entry/root.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
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
validates :config, allowed_keys: ALLOWED_KEYS
@@ -64,6 +64,9 @@ module Gitlab
description: 'Configure caching between build jobs.',
reserved: true
+ entry :workflow, Entry::Workflow,
+ description: 'List of evaluable rules to determine Pipeline status'
+
helpers :default, :jobs, :stages, :types, :variables
delegate :before_script_value,
diff --git a/lib/gitlab/ci/config/entry/workflow.rb b/lib/gitlab/ci/config/entry/workflow.rb
new file mode 100644
index 00000000000..a51a3fbdcd2
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/workflow.rb
@@ -0,0 +1,25 @@
+# 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
diff --git a/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
new file mode 100644
index 00000000000..5b46a43b725
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules.rb
@@ -0,0 +1,50 @@
+# 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
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 1d698a32ba8..dce56b22666 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -28,7 +28,7 @@ module Gitlab
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
@rules = Gitlab::Ci::Build::Rules
- .new(attributes.delete(:rules))
+ .new(attributes.delete(:rules), default_when: 'on_success')
@cache = Seed::Build::Cache
.new(pipeline, attributes.delete(:cache))
end
@@ -40,7 +40,7 @@ module Gitlab
def included?
strong_memoize(:inclusion) do
if @using_rules
- included_by_rules?
+ rules_result.pass?
elsif @using_only || @using_except
all_of_only? && none_of_except?
else
@@ -83,26 +83,14 @@ module Gitlab
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
def all_of_only?
- @only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @only.all? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def none_of_except?
- @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
+ @except.none? { |spec| spec.satisfied_by?(@pipeline, evaluate_context) }
end
def needs_errors
@@ -144,13 +132,21 @@ module Gitlab
}
end
- def included_by_rules?
- rules_attributes[:when] != 'never'
+ def rules_attributes
+ return {} unless @using_rules
+
+ rules_result.build_attributes
end
- def rules_attributes
- strong_memoize(:rules_attributes) do
- @using_rules ? @rules.evaluate(@pipeline, self).build_attributes : {}
+ def rules_result
+ strong_memoize(:rules_result) do
+ @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
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index f48ffa9c1f7..833c545fc5b 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -39,7 +39,7 @@ module Gitlab
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
- yaml_variables: yaml_variables(name),
+ yaml_variables: transform_to_yaml_variables(job_variables(name)),
needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible],
rules: job[:rules],
@@ -83,6 +83,13 @@ module Gitlab
end
end
+ def workflow_attributes
+ {
+ rules: @config.dig(:workflow, :rules),
+ yaml_variables: transform_to_yaml_variables(@variables)
+ }
+ end
+
def self.validation_message(content, opts = {})
return 'Please provide content of .gitlab-ci.yml' if content.blank?
@@ -118,20 +125,17 @@ module Gitlab
end
end
- def yaml_variables(name)
- variables = (@variables || {})
- .merge(job_variables(name))
+ def job_variables(name)
+ job_variables = @jobs.dig(name.to_sym, :variables)
- variables.map do |key, value|
- { key: key.to_s, value: value, public: true }
- end
+ @variables.to_h
+ .merge(job_variables.to_h)
end
- def job_variables(name)
- job = @jobs[name.to_sym]
- return {} unless job
-
- job[:variables] || {}
+ def transform_to_yaml_variables(variables)
+ variables.to_h.map do |key, value|
+ { key: key.to_s, value: value, public: true }
+ end
end
def validate_job_stage!(name, job)
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
new file mode 100644
index 00000000000..3adde213f59
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -0,0 +1,26 @@
+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
diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb
new file mode 100644
index 00000000000..6bc8f862779
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/context/global_spec.rb
@@ -0,0 +1,25 @@
+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
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index 7140c14facb..66f2cb640b9 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
index e0f341461fb..04cdaa9d0ae 100644
--- a/spec/lib/gitlab/ci/build/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb
index b783bbb8287..1ebcc4f9414 100644
--- a/spec/lib/gitlab/ci/build/rules_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules_spec.rb
@@ -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'))
diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb
index a0856037340..dad4f408e50 100644
--- a/spec/lib/gitlab/ci/config/entry/default_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb
index 3877eec8887..43bd53b780f 100644
--- a/spec/lib/gitlab/ci/config/entry/root_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
new file mode 100644
index 00000000000..f2832b94bf0
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb
@@ -0,0 +1,76 @@
+# 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
diff --git a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb
new file mode 100644
index 00000000000..7b76adaf683
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb
@@ -0,0 +1,60 @@
+# 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
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 62e5fd566f7..53dcb6359fe 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 35a4749922e..4b1c7483b11 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -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 } } }
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 40a3b115cb5..c922266647b 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -1,16 +1,16 @@
# 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
+
+ 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