diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-17 21:06:14 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-11-17 21:06:14 +0000 |
commit | 77fc73217e022d796ce25c0e684268b1efc680f4 (patch) | |
tree | c5f65b059e3b80037f251e7511533fda9fa478f0 | |
parent | 0a358b68c5a6f3d17c7435714e21fd827fa3cfa8 (diff) | |
download | gitlab-ce-77fc73217e022d796ce25c0e684268b1efc680f4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
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 |