Commit d609eaed authored by Grzegorz Bizon's avatar Grzegorz Bizon

Implement pipeline variables expressions / EE port

parent 1aec54db
......@@ -27,7 +27,9 @@ module Ci
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
# The "environment" field for builds is a String, and is the unexpanded name
##
# The "environment" field for builds is a String, and is the unexpanded name!
#
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
......@@ -203,7 +205,9 @@ module Ci
end
def expanded_environment_name
ExpandVariables.expand(environment, simple_variables) if environment
if has_environment?
ExpandVariables.expand(environment, simple_variables)
end
end
def has_environment?
......@@ -253,31 +257,44 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
# Variables whose value does not depend on environment
def simple_variables
variables(environment: nil)
end
# All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables(environment: persisted_environment)
collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
##
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
variables.concat(project.deployment_variables(environment: environment)) if has_environment?
variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
variables.concat(project.group.secret_variables_for(ref, project)) if project.group
variables.concat(secret_variables(environment: environment))
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end
end
##
# Variables that do not depend on the environment name.
#
def simple_variables
scoped_variables(environment: nil).to_runner_variables
end
##
# All variables, including persisted environment variables.
#
def variables
scoped_variables
.concat(persisted_environment_variables)
.to_runner_variables
end
collection.to_runner_variables
def variables_hash
scoped_variables.to_hash
end
def features
......@@ -454,9 +471,14 @@ module Ci
end
end
def secret_variables(environment: persisted_environment)
def secret_group_variables
return [] unless project.group
project.group.secret_variables_for(ref, project)
end
def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end
def steps
......@@ -561,16 +583,16 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_ID', value: id.to_s) if persisted?
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false) if persisted?
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER) if persisted?
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false) if persisted?
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false) if persisted?
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
......@@ -580,7 +602,7 @@ module Ci
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted_environment
return variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
......@@ -593,8 +615,11 @@ module Ci
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
if persisted?
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
end
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
......
---
title: Add support for pipeline variables expressions in only/except
merge_request: 17316
author:
type: added
......@@ -9,7 +9,7 @@ module Gitlab
end
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
pipeline.has_kubernetes_active?
end
end
......
......@@ -7,7 +7,7 @@ module Gitlab
@patterns = Array(refs)
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
@patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
......
......@@ -15,7 +15,7 @@ module Gitlab
@spec = spec
end
def satisfied_by?(pipeline)
def satisfied_by?(pipeline, seed = nil)
raise NotImplementedError
end
end
......
module Gitlab
module Ci
module Build
module Policy
class Variables < Policy::Specification
def initialize(expressions)
@expressions = Array(expressions)
end
def satisfied_by?(pipeline, seed)
variables = seed.to_resource.variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
.new(statement, variables)
end
statements.any?(&:truthful?)
end
end
end
end
end
end
......@@ -25,15 +25,31 @@ module Gitlab
include Entry::Validatable
include Entry::Attributable
attributes :refs, :kubernetes
attributes :refs, :kubernetes, :variables
validations do
validates :config, presence: true
validates :config, allowed_keys: %i[refs kubernetes]
validates :config, allowed_keys: %i[refs kubernetes variables]
validate :variables_expressions_syntax
with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true
validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true
end
def variables_expressions_syntax
return unless variables.is_a?(Array)
statements = variables.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement.new(statement)
end
statements.each do |statement|
unless statement.valid?
errors.add(:variables, "Invalid expression #{statement.inspect}")
end
end
end
end
end
......
......@@ -17,8 +17,6 @@ module Gitlab
# Populate pipeline with all stages and builds from pipeline seeds.
#
pipeline.stage_seeds.each do |stage|
stage.user = current_user
pipeline.stages << stage.to_resource
stage.seeds.each do |build|
......
......@@ -4,7 +4,7 @@ module Gitlab
module Expression
module Lexeme
class String < Lexeme::Value
PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
def initialize(value)
@value = value
......
......@@ -11,7 +11,7 @@ module Gitlab
end
def evaluate(variables = {})
HashWithIndifferentAccess.new(variables).fetch(@name, nil)
variables.with_indifferent_access.fetch(@name, nil)
end
def self.build(string)
......
......@@ -14,12 +14,9 @@ module Gitlab
%w[variable]
].freeze
def initialize(statement, pipeline)
def initialize(statement, variables = {})
@lexer = Expression::Lexer.new(statement)
@variables = pipeline.variables.map do |variable|
[variable.key, variable.value]
end
@variables = variables.with_indifferent_access
end
def parse_tree
......@@ -35,6 +32,20 @@ module Gitlab
def evaluate
parse_tree.evaluate(@variables.to_h)
end
def truthful?
evaluate.present?
end
def inspect
"syntax: #{@lexer.lexemes.join(' ')}"
end
def valid?
parse_tree.is_a?(Lexeme::Base)
rescue StatementError
false
end
end
end
end
......
......@@ -11,21 +11,16 @@ module Gitlab
@pipeline = pipeline
@attributes = attributes
@only = attributes.delete(:only)
@except = attributes.delete(:except)
end
def user=(current_user)
@attributes.merge!(user: current_user)
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
end
def included?
strong_memoize(:inclusion) do
only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
@only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
@except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
end
end
......@@ -33,6 +28,7 @@ module Gitlab
@attributes.merge(
pipeline: @pipeline,
project: @pipeline.project,
user: @pipeline.user,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
......
......@@ -17,10 +17,6 @@ module Gitlab
end
end
def user=(current_user)
@builds.each { |seed| seed.user = current_user }
end
def attributes
{ name: @attributes.fetch(:name),
pipeline: @pipeline,
......
......@@ -30,7 +30,13 @@ module Gitlab
end
def to_runner_variables
self.map(&:to_hash)
self.map(&:to_runner_variable)
end
def to_hash
self.to_runner_variables
.map { |env| [env.fetch(:key), env.fetch(:value)] }
.to_h.with_indifferent_access
end
end
end
......
......@@ -17,7 +17,7 @@ module Gitlab
end
def ==(other)
to_hash == self.class.fabricate(other).to_hash
to_runner_variable == self.class.fabricate(other).to_runner_variable
end
##
......@@ -25,7 +25,7 @@ module Gitlab
# don't expose `file` attribute at all (stems from what the runner
# expects).
#
def to_hash
def to_runner_variable
@variable.reject do |hash_key, hash_value|
hash_key == :file && hash_value == false
end
......
require 'spec_helper'
describe Gitlab::Ci::Build::Policy::Variables do
set(:project) { create(:project) }
let(:pipeline) do
build(:ci_empty_pipeline, project: project, ref: 'master')
end
let(:ci_build) do
build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
end
let(:seed) { double('build seed', to_resource: ci_build) }
before do
pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
end
describe '#satisfied_by?' do
it 'is satisfied by a defined and existing variable' do
policy = described_class.new(['$CI_PROJECT_ID', '$UNDEFINED'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied by an overriden empty variable' do
policy = described_class.new(['$CI_PROJECT_NAME'])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied by a truthy pipeline expression' do
policy = described_class.new([%($CI_PIPELINE_SOURCE == "#{pipeline.source}")])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'is not satisfied by a falsy pipeline expression' do
policy = described_class.new([%($CI_PIPELINE_SOURCE == "invalid source")])
expect(policy).not_to be_satisfied_by(pipeline, seed)
end
it 'is satisfied by a truthy expression using undefined variable' do
policy = described_class.new(['$UNDEFINED', '$UNDEFINED == null'])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'allows to evaluate regular secret variables' do
secret = create(:ci_variable, project: project,
key: 'SECRET',
value: 'secret value')
policy = described_class.new(["$SECRET == 'secret value'"])
expect(policy).to be_satisfied_by(pipeline, seed)
end
it 'does not persist neither pipeline nor build' do
described_class.new('$VAR').satisfied_by?(pipeline, seed)
expect(pipeline).not_to be_persisted
expect(seed.to_resource).not_to be_persisted
end
end
end
......@@ -83,6 +83,31 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
context 'when specifying valid variables expressions policy' do
let(:config) { { variables: ['$VAR == null'] } }
it 'is a correct configuraton' do
expect(entry).to be_valid
expect(entry.value).to eq(config)
end
end
context 'when specifying variables expressions in invalid format' do
let(:config) { { variables: '$MY_VAR' } }
it 'reports an error about invalid format' do
expect(entry.errors).to include /should be an array of strings/
end
end
context 'when specifying invalid variables expressions statement' do
let(:config) { { variables: ['$MY_VAR =='] } }
it 'reports an error about invalid statement' do
expect(entry.errors).to include /invalid expression syntax: variable equals/
end
end
context 'when specifying unknown policy' do
let(:config) { { refs: ['master'], invalid: :something } }
......
......@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
ref: 'master')
ref: 'master',
user: user)
end
let(:command) do
......@@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
it 'correctly assigns user' do
expect(pipeline.builds).to all(have_attributes(user: user))
end
end
context 'when pipeline is empty' do
......
......@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
expect(token).not_to be_nil
expect(token.build.evaluate).to eq 'some " string'
end
it 'allows to use an empty string inside single quotes' do
scanner = StringScanner.new(%(''))
token = described_class.scan(scanner)
expect(token.build.evaluate).to eq ''
end
it 'allow to use an empty string inside double quotes' do
scanner = StringScanner.new(%(""))
token = described_class.scan(scanner)
expect(token.build.evaluate).to eq ''
end
end
end
......
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Statement do
let(:pipeline) { build(:ci_pipeline) }
subject do
described_class.new(text, pipeline)
described_class.new(text, variables)
end
let(:variables) do
{ 'PRESENT_VARIABLE' => 'my variable',
EMPTY_VARIABLE: '' }
end
before do
pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
describe '.new' do
context 'when variables are not provided' do
it 'allows to properly initializes the statement' do
statement = described_class.new('$PRESENT_VARIABLE')
expect(statement.evaluate).to be_nil
end
end
end
describe '#parse_tree' do
......@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
context 'when expression grammar is incorrect' do
table = [
'$VAR "text"', # missing operator
'== "123"', # invalid right side
"'single quotes'", # single quotes string
'$VAR ==', # invalid right side
'12345', # unknown syntax
'' # empty statement
'$VAR "text"', # missing operator
'== "123"', # invalid left side
'"some string"', # only string provided
'$VAR ==', # invalid right side
'12345', # unknown syntax
'' # empty statement
]
table.each do |syntax|
it "raises an error when syntax is `#{syntax}`" do
expect { described_class.new(syntax, pipeline).parse_tree }
.to raise_error described_class::StatementError
context "when expression grammar is #{syntax.inspect}" do
let(:text) { syntax }
it 'aises a statement error exception' do
expect { subject.parse_tree }
.to raise_error described_class::StatementError
end
it 'is an invalid statement' do
expect(subject).not_to be_valid
end
end
end
end
......@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
expect(subject.parse_tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
it 'is a valid statement' do
expect(subject).to be_valid
end
end
context 'when using a single token' do
let(:text) { '$VARIABLE' }
let(:text) { '$PRESENT_VARIABLE' }
it 'returns a single token instance' do
expect(subject.parse_tree)
......@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
describe '#evaluate' do
statements = [
['$VARIABLE == "my variable"', true],
["$VARIABLE == 'my variable'", true],
['"my variable" == $VARIABLE', true],
['$VARIABLE == null', false],
['$VAR == null', true],
['null == $VAR', true],
['$VARIABLE', 'my variable'],
['$VAR', nil]
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'my variable'", true],
['"my variable" == $PRESENT_VARIABLE', true],
['$PRESENT_VARIABLE == null', false],
['$EMPTY_VARIABLE == null', false],
['"" == $EMPTY_VARIABLE', true],
['$EMPTY_VARIABLE', ''],
['$UNDEFINED_VARIABLE == null', true],
['null == $UNDEFINED_VARIABLE', true],
['$PRESENT_VARIABLE', 'my variable'],
['$UNDEFINED_VARIABLE', nil]
]
statements.each do |expression, value|
......@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
end
end
describe '#truthful?' do
statements = [
['$PRESENT_VARIABLE == "my variable"', true],
["$PRESENT_VARIABLE == 'no match'", false],
['$UNDEFINED_VARIABLE == null', true],
['$PRESENT_VARIABLE', true],
['$UNDEFINED_VARIABLE', false],
['$EMPTY_VARIABLE', false]
]
statements.each do |expression, value|
context "when using expression `#{expression}`" do
let(:text) { expression }
it "returns `#{value.inspect}`" do
expect(subject.truthful?).to eq value
end
end
end
end
end
......@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes user to a build' do
subject.user = user
expect(subject.attributes).to include(user: user)
end
end
describe '#to_resource' do
it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build)
......
......@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes relevant pipeline attributes' do
subject.user = user
expect(subject.seeds.map(&:attributes)).to all(include(user: user))
end
end
describe '#to_resource' do
it 'builds a valid stage object with all builds' do
subject.to_resource.save!
......
......@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do
end
end
describe '#to_hash' do
it 'returns a hash representation of a collection item' do
expect(described_class.new(**variable).to_hash).to eq variable
describe '#to_runner_variable' do
it 'returns a runner-compatible hash representation' do
runner_variable = described_class
.new(**variable)
.to_runner_variable
expect(runner_variable).to eq variable
end
end
end
......@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do
collection = described_class.new([variable])
expect(collection.first.to_hash).to eq variable
expect(collection.first.to_runner_variable).to eq variable
end
it 'can be initialized without an argument' do
......@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do
.to eq [{ key: 'TEST', value: 1, public: true }]
end
end
describe '#to_hash' do
it 'returns regular hash in valid order without duplicates' do
collection = described_class.new
.append(key: 'TEST1', value: 'test-1')
.append(key: 'TEST2', value: 'test-2')
.append(key: 'TEST1', value: 'test-3')
expect(collection.to_hash).to eq('TEST1' => 'test-3',
'TEST2' => 'test-2')
expect(collection.to_hash).to include(TEST1: 'test-3')
expect(collection.to_hash).not_to include(TEST1: 'test-1')
end
end
end
......@@ -1311,6 +1311,14 @@ module Gitlab
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
it 'returns errors if pipeline variables expression is invalid' do
config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
expect { Gitlab::Ci::YamlProcessor.new(config) }
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only variables invalid expression syntax: equals null')
end
end
describe "Validate configuration templates" do
......
......@@ -2019,6 +2019,92 @@ describe Ci::Build do
end
end
end
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline
)
end
it 'returns static predefined variables' do
expect(build.variables.size).to be >= 28
expect(build.variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
expect(build).not_to be_persisted
end
end
end
describe '#scoped_variables' do
context 'when build has not been persisted yet' do
let(:build) do
described_class.new(
name: 'rspec',
stage: 'test',
ref: 'feature',
project: project,
pipeline: pipeline
)
end
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
build.scoped_variables
expect(build).not_to be_persisted
end
it 'returns static predefined variables' do
keys = %w[CI_JOB_NAME
CI_COMMIT_SHA
CI_COMMIT_REF_NAME
CI_COMMIT_REF_SLUG
CI_JOB_STAGE]
variables = build.scoped_variables
variables.map { |env| env[:key] }.tap do |names|
expect(names).to include(*keys)
end
expect(variables)
.to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
end
it 'does not return prohibited variables' do
keys = %w[CI_JOB_ID
CI_JOB_TOKEN
CI_BUILD_ID
CI_BUILD_TOKEN
CI_REGISTRY_USER
CI_REGISTRY_PASSWORD
CI_REPOSITORY_URL
CI_ENVIRONMENT_URL]
build.scoped_variables.map { |env| env[:key] }.tap do |names|
expect(names).not_to include(*keys)
end
end
end
end
describe '#variables_hash' do
before do
project.variables.create!(key: 'MY_VAR', value: 'my value 1')
pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
end
it 'returns a regular hash created in valid order' do
expect(build.variables_hash).to include('MY_VAR': 'my value 2')
expect(build.variables_hash).not_to include('MY_VAR': 'my value 1')
end
end
describe 'state transition: any => [:pending]' do
......
......@@ -353,6 +353,20 @@ describe Ci::Pipeline, :mailer do
end
end
end
context 'when variables policy is specified' do
let(:config) do
{ unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } }
end
it 'returns stage seeds only when variables expression is truthy' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.dig(0, 0, :name)).to eq 'unit'
end
end
end
describe '#seeds_size' do
......
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