diff --git a/.rubocop_todo/rspec/spec_file_path_format.yml b/.rubocop_todo/rspec/spec_file_path_format.yml index 0ed9f1b545812887f32d93cddd49077eb0a95d8e..55d22be40e78516c4c3da6723ecf7e9961cdd2bf 100644 --- a/.rubocop_todo/rspec/spec_file_path_format.yml +++ b/.rubocop_todo/rspec/spec_file_path_format.yml @@ -34,6 +34,7 @@ RSpec/SpecFilePathFormat: - 'spec/requests/api/pages/pages_spec.rb' - 'spec/services/ci/create_pipeline_service/artifacts_spec.rb' - 'spec/services/ci/create_pipeline_service/cache_spec.rb' + - 'spec/services/ci/create_pipeline_service/component_interpolation_spec.rb' - 'spec/services/ci/create_pipeline_service/composite_identity_spec.rb' - 'spec/services/ci/create_pipeline_service/creation_errors_and_warnings_spec.rb' - 'spec/services/ci/create_pipeline_service/cross_project_pipeline_spec.rb' diff --git a/config/feature_flags/beta/ci_component_context_interpolation.yml b/config/feature_flags/beta/ci_component_context_interpolation.yml new file mode 100644 index 0000000000000000000000000000000000000000..36cafc6a3d209494be8c83dd5e640b24843fe28e --- /dev/null +++ b/config/feature_flags/beta/ci_component_context_interpolation.yml @@ -0,0 +1,10 @@ +--- +name: ci_component_context_interpolation +description: Implementing component context interpolation in CI. +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/438275 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/206183 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/571986 +milestone: '18.5' +group: group::pipeline authoring +type: beta +default_enabled: false diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb index d3f6faee4b86e9faca993e1dfd15f1369095a65e..f7978b5822386dc232ddee35ec0817eccb9e6f49 100644 --- a/lib/gitlab/ci/config/external/context.rb +++ b/lib/gitlab/ci/config/external/context.rb @@ -13,7 +13,7 @@ class Context attr_reader :project, :sha, :user, :parent_pipeline, :variables, :pipeline_config, :parallel_requests, :pipeline, :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes, - :pipeline_policy_context + :pipeline_policy_context, :component_data attr_accessor :total_file_size_in_bytes @@ -25,7 +25,7 @@ class Context # rubocop:disable Metrics/ParameterLists -- all arguments needed def initialize( project: nil, pipeline: nil, sha: nil, user: nil, parent_pipeline: nil, variables: nil, - pipeline_config: nil, logger: nil, pipeline_policy_context: nil + pipeline_config: nil, logger: nil, pipeline_policy_context: nil, component_data: nil ) @project = project @pipeline = pipeline @@ -35,6 +35,7 @@ def initialize( @variables = variables || Ci::Variables::Collection.new @pipeline_config = pipeline_config @pipeline_policy_context = pipeline_policy_context + @component_data = component_data || {} @expandset = [] @parallel_requests = [] @execution_deadline = 0 @@ -86,6 +87,7 @@ def mutate(attrs = {}) ctx.max_includes = max_includes ctx.max_total_yaml_size_bytes = max_total_yaml_size_bytes ctx.parallel_requests = parallel_requests + ctx.component_data = component_data end end @@ -138,7 +140,7 @@ def internal_include? protected attr_writer :pipeline, :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes, - :parallel_requests + :parallel_requests, :component_data private diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb index 9dc77252638b3135ad8129471d4b38d5b9136190..c81186b2fe2ec4750f8d66127cd95496359e8e36 100644 --- a/lib/gitlab/ci/config/external/file/base.rb +++ b/lib/gitlab/ci/config/external/file/base.rb @@ -129,7 +129,8 @@ def yaml_context def yaml_context_attributes { - variables: context.variables + variables: context.variables, + component: (ci_component_context_interpolation_enabled? ? context.component_data : {}) } end @@ -160,6 +161,11 @@ def masked_location context.mask_variables_from(location) end end + + def ci_component_context_interpolation_enabled? + ::Feature.enabled?(:ci_component_context_interpolation, context.project) + end + strong_memoize_attr :ci_component_context_interpolation_enabled? end end end diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb index b34c303785b7b0a37d4fa0664cb3dd27025a4f08..8d5061c5455947efbd35aa386d9a0d91eb163c37 100644 --- a/lib/gitlab/ci/config/external/file/component.rb +++ b/lib/gitlab/ci/config/external/file/component.rb @@ -83,10 +83,16 @@ def expand_context_attrs project: component_payload.fetch(:project), sha: component_payload.fetch(:sha), user: context.user, - variables: context.variables + variables: context.variables, + component_data: component_yaml_context } end + override :yaml_context_attributes + def yaml_context_attributes + super.merge(component: component_yaml_context) + end + def masked_blob return unless component_payload @@ -114,6 +120,12 @@ def component_attrs name: component_payload.fetch(:name) } end + + def component_yaml_context + return {} unless ci_component_context_interpolation_enabled? + + component_attrs.slice(*Config::Header::Component::ALLOWED_VALUES) + end end end end diff --git a/lib/gitlab/ci/config/header/component.rb b/lib/gitlab/ci/config/header/component.rb new file mode 100644 index 0000000000000000000000000000000000000000..96ddbd750762fff8e8555d993ecef6d008fcb0fc --- /dev/null +++ b/lib/gitlab/ci/config/header/component.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Header + ## + # Component context configuration used for interpolation with the CI configuration. + # + # This class defines the available component context information that can be used + # in CI configuration interpolation. + # + class Component < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + ALLOWED_VALUES = %i[name sha].freeze + ALLOWED_VALUES_TO_S = ALLOWED_VALUES.map(&:to_s).freeze + + validations do + validates :config, type: Array, array_of_strings: true, allowed_array_values: { in: ALLOWED_VALUES_TO_S } + end + + def value + return [] unless valid? + + config.uniq.map(&:to_sym) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/header/root.rb b/lib/gitlab/ci/config/header/root.rb index fc5a6fcd95ab66b0d14697b98f2551c788f2450f..ebedd851a97561fef7dd6b329f760d27d031cac0 100644 --- a/lib/gitlab/ci/config/header/root.rb +++ b/lib/gitlab/ci/config/header/root.rb @@ -29,6 +29,10 @@ class Root < ::Gitlab::Config::Entry::Node def spec_inputs_value spec_entry.inputs_value end + + def spec_component_value + spec_entry.component_value + end end end end diff --git a/lib/gitlab/ci/config/header/spec.rb b/lib/gitlab/ci/config/header/spec.rb index 4753c1eb4412b158d61fa694051f8ab57031bb15..32e034e319774fe7cc0abbbce3e0ab4a4c882d23 100644 --- a/lib/gitlab/ci/config/header/spec.rb +++ b/lib/gitlab/ci/config/header/spec.rb @@ -7,7 +7,7 @@ module Header class Spec < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Configurable - ALLOWED_KEYS = %i[inputs].freeze + ALLOWED_KEYS = %i[inputs component].freeze validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -17,6 +17,11 @@ class Spec < ::Gitlab::Config::Entry::Node description: 'Allowed input parameters used for interpolation.', inherit: false, metadata: { composable_class: ::Gitlab::Ci::Config::Header::Input } + + entry :component, Header::Component, + description: 'The available component context used for interpolation.', + inherit: false, + default: [] end end end diff --git a/lib/gitlab/ci/config/interpolation/interpolator.rb b/lib/gitlab/ci/config/interpolation/interpolator.rb index 9a5ce5b415023cd57cfb9a48288eec96be5ce804..4589be2a45208243284c2ff3cf76b2388889d8aa 100644 --- a/lib/gitlab/ci/config/interpolation/interpolator.rb +++ b/lib/gitlab/ci/config/interpolation/interpolator.rb @@ -89,13 +89,17 @@ def inputs def context @context ||= Context.new( - { inputs: inputs.to_hash }, variables: yaml_context.variables + { inputs: inputs.to_hash, component: component_data }, variables: yaml_context.variables ) end def template @template ||= Template.new(content, context) end + + def component_data + yaml_context.component.slice(*header.spec_component_value) + end end end end diff --git a/lib/gitlab/ci/config/yaml/context.rb b/lib/gitlab/ci/config/yaml/context.rb index 84c29eac76603248e1e929e95e7149590f69c85a..79922c478690cf38638326a83e3fe84e36094770 100644 --- a/lib/gitlab/ci/config/yaml/context.rb +++ b/lib/gitlab/ci/config/yaml/context.rb @@ -5,10 +5,11 @@ module Ci class Config module Yaml class Context - attr_reader :variables + attr_reader :variables, :component - def initialize(variables: []) + def initialize(variables: [], component: {}) @variables = variables + @component = component end end end diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 13cb3ae613093b4fd59147603aaf1d8c59deb8fc..1ad7febaca81e7382ec9ecfce12ef284b29bfae1 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -31,6 +31,7 @@ it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } it { expect(subject.variables_hash).to include('a' => 'b') } it { expect(subject.pipeline_config).to eq(pipeline_config) } + it { expect(subject.component_data).to eq({}) } end context 'without values' do @@ -42,6 +43,21 @@ it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } it { expect(subject.pipeline_config).to be_nil } + it { expect(subject.component_data).to eq({}) } + end + + context 'with component_data' do + let(:component_data) { { name: 'my-component', sha: 'abc123' } } + let(:attributes) do + { + project: project, + user: user, + sha: sha, + component_data: component_data + } + end + + it { expect(subject.component_data).to eq(component_data) } end describe 'max_includes' do @@ -177,6 +193,7 @@ it { expect(mutated.execution_deadline).to eq(subject.execution_deadline) } it { expect(mutated.logger).to eq(subject.logger) } it { expect(mutated.parallel_requests).to eq(subject.parallel_requests) } + it { expect(mutated.component_data).to eq(subject.component_data) } end context 'with attributes' do diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index d254c276dfc8c1c58ca83b5f89ed5af2a378d04e..075bd7665b580ca6e8c0e1a7e3c69d24177c983d 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -184,7 +184,28 @@ project: project, sha: 'my_component_sha', user: context.user, - variables: context.variables) + variables: context.variables, + component_data: { + name: 'my_component', + sha: 'my_component_sha' + } + ) + end + + context 'when the FF ci_component_context_interpolation is disabled' do + before do + stub_feature_flags(ci_component_context_interpolation: false) + end + + it 'inherits user and variables while changes project and sha without component_data' do + is_expected.to include( + project: project, + sha: 'my_component_sha', + user: context.user, + variables: context.variables, + component_data: {} + ) + end end end diff --git a/spec/lib/gitlab/ci/config/header/component_spec.rb b/spec/lib/gitlab/ci/config/header/component_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..927e1f23d178176f662f4f2d8cb213e7067476a7 --- /dev/null +++ b/spec/lib/gitlab/ci/config/header/component_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Header::Component, feature_category: :pipeline_composition do + let(:component) { described_class.new(config) } + + describe 'validations' do + context 'when config is an array of valid strings' do + let(:config) { %w[name sha] } + + it 'passes validations' do + expect(component).to be_valid + expect(component.errors).to be_empty + end + end + + context 'when config is an empty array' do + let(:config) { [] } + + it 'passes validations' do + expect(component).to be_valid + expect(component.errors).to be_empty + end + end + + context 'when config contains invalid values' do + let(:config) { %w[name invalid_key] } + + it 'fails validations' do + expect(component).not_to be_valid + expect(component.errors).to include(/component config contains unknown values: invalid_key/) + end + end + + context 'when config is not an array' do + let(:config) { 'name' } + + it 'fails validations' do + expect(component).not_to be_valid + expect(component.errors).to include(/component config should be an array/) + end + end + + context 'when config is a hash' do + let(:config) { { name: true } } + + it 'fails validations' do + expect(component).not_to be_valid + expect(component.errors).to include(/component config should be an array/) + end + end + + context 'when config contains non-string values' do + let(:config) { ['name', 123] } + + it 'fails validations' do + expect(component).not_to be_valid + expect(component.errors).to include(/component config should be an array of strings/) + end + end + end + + describe '#value' do + subject(:value) { component.value } + + context 'when config is valid' do + let(:config) { %w[name sha] } + + it { is_expected.to match_array([:name, :sha]) } + end + + context 'when config has duplicate values' do + let(:config) { %w[name name sha] } + + it { is_expected.to match_array([:name, :sha]) } + end + + context 'when config has invalid values' do + let(:config) { %w[name invalid] } + + it { is_expected.to be_empty } + end + + context 'when config is empty array' do + let(:config) { [] } + + it { is_expected.to be_empty } + end + + context 'when config is not an array' do + let(:config) { 'invalid' } + + it { is_expected.to be_empty } + end + + context 'when config is nil' do + let(:config) { nil } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/config/header/root_spec.rb b/spec/lib/gitlab/ci/config/header/root_spec.rb index a481d3f2c096217bb7f3ce82735f9805f6d1f954..2fa9ab98d1385ef1b6fb61b875ff8b4887d2ea5b 100644 --- a/spec/lib/gitlab/ci/config/header/root_spec.rb +++ b/spec/lib/gitlab/ci/config/header/root_spec.rb @@ -130,4 +130,68 @@ }) end end + + describe '#spec_component_value' do + context 'when component is specified' do + let(:header_hash) do + { + spec: { + component: %w[name sha] + } + } + end + + it 'returns the component value as symbols' do + expect(config.spec_component_value).to match_array([:name, :sha]) + end + end + + context 'when component is empty' do + let(:header_hash) do + { + spec: { + component: [] + } + } + end + + it 'returns empty array' do + expect(config.spec_component_value).to be_empty + end + end + + context 'when component is not specified' do + let(:header_hash) do + { + spec: { + inputs: { + foo: { default: 'bar' } + } + } + } + end + + it 'returns empty array by default' do + expect(config.spec_component_value).to be_empty + end + end + + context 'when both inputs and component are specified' do + let(:header_hash) do + { + spec: { + inputs: { + foo: { default: 'bar' } + }, + component: %w[name] + } + } + end + + it 'returns both values correctly' do + expect(config.spec_inputs_value).to eq({ foo: { default: 'bar' } }) + expect(config.spec_component_value).to match_array([:name]) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/header/spec_spec.rb b/spec/lib/gitlab/ci/config/header/spec_spec.rb index 74cfb39dfd509cf17b76c60ab70e2df6933d928c..8c1b081cc4caec227801e8969140f802c80163d2 100644 --- a/spec/lib/gitlab/ci/config/header/spec_spec.rb +++ b/spec/lib/gitlab/ci/config/header/spec_spec.rb @@ -28,6 +28,79 @@ end end + context 'when spec contains component configuration' do + let(:spec_hash) do + { + component: %w[name sha] + } + end + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns the component value' do + expect(config.component_value).to match_array([:name, :sha]) + end + end + + context 'when spec contains both inputs and component' do + let(:spec_hash) do + { + inputs: { + foo: { + default: 'bar' + } + }, + component: %w[name] + } + end + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns both values correctly' do + expect(config.inputs_value).to eq({ foo: { default: 'bar' } }) + expect(config.component_value).to match_array([:name]) + end + end + + context 'when spec contains empty component array' do + let(:spec_hash) do + { + component: [] + } + end + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns empty component value' do + expect(config.component_value).to eq([]) + end + end + + context 'when spec component is not specified' do + let(:spec_hash) do + { + inputs: { + foo: { + default: 'bar' + } + } + } + end + + it 'returns default empty array for component' do + expect(config.component_value).to eq([]) + end + end + context 'when spec contains a required value' do let(:spec_hash) do { inputs: { foo: nil } } diff --git a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 6ed4d70c9abd3831036c9ac571b130fa57304084..5d7be700073cfb3ad55c32a7a62cf5d3b408cac7 100644 --- a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -172,6 +172,76 @@ end end + context 'when using component interpolation' do + let(:yaml_context) do + ::Gitlab::Ci::Config::Yaml::Context.new( + variables: [], + component: { name: 'my-component', sha: 'abc123' } + ) + end + + context 'when component values are specified in spec' do + let(:header) do + { spec: { component: %w[name sha] } } + end + + let(:content) do + { test: 'Component $[[ component.name ]] at $[[ component.sha ]]' } + end + + let(:arguments) { {} } + + it 'correctly interpolates component data' do + subject.interpolate! + + expect(subject).to be_interpolated + expect(subject).to be_valid + expect(subject.to_hash).to eq({ test: 'Component my-component at abc123' }) + end + end + + context 'when component value not in spec is accessed' do + let(:header) do + { spec: { component: %w[name] } } + end + + let(:content) do + { test: 'Component $[[ component.name ]] at $[[ component.sha ]]' } + end + + let(:arguments) { {} } + + it 'returns an error for unspecified component value' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.errors).to include 'unknown interpolation provided: `sha` in `component.sha`' + end + end + + context 'when both inputs and component are used' do + let(:header) do + { spec: { inputs: { env: nil }, component: %w[name] } } + end + + let(:content) do + { + test: 'Deploy to $[[ inputs.env ]] using $[[ component.name ]]' + } + end + + let(:arguments) { { env: 'production' } } + + it 'correctly interpolates both inputs and component data' do + subject.interpolate! + + expect(subject).to be_interpolated + expect(subject).to be_valid + expect(subject.to_hash).to eq({ test: 'Deploy to production using my-component' }) + end + end + end + describe '#to_hash' do context 'when interpolation is not used' do let(:result) do diff --git a/spec/lib/gitlab/ci/config/yaml/context_spec.rb b/spec/lib/gitlab/ci/config/yaml/context_spec.rb index eca928f8bc5f51f923d6276b9cf5c237b0a07d57..ccb535ba327c18797f706ae4d02290e480e3e4c8 100644 --- a/spec/lib/gitlab/ci/config/yaml/context_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/context_spec.rb @@ -26,6 +26,18 @@ expect(context.variables).to eq([]) end end + + context 'with component data provided' do + let(:component_data) do + { name: 'my-component', sha: 'abc123' } + end + + subject(:context) { described_class.new(component: component_data) } + + it 'stores the component data' do + expect(context.component).to eq(component_data) + end + end end describe '#variables' do @@ -41,4 +53,16 @@ expect(context.variables).to eq(variables) end end + + describe '#component' do + let(:component_data) do + { name: 'test-component', sha: 'def456' } + end + + subject(:context) { described_class.new(component: component_data) } + + it 'returns the component data' do + expect(context.component).to eq(component_data) + end + end end diff --git a/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb b/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..39e9c1c645da268487f038e7a71216175b232560 --- /dev/null +++ b/spec/services/ci/create_pipeline_service/component_interpolation_spec.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_composition do + context 'for spec:component' do + let_it_be(:project) { create(:project, :small_repo) } + let_it_be(:user) { project.first_owner } + + let_it_be(:components_project) do + create(:project, :small_repo, creator: user, namespace: user.namespace) + end + + let_it_be(:catalog_resource) { create(:ci_catalog_resource, :published, project: components_project) } + + let_it_be(:component_name) { 'my-component' } + let_it_be(:component_version) { '0.1.1' } + let_it_be(:component_file_path) { "templates/#{component_name}/template.yml" } + + let_it_be(:component_yaml) do + <<~YAML + spec: + component: [name, sha] + inputs: + compiler: + default: gcc + optimization_level: + type: number + default: 2 + + --- + + test: + script: + - echo "Building with $[[ inputs.compiler ]] and optimization level $[[ inputs.optimization_level ]]" + - echo "Component $[[ component.name ]] / $[[ component.sha ]]" + YAML + end + + let_it_be(:component_sha) do + components_project.repository.create_file( + user, component_file_path, component_yaml, message: 'Add my first CI component', branch_name: 'master' + ) + end + + let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) } + + subject(:execute) { service.execute(:push, content: project_ci_yaml) } + + before_all do + components_project.repository.add_tag(user, component_version, component_sha) + + create(:release, :with_catalog_resource_version, + tag: component_version, author: user, project: components_project, sha: component_sha + ) + end + + context 'when the component file is included as include:component' do + let(:project_ci_yaml) do + <<~YAML + include: + - component: #{component_path} + YAML + end + + context 'when the component path is with a full version' do + let_it_be(:component_path) do + "#{Gitlab.config.gitlab.host}/#{components_project.full_path}/#{component_name}@#{component_version}" + end + + it 'creates a pipeline with correct jobs' do + response = execute + pipeline = response.payload + + expect(response).to be_success + expect(pipeline).to be_created_successfully + + expect(pipeline.builds.map(&:name)).to contain_exactly('test') + + test_job = pipeline.builds.find { |build| build.name == 'test' } + expect(test_job.options[:script]).to eq([ + 'echo "Building with gcc and optimization level 2"', + "echo \"Component #{component_name} / #{component_sha}\"" + ]) + end + + context 'when the FF ci_component_context_interpolation is disabled' do + before do + stub_feature_flags(ci_component_context_interpolation: false) + end + + it 'does not create a pipeline' do + response = execute + pipeline = response.payload + + expect(response).to be_error + expect(response.message).to include('unknown interpolation provided: `name` in `component.name`') + + expect(pipeline).not_to be_created_successfully + end + end + end + end + + context 'when the component file is included as include:project:file' do + let(:project_ci_yaml) do + <<~YAML + include: + - project: #{components_project.full_path} + file: #{component_file_path} + YAML + end + + it 'does not interpolate and returns errors' do + response = execute + pipeline = response.payload + + expect(response).not_to be_success + expect(pipeline).not_to be_created_successfully + + expect(response.message).to eq( + "`templates/my-component/template.yml`: unknown interpolation provided: `name` in `component.name`" + ) + end + end + end +end