Commit 96251702 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'mc/backstage/needs-canonical-form' into 'master'

Separate bridge and DAG needs

Closes #14928 and #30675

See merge request gitlab-org/gitlab!17539
parents e061d8d5 e7e3d6d2
...@@ -23,27 +23,35 @@ module EE ...@@ -23,27 +23,35 @@ module EE
validates :name, presence: true validates :name, presence: true
validates :name, type: Symbol validates :name, type: Symbol
validate do
unless trigger.present? || needs.present?
errors.add(:config, 'should contain either a trigger or a needs:pipeline')
end
end
with_options allow_nil: true do with_options allow_nil: true do
validates :when, validates :when,
inclusion: { in: %w[on_success on_failure always], inclusion: { in: %w[on_success on_failure always],
message: 'should be on_success, on_failure or always' } message: 'should be on_success, on_failure or always' }
validates :extends, type: String validates :extends, type: String
end end
validate on: :composed do
unless trigger.present? || bridge_needs.present?
errors.add(:config, 'should contain either a trigger or a needs:pipeline')
end
end
validate on: :composed do
next unless bridge_needs.present?
next if bridge_needs.one?
errors.add(:config, 'should contain at most one bridge need')
end
end end
entry :trigger, ::EE::Gitlab::Ci::Config::Entry::Trigger, entry :trigger, ::EE::Gitlab::Ci::Config::Entry::Trigger,
description: 'CI/CD Bridge downstream trigger definition.', description: 'CI/CD Bridge downstream trigger definition.',
inherit: false inherit: false
entry :needs, ::EE::Gitlab::Ci::Config::Entry::Needs, entry :needs, ::Gitlab::Ci::Config::Entry::Needs,
description: 'CI/CD Bridge needs dependency definition.', description: 'CI/CD Bridge needs dependency definition.',
inherit: false inherit: false,
metadata: { allowed_needs: %i[bridge job] }
entry :stage, ::Gitlab::Ci::Config::Entry::Stage, entry :stage, ::Gitlab::Ci::Config::Entry::Stage,
description: 'Pipeline stage this job will be executed into.', description: 'Pipeline stage this job will be executed into.',
...@@ -93,6 +101,10 @@ module EE ...@@ -93,6 +101,10 @@ module EE
except: except_value }.compact except: except_value }.compact
end end
def bridge_needs
needs_value[:bridge] if needs_value
end
private private
def overwrite_entry(deps, key, current_entry) def overwrite_entry(deps, key, current_entry)
......
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Config
module Entry
module Need
extend ActiveSupport::Concern
prepended do
strategy :Bridge, class: EE::Gitlab::Ci::Config::Entry::Need::Bridge, if: -> (config) { config.is_a?(Hash) }
end
class Bridge < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[pipeline].freeze
attributes :pipeline
validations do
validates :config, presence: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :pipeline, type: String, presence: true
end
def type
:bridge
end
end
end
end
end
end
end
end
# frozen_string_literal: true
module EE
module Gitlab
module Ci
module Config
module Entry
##
# Entry that represents a cross-project needs dependency.
#
class Needs < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[pipeline].freeze
attributes :pipeline
validations do
validates :config, presence: true
validates :config, allowed_keys: ALLOWED_KEYS
validates :pipeline, type: String, presence: true
end
end
end
end
end
end
end
...@@ -88,7 +88,7 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do ...@@ -88,7 +88,7 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do
describe '#value' do describe '#value' do
it 'is returns a bridge job configuration' do it 'is returns a bridge job configuration' do
expect(subject.value).to eq(name: :my_bridge, expect(subject.value).to eq(name: :my_bridge,
needs: { pipeline: 'some/project' }, needs: { bridge: [{ pipeline: 'some/project' }] },
ignore: false, ignore: false,
stage: 'test', stage: 'test',
only: { refs: %w[branches tags] }) only: { refs: %w[branches tags] })
...@@ -160,6 +160,50 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do ...@@ -160,6 +160,50 @@ describe EE::Gitlab::Ci::Config::Entry::Bridge do
end end
end end
context 'when bridge has only job needs' do
let(:config) do
{
needs: ['some_job']
}
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
end
context 'when bridge has bridge and job needs' do
let(:config) do
{
trigger: 'other-project',
needs: ['some_job', { pipeline: 'some/other_project' }]
}
end
describe '#valid?' do
it { is_expected.to be_valid }
end
end
context 'when bridge has more than one valid bridge needs' do
let(:config) do
{
trigger: 'other-project',
needs: [{ pipeline: 'some/project' }, { pipeline: 'some/other_project' }]
}
end
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns an error about too many bridge needs' do
expect(subject.errors).to contain_exactly('bridge config should contain at most one bridge need')
end
end
end
context 'when bridge config contains unknown keys' do context 'when bridge config contains unknown keys' do
let(:config) { { unknown: 123 } } let(:config) { { unknown: 123 } }
......
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Need do
subject { described_class.new(config) }
context 'when upstream is specified' do
let(:config) { { pipeline: 'some/project' } }
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#value' do
it 'returns job needs configuration' do
expect(subject.value).to eq(pipeline: 'some/project')
end
end
end
context 'when need is empty' do
let(:config) { {} }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'is returns an error about an empty config' do
expect(subject.errors)
.to include("bridge config can't be blank")
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
require_dependency 'active_model'
describe EE::Gitlab::Ci::Config::Entry::Needs do describe ::Gitlab::Ci::Config::Entry::Needs do
subject { described_class.new(config) } subject(:needs) { described_class.new(config) }
context 'when upstream config is a non-empty string' do before do
let(:config) { { pipeline: 'some/project' } } needs.metadata[:allowed_needs] = %i[job bridge]
end
describe '#valid?' do describe 'validations' do
it { is_expected.to be_valid } before do
needs.compose!
end end
describe '#value' do context 'when entry config value is correct' do
it 'is returns a upstream configuration hash' do let(:config) { ['job_name', pipeline: 'some/project'] }
expect(subject.value).to eq(pipeline: 'some/project')
describe '#valid?' do
it { is_expected.to be_valid }
end end
end end
end
context 'when upstream config an empty string' do context 'when wrong needs type is used' do
let(:config) { '' } let(:config) { ['job_name', { pipeline: 'some/project' }, 123] }
describe '#valid?' do describe '#valid?' do
it { is_expected.not_to be_valid } it { is_expected.not_to be_valid }
end end
describe '#errors' do describe '#errors' do
it 'is returns an error about an empty config' do it 'returns error about incorrect type' do
expect(subject.errors.first) expect(needs.errors).to contain_exactly(
.to eq("needs config can't be blank") 'need has an unsupported type')
end
end end
end end
end
context 'when upstream configuration is not valid' do context 'when bridge needs has wrong attributes' do
context 'when branch is not provided' do let(:config) { ['job_name', project: 'some/project'] }
let(:config) { { pipeline: 123 } }
describe '#valid?' do describe '#valid?' do
it { is_expected.not_to be_valid } it { is_expected.not_to be_valid }
end end
end
end
describe '#errors' do describe '.compose!' do
it 'returns an error message' do context 'when valid job entries composed' do
expect(subject.errors.first) let(:config) { ['first_job_name', pipeline: 'some/project'] }
.to eq('needs pipeline should be a string')
before do
needs.compose!
end
it 'is valid' do
expect(needs).to be_valid
end
describe '#value' do
it 'returns key value' do
expect(needs.value).to eq(
job: [{ name: 'first_job_name' }],
bridge: [{ pipeline: 'some/project' }]
)
end
end
describe '#descendants' do
it 'creates valid descendant nodes' do
expect(needs.descendants.count).to eq 2
expect(needs.descendants)
.to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need))
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Job do
let(:entry) { described_class.new(config, name: :rspec) }
describe 'validations' do
before do
entry.compose!
end
context 'when entry value is not correct' do
context 'when has needs' do
context 'when needs is bridge type' do
let(:config) do
{
script: 'echo',
stage: 'test',
needs: { pipeline: 'some/project' }
}
end
it 'returns error about invalid needs type' do
expect(entry).not_to be_valid
expect(entry.errors).to contain_exactly('needs config uses invalid types: bridge')
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::YamlProcessor do
describe 'Bridge Needs' do
let(:config) do
{
build: { stage: 'build', script: 'test' },
bridge: { stage: 'test', needs: needs }
}
end
subject { described_class.new(YAML.dump(config)) }
context 'needs upstream pipeline' do
let(:needs) { { pipeline: 'some/project' } }
it 'creates jobs with valid specification' do
expect(subject.builds.size).to eq(2)
expect(subject.builds[0]).to eq(
stage: "build",
stage_idx: 1,
name: "build",
options: {
script: ["test"]
},
when: "on_success",
allow_failure: false,
yaml_variables: []
)
expect(subject.builds[1]).to eq(
stage: "test",
stage_idx: 2,
name: "bridge",
options: {
bridge_needs: { pipeline: 'some/project' }
},
when: "on_success",
allow_failure: false,
yaml_variables: []
)
end
end
context 'needs both job and pipeline' do
let(:needs) { ['build', { pipeline: 'some/project' }] }
it 'creates jobs with valid specification' do
expect(subject.builds.size).to eq(2)
expect(subject.builds[0]).to eq(
stage: "build",
stage_idx: 1,
name: "build",
options: {
script: ["test"]
},
when: "on_success",
allow_failure: false,
yaml_variables: []
)
expect(subject.builds[1]).to eq(
stage: "test",
stage_idx: 2,
name: "bridge",
options: {
bridge_needs: { pipeline: 'some/project' }
},
needs_attributes: [
{ name: "build" }
],
when: "on_success",
allow_failure: false,
yaml_variables: []
)
end
end
end
end
...@@ -50,7 +50,6 @@ module Gitlab ...@@ -50,7 +50,6 @@ module Gitlab
validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) } validates :timeout, duration: { limit: ChronicDuration.output(Project::MAX_BUILD_TIMEOUT) }
validates :dependencies, array_of_strings: true validates :dependencies, array_of_strings: true
validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true validates :extends, array_of_strings_or_string: true
validates :rules, array_of_hashes: true validates :rules, array_of_hashes: true
end end
...@@ -114,6 +113,11 @@ module Gitlab ...@@ -114,6 +113,11 @@ module Gitlab
description: 'List of evaluable Rules to determine job inclusion.', description: 'List of evaluable Rules to determine job inclusion.',
inherit: false inherit: false
entry :needs, Entry::Needs,
description: 'Needs configuration for this job.',
metadata: { allowed_needs: %i[job] },
inherit: false
entry :variables, Entry::Variables, entry :variables, Entry::Variables,
description: 'Environment variables available for this job.', description: 'Environment variables available for this job.',
inherit: false inherit: false
......
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
class Need < ::Gitlab::Config::Entry::Simplifiable
strategy :Job, if: -> (config) { config.is_a?(String) }
class Job < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
validates :config, type: String
end
def type
:job
end
def value
{ name: @config }
end
end
class UnknownStrategy < ::Gitlab::Config::Entry::Node
def type
end
def value
end
def errors
["#{location} has an unsupported type"]
end
end
end
end
end
end
end
::Gitlab::Ci::Config::Entry::Need.prepend_if_ee('::EE::Gitlab::Ci::Config::Entry::Need')
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents a set of needs dependencies.
#
class Needs < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, presence: true
validate do
unless config.is_a?(Hash) || config.is_a?(Array)
errors.add(:config, 'can only be a Hash or an Array')
end
end
validate on: :composed do
extra_keys = value.keys - opt(:allowed_needs)
if extra_keys.any?
errors.add(:config, "uses invalid types: #{extra_keys.join(', ')}")
end
end
end
def compose!(deps = nil)
super(deps) do
[@config].flatten.each_with_index do |need, index|
@entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Need)
.value(need)
.with(key: "need", parent: self, description: "need definition.") # rubocop:disable CodeReuse/ActiveRecord
.create!
end
@entries.each_value do |entry|
entry.compose!(deps)
end
end
end
def value
values = @entries.values.select(&:type)
values.group_by(&:type).transform_values do |values|
values.map(&:value)
end
end
end
end
end
end
end
...@@ -40,7 +40,7 @@ module Gitlab ...@@ -40,7 +40,7 @@ module Gitlab
environment: job[:environment_name], environment: job[:environment_name],
coverage_regex: job[:coverage], coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name), yaml_variables: yaml_variables(name),
needs_attributes: job[:needs]&.map { |need| { name: need } }, needs_attributes: job.dig(:needs, :job),
interruptible: job[:interruptible], interruptible: job[:interruptible],
rules: job[:rules], rules: job[:rules],
options: { options: {
...@@ -59,7 +59,7 @@ module Gitlab ...@@ -59,7 +59,7 @@ module Gitlab
instance: job[:instance], instance: job[:instance],
start_in: job[:start_in], start_in: job[:start_in],
trigger: job[:trigger], trigger: job[:trigger],
bridge_needs: job[:needs] bridge_needs: job.dig(:needs, :bridge)&.first
}.compact }.compact }.compact }.compact
end end
...@@ -159,17 +159,19 @@ module Gitlab ...@@ -159,17 +159,19 @@ module Gitlab
end end
def validate_job_needs!(name, job) def validate_job_needs!(name, job)
return unless job[:needs] return unless job.dig(:needs, :job)
stage_index = @stages.index(job[:stage]) stage_index = @stages.index(job[:stage])
job[:needs].each do |need| job.dig(:needs, :job).each do |need|
raise ValidationError, "#{name} job: undefined need: #{need}" unless @jobs[need.to_sym] need_job_name = need[:name]
needs_stage_index = @stages.index(@jobs[need.to_sym][:stage]) raise ValidationError, "#{name} job: undefined need: #{need_job_name}" unless @jobs[need_job_name.to_sym]
needs_stage_index = @stages.index(@jobs[need_job_name.to_sym][:stage])
unless needs_stage_index.present? && needs_stage_index < stage_index unless needs_stage_index.present? && needs_stage_index < stage_index
raise ValidationError, "#{name} job: need #{need} is not defined in prior stages" raise ValidationError, "#{name} job: need #{need_job_name} is not defined in prior stages"
end end
end end
end end
......
...@@ -29,22 +29,24 @@ module Gitlab ...@@ -29,22 +29,24 @@ module Gitlab
def compose!(deps = nil) def compose!(deps = nil)
return unless valid? return unless valid?
self.class.nodes.each do |key, factory| super do
# If we override the config type validation self.class.nodes.each do |key, factory|
# we can end with different config types like String # If we override the config type validation
next unless config.is_a?(Hash) # we can end with different config types like String
next unless config.is_a?(Hash)
factory factory
.value(config[key]) .value(config[key])
.with(key: key, parent: self) .with(key: key, parent: self)
entries[key] = factory.create! entries[key] = factory.create!
end end
yield if block_given? yield if block_given?
entries.each_value do |entry| entries.each_value do |entry|
entry.compose!(deps) entry.compose!(deps)
end
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -67,12 +69,13 @@ module Gitlab ...@@ -67,12 +69,13 @@ module Gitlab
private private
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil) def entry(key, entry, description: nil, default: nil, inherit: nil, reserved: nil, metadata: {})
factory = ::Gitlab::Config::Entry::Factory.new(entry) factory = ::Gitlab::Config::Entry::Factory.new(entry)
.with(description: description) .with(description: description)
.with(default: default) .with(default: default)
.with(inherit: inherit) .with(inherit: inherit)
.with(reserved: reserved) .with(reserved: reserved)
.metadata(metadata)
(@nodes ||= {}).merge!(key.to_sym => factory) (@nodes ||= {}).merge!(key.to_sym => factory)
end end
......
...@@ -112,6 +112,10 @@ module Gitlab ...@@ -112,6 +112,10 @@ module Gitlab
@aspects ||= [] @aspects ||= []
end end
def self.with_aspect(blk)
self.aspects.append(blk)
end
private private
attr_reader :entries attr_reader :entries
......
...@@ -4,11 +4,11 @@ module Gitlab ...@@ -4,11 +4,11 @@ module Gitlab
module Config module Config
module Entry module Entry
class Simplifiable < SimpleDelegator class Simplifiable < SimpleDelegator
EntryStrategy = Struct.new(:name, :condition) EntryStrategy = Struct.new(:name, :klass, :condition)
attr_reader :subject attr_reader :subject
def initialize(config, **metadata) def initialize(config, **metadata, &blk)
unless self.class.const_defined?(:UnknownStrategy) unless self.class.const_defined?(:UnknownStrategy)
raise ArgumentError, 'UndefinedStrategy not available!' raise ArgumentError, 'UndefinedStrategy not available!'
end end
...@@ -19,14 +19,13 @@ module Gitlab ...@@ -19,14 +19,13 @@ module Gitlab
entry = self.class.entry_class(strategy) entry = self.class.entry_class(strategy)
@subject = entry.new(config, metadata) @subject = entry.new(config, metadata, &blk)
yield(@subject) if block_given?
super(@subject) super(@subject)
end end
def self.strategy(name, **opts) def self.strategy(name, **opts)
EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy| EntryStrategy.new(name, opts.dig(:class), opts.fetch(:if)).tap do |strategy|
strategies.append(strategy) strategies.append(strategy)
end end
end end
...@@ -37,7 +36,7 @@ module Gitlab ...@@ -37,7 +36,7 @@ module Gitlab
def self.entry_class(strategy) def self.entry_class(strategy)
if strategy.present? if strategy.present?
self.const_get(strategy.name, false) strategy.klass || self.const_get(strategy.name, false)
else else
self::UnknownStrategy self::UnknownStrategy
end end
......
...@@ -7,14 +7,27 @@ module Gitlab ...@@ -7,14 +7,27 @@ module Gitlab
extend ActiveSupport::Concern extend ActiveSupport::Concern
def self.included(node) def self.included(node)
node.aspects.append -> do node.with_aspect -> do
@validator = self.class.validator.new(self) validate(:new)
@validator.validate(:new)
end end
end end
def validator
@validator ||= self.class.validator.new(self)
end
def validate(context = nil)
validator.validate(context)
end
def compose!(deps = nil, &blk)
super(deps, &blk)
validate(:composed)
end
def errors def errors
@validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables validator.messages + descendants.flat_map(&:errors)
end end
class_methods do class_methods do
......
...@@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do let(:result) do
%i[before_script script stage type after_script cache %i[before_script script stage type after_script cache
image services only except rules variables artifacts image services only except rules needs variables artifacts
environment coverage retry] environment coverage retry]
end end
...@@ -384,21 +384,6 @@ describe Gitlab::Ci::Config::Entry::Job do ...@@ -384,21 +384,6 @@ describe Gitlab::Ci::Config::Entry::Job do
end end
context 'when has needs' do context 'when has needs' do
context 'that are not a array of strings' do
let(:config) do
{
stage: 'test',
script: 'echo',
needs: 'build-job'
}
end
it 'returns error about invalid type' do
expect(entry).not_to be_valid
expect(entry.errors).to include 'job needs should be an array of strings'
end
end
context 'when have dependencies that are not subset of needs' do context 'when have dependencies that are not subset of needs' do
let(:config) do let(:config) do
{ {
......
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Need do
subject(:need) { described_class.new(config) }
context 'when job is specified' do
let(:config) { 'job_name' }
describe '#valid?' do
it { is_expected.to be_valid }
end
describe '#value' do
it 'returns job needs configuration' do
expect(need.value).to eq(name: 'job_name')
end
end
end
context 'when need is empty' do
let(:config) { '' }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'is returns an error about an empty config' do
expect(need.errors)
.to contain_exactly("job config can't be blank")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) }
before do
needs.metadata[:allowed_needs] = %i[job]
end
describe 'validations' do
before do
needs.compose!
end
context 'when entry config value is correct' do
let(:config) { ['job_name'] }
describe '#valid?' do
it { is_expected.to be_valid }
end
end
context 'when config value has wrong type' do
let(:config) { 123 }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about incorrect type' do
expect(needs.errors)
.to include('needs config can only be a hash or an array')
end
end
end
context 'when wrong needs type is used' do
let(:config) { [123] }
describe '#valid?' do
it { is_expected.not_to be_valid }
end
describe '#errors' do
it 'returns error about incorrect type' do
expect(needs.errors).to contain_exactly(
'need has an unsupported type')
end
end
end
end
describe '.compose!' do
context 'when valid job entries composed' do
let(:config) { %w[first_job_name second_job_name] }
before do
needs.compose!
end
describe '#value' do
it 'returns key value' do
expect(needs.value).to eq(
job: [
{ name: 'first_job_name' },
{ name: 'second_job_name' }
]
)
end
end
describe '#descendants' do
it 'creates valid descendant nodes' do
expect(needs.descendants.count).to eq 2
expect(needs.descendants)
.to all(be_an_instance_of(::Gitlab::Ci::Config::Entry::Need))
end
end
end
end
end
...@@ -1253,7 +1253,7 @@ module Gitlab ...@@ -1253,7 +1253,7 @@ module Gitlab
end end
end end
describe "Needs" do describe "Job Needs" do
let(:needs) { } let(:needs) { }
let(:dependencies) { } let(:dependencies) { }
...@@ -1293,12 +1293,7 @@ module Gitlab ...@@ -1293,12 +1293,7 @@ module Gitlab
stage: "test", stage: "test",
stage_idx: 2, stage_idx: 2,
name: "test1", name: "test1",
options: { options: { script: ["test"] },
script: ["test"],
# This does not make sense, there is a follow-up:
# https://gitlab.com/gitlab-org/gitlab-foss/issues/65569
bridge_needs: %w[build1 build2]
},
needs_attributes: [ needs_attributes: [
{ name: "build1" }, { name: "build1" },
{ name: "build2" } { name: "build2" }
...@@ -1310,12 +1305,6 @@ module Gitlab ...@@ -1310,12 +1305,6 @@ module Gitlab
end end
end end
context 'needs two builds defined as symbols' do
let(:needs) { [:build1, :build2] }
it { expect { subject }.not_to raise_error }
end
context 'undefined need' do context 'undefined need' do
let(:needs) { ['undefined'] } let(:needs) { ['undefined'] }
......
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