diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 5052d84378fc599554bb956215a08c69a535ccc9..77fb72382984d1b39ed076c8b2f1ffe146fd5326 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -266,6 +266,12 @@ def forward_pipeline_variables? end end + def expand_file_refs? + strong_memoize(:expand_file_refs) do + !Feature.enabled?(:ci_prevent_file_var_expansion_downstream_pipeline, project) + end + end + private def cross_project_params diff --git a/config/feature_flags/development/ci_prevent_file_var_expansion_downstream_pipeline.yml b/config/feature_flags/development/ci_prevent_file_var_expansion_downstream_pipeline.yml new file mode 100644 index 0000000000000000000000000000000000000000..f0bcacbe2bd44f4657a8d757a53d4acfb3bccade --- /dev/null +++ b/config/feature_flags/development/ci_prevent_file_var_expansion_downstream_pipeline.yml @@ -0,0 +1,8 @@ +--- +name: ci_prevent_file_var_expansion_downstream_pipeline +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124320 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414583 +milestone: '16.3' +type: development +group: group::pipeline security +default_enabled: false diff --git a/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb b/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb index 6690e9f1c1f2fb6b4a3a4cbc000362b94c31b101..bb7a6e7ab59b2fd5a489dbd7bef2396eaa2fbaf2 100644 --- a/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb +++ b/lib/gitlab/ci/variables/downstream/expandable_variable_generator.rb @@ -6,9 +6,40 @@ module Variables module Downstream class ExpandableVariableGenerator < Base def for(item) - expanded_value = ::ExpandVariables.expand(item.value, context.all_bridge_variables) + expanded_var = expanded_var_for(item) + file_vars = file_var_dependencies_for(item) - [{ key: item.key, value: expanded_value }] + [expanded_var].concat(file_vars) + end + + private + + def expanded_var_for(item) + { + key: item.key, + value: ::ExpandVariables.expand( + item.value, + context.all_bridge_variables, + expand_file_refs: context.expand_file_refs + ) + } + end + + def file_var_dependencies_for(item) + return [] if context.expand_file_refs + return [] unless item.depends_on + + item.depends_on.filter_map do |dependency| + dependency_variable = context.all_bridge_variables[dependency] + + if dependency_variable&.file? + { + key: dependency_variable.key, + value: dependency_variable.value, + variable_type: :file + } + end + end end end end diff --git a/lib/gitlab/ci/variables/downstream/generator.rb b/lib/gitlab/ci/variables/downstream/generator.rb index 93c995cc918fe18b32f41301df546ea07e3ad8cc..350d29958cfc12ab14e8bdc0e2c5098262f29741 100644 --- a/lib/gitlab/ci/variables/downstream/generator.rb +++ b/lib/gitlab/ci/variables/downstream/generator.rb @@ -7,12 +7,12 @@ module Downstream class Generator include Gitlab::Utils::StrongMemoize - Context = Struct.new(:all_bridge_variables, keyword_init: true) + Context = Struct.new(:all_bridge_variables, :expand_file_refs, keyword_init: true) def initialize(bridge) @bridge = bridge - context = Context.new(all_bridge_variables: bridge.variables) + context = Context.new(all_bridge_variables: bridge.variables, expand_file_refs: bridge.expand_file_refs?) @raw_variable_generator = RawVariableGenerator.new(context) @expandable_variable_generator = ExpandableVariableGenerator.new(context) diff --git a/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb b/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..95cc6d95c90defe743e33dd5743f80ef0a2c44fc --- /dev/null +++ b/qa/qa/specs/features/api/4_verify/file_variable_downstream_pipeline_spec.rb @@ -0,0 +1,241 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify', :runner, product_group: :pipeline_security, feature_flag: { + name: 'ci_prevent_file_var_expansion_downstream_pipeline', + scope: :project + } do + describe 'Pipeline with file variables and downstream pipelines' do + let(:random_string) { Faker::Alphanumeric.alphanumeric(number: 8) } + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" } + + let!(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'upstream-project-with-file-variables' + end + end + + let!(:downstream_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'downstream-project' + end + end + + let!(:project_runner) do + Resource::ProjectRunner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let!(:downstream_project_runner) do + Resource::ProjectRunner.fabricate! do |runner| + runner.project = downstream_project + runner.name = "#{executor}-downstream" + runner.tags = [executor] + end + end + + let(:add_ci_file) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml and child.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + default: + tags: [#{executor}] + + variables: + EXTRA_ARGS: "-f $TEST_PROJECT_FILE" + DOCKER_REMOTE_ARGS: --tlscacert="$DOCKER_CA_CERT" + EXTRACTED_CRT_FILE: ${DOCKER_CA_CERT}.crt + MY_FILE_VAR: $TEST_PROJECT_FILE + + trigger_child: + trigger: + strategy: depend + include: + - local: child.yml + + trigger_downstream_project: + trigger: + strategy: depend + project: #{downstream_project.path_with_namespace} + + YAML + }, + { + file_path: 'child.yml', + content: <<~YAML + default: + tags: [#{executor}] + + child_job_echo: + script: + - echo "run something $EXTRA_ARGS" + - echo "docker run $DOCKER_REMOTE_ARGS" + - echo "run --output=$EXTRACTED_CRT_FILE" + - echo "Will read private key from $MY_FILE_VAR" + + child_job_cat: + script: + - cat "$MY_FILE_VAR" + - cat "$DOCKER_CA_CERT" + YAML + } + ] + ) + end + end + + let(:add_downstream_project_ci_file) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = downstream_project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + default: + tags: [#{executor}] + + downstream_job_echo: + script: + - echo "run something $EXTRA_ARGS" + - echo "docker run $DOCKER_REMOTE_ARGS" + - echo "run --output=$EXTRACTED_CRT_FILE" + - echo "Will read private key from $MY_FILE_VAR" + + downstream_job_cat: + script: + - cat "$MY_FILE_VAR" + - cat "$DOCKER_CA_CERT" + YAML + } + ] + ) + end + end + + let(:add_project_file_variables) do + { + 'TEST_PROJECT_FILE' => "hello, this is test\n", + 'DOCKER_CA_CERT' => "This is secret\n" + }.each do |file_name, content| + add_file_variable_to_project(project, file_name, content) + end + end + + let(:upstream_pipeline) do + Resource::Pipeline.fabricate_via_api! do |pipeline| + pipeline.project = project + end + end + + def child_pipeline + Resource::Pipeline.fabricate_via_api! do |pipeline| + pipeline.project = project + pipeline.id = upstream_pipeline.downstream_pipeline_id(bridge_name: 'trigger_child') + end + end + + def downstream_project_pipeline + Resource::Pipeline.fabricate_via_api! do |pipeline| + pipeline.project = downstream_project + pipeline.id = upstream_pipeline.downstream_pipeline_id(bridge_name: 'trigger_downstream_project') + end + end + + around do |example| + Runtime::Feature.enable(:ci_prevent_file_var_expansion_downstream_pipeline, project: project) + example.run + Runtime::Feature.disable(:ci_prevent_file_var_expansion_downstream_pipeline, project: project) + end + + before do + add_project_file_variables + add_downstream_project_ci_file + add_ci_file + upstream_pipeline + wait_for_pipelines + end + + after do + project_runner.remove_via_api! + downstream_project_runner.remove_via_api! + end + + it( + 'creates variable with file path in downstream pipelines and can read file variable content', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/416337' + ) do + child_echo_job = Resource::Job.fabricate_via_api! do |job| + job.project = project + job.id = project.job_by_name('child_job_echo')[:id] + end + + child_cat_job = Resource::Job.fabricate_via_api! do |job| + job.project = project + job.id = project.job_by_name('child_job_cat')[:id] + end + + downstream_project_echo_job = Resource::Job.fabricate_via_api! do |job| + job.project = downstream_project + job.id = downstream_project.job_by_name('downstream_job_echo')[:id] + end + + downstream_project_cat_job = Resource::Job.fabricate_via_api! do |job| + job.project = downstream_project + job.id = downstream_project.job_by_name('downstream_job_cat')[:id] + end + + aggregate_failures do + trace = child_echo_job.trace + expect(trace).to include('run something -f', "#{project.name}.tmp/TEST_PROJECT_FILE") + expect(trace).to include('docker run --tlscacert=', "#{project.name}.tmp/DOCKER_CA_CERT") + expect(trace).to include('run --output=', "#{project.name}.tmp/DOCKER_CA_CERT.crt") + expect(trace).to include('Will read private key from', "#{project.name}.tmp/TEST_PROJECT_FILE") + + trace = child_cat_job.trace + expect(trace).to have_content('hello, this is test') + expect(trace).to have_content('This is secret') + + trace = downstream_project_echo_job.trace + expect(trace).to include('run something -f', "#{downstream_project.name}.tmp/TEST_PROJECT_FILE") + expect(trace).to include('docker run --tlscacert=', "#{downstream_project.name}.tmp/DOCKER_CA_CERT") + expect(trace).to include('run --output=', "#{downstream_project.name}.tmp/DOCKER_CA_CERT.crt") + expect(trace).to include('Will read private key from', "#{downstream_project.name}.tmp/TEST_PROJECT_FILE") + + trace = downstream_project_cat_job.trace + expect(trace).to have_content('hello, this is test') + expect(trace).to have_content('This is secret') + end + end + + private + + def add_file_variable_to_project(project, key, value) + Resource::CiVariable.fabricate_via_api! do |ci_variable| + ci_variable.project = project + ci_variable.key = key + ci_variable.value = value + ci_variable.variable_type = 'file' + end + end + + def wait_for_pipelines + Support::Waiter.wait_until(max_duration: 300, sleep_interval: 10) do + upstream_pipeline.reload! + upstream_pipeline.status == 'success' && + child_pipeline.status == 'success' && + downstream_project_pipeline.status == 'success' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb index 5b33527e06ce03d65ef46adce4474a2a60487c96..95d0f089f6d992c35a94c59929aed81f96859c9e 100644 --- a/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/expandable_variable_generator_spec.rb @@ -7,13 +7,19 @@ Gitlab::Ci::Variables::Collection.fabricate( [ { key: 'REF1', value: 'ref 1' }, - { key: 'REF2', value: 'ref 2' } + { key: 'REF2', value: 'ref 2' }, + { key: 'NESTED_REF1', value: 'nested $REF1' } ] ) end + let(:expand_file_refs) { false } + let(:context) do - Gitlab::Ci::Variables::Downstream::Generator::Context.new(all_bridge_variables: all_bridge_variables) + Gitlab::Ci::Variables::Downstream::Generator::Context.new( + all_bridge_variables: all_bridge_variables, + expand_file_refs: expand_file_refs + ) end subject(:generator) { described_class.new(context) } @@ -34,5 +40,54 @@ expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 ' }]) end end + + context 'when given a variable with nested interpolation' do + it 'returns an array containing the expanded variables' do + var = Gitlab::Ci::Variables::Collection::Item.fabricate({ key: 'VAR1', value: '$REF1 $REF2 $NESTED_REF1' }) + + expect(generator.for(var)).to match_array([{ key: 'VAR1', value: 'ref 1 ref 2 nested $REF1' }]) + end + end + + context 'when given a variable with expansion on a file variable' do + let(:all_bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF2', value: 'ref 2', file: true }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', file: true } + ] + ) + end + + context 'when expand_file_refs is false' do + let(:expand_file_refs) { false } + + it 'returns an array containing the unexpanded variable and the file variable dependency' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = [ + { key: 'VAR1', value: 'ref 1 $FILE_REF2 $NESTED_REF3' }, + { key: 'FILE_REF2', value: 'ref 2', variable_type: :file }, + { key: 'NESTED_REF3', value: 'ref 3 $REF1 and $FILE_REF2', variable_type: :file } + ] + + expect(generator.for(var)).to match_array(expected) + end + end + + context 'when expand_file_refs is true' do + let(:expand_file_refs) { true } + + it 'returns an array containing the expanded variables' do + var = { key: 'VAR1', value: '$REF1 $FILE_REF2 $FILE_REF3 $NESTED_REF3' } + var = Gitlab::Ci::Variables::Collection::Item.fabricate(var) + + expected = { key: 'VAR1', value: 'ref 1 ref 2 ref 3 $REF1 and $FILE_REF2' } + expect(generator.for(var)).to contain_exactly(expected) + end + end + end end end diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb index 61e8b9a8c4ad4dd8af294c2ea82cb23e62067c7c..cd68b0cdf2bb71c85ee316d89d4576bbbf3530cf 100644 --- a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb @@ -45,6 +45,7 @@ variables: bridge_variables, forward_yaml_variables?: true, forward_pipeline_variables?: true, + expand_file_refs?: false, yaml_variables: yaml_variables, pipeline_variables: pipeline_variables, pipeline_schedule_variables: pipeline_schedule_variables @@ -81,5 +82,61 @@ expect(generator.calculate).to be_empty end + + context 'with file variable interpolation' do + let(:bridge_variables) do + Gitlab::Ci::Variables::Collection.fabricate( + [ + { key: 'REF1', value: 'ref 1' }, + { key: 'FILE_REF3', value: 'ref 3', file: true } + ] + ) + end + + let(:yaml_variables) do + [{ key: 'INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_variables) do + [{ key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + let(:pipeline_schedule_variables) do + [{ key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + + context 'when expand_file_refs is true' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(true) + end + + it 'expands file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + + context 'when expand_file_refs is false' do + before do + allow(bridge).to receive(:expand_file_refs?).and_return(false) + end + + it 'does not expand file variables and adds file variables' do + expected = [ + { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'FILE_REF3', value: 'ref 3', variable_type: :file } + ] + + expect(generator.calculate).to contain_exactly(*expected) + end + end + end end end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index d93250af1777ec6e55f0c7d3bffd4f357ab5f02c..b284cacf3545246f370b983242fe56314bb163f1 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ci::Bridge, feature_category: :continuous_integration do - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :in_group) } let_it_be(:target_project) { create(:project, name: 'project', namespace: create(:namespace, name: 'my')) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -196,11 +196,18 @@ end describe '#downstream_variables' do + # A new pipeline needs to be created in each test. + # The pipeline #variables_builder is memoized. The builder internally also memoizes variables. + # Having pipeline in a let_it_be might lead to flaky tests + # because a test might expect new variables but the variables builder does not + # return the new variables due to memoized results from previous tests. + let(:pipeline) { create(:ci_pipeline, project: project) } + subject(:downstream_variables) { bridge.downstream_variables } it 'returns variables that are going to be passed downstream' do expect(bridge.downstream_variables) - .to include(key: 'BRIDGE', value: 'cross') + .to contain_exactly(key: 'BRIDGE', value: 'cross') end context 'when using variables interpolation' do @@ -241,14 +248,49 @@ end end + context 'when using variables interpolation on file variables' do + let(:yaml_variables) do + [ + { + key: 'EXPANDED_FILE', + value: '$TEST_FILE_VAR' + } + ] + end + + before do + bridge.yaml_variables = yaml_variables + create(:ci_variable, :file, project: bridge.pipeline.project, key: 'TEST_FILE_VAR', value: 'test-file-value') + end + + it 'does not expand file variable and forwards the file variable' do + expected_vars = [ + { key: 'EXPANDED_FILE', value: '$TEST_FILE_VAR' }, + { key: 'TEST_FILE_VAR', value: 'test-file-value', variable_type: :file } + ] + + expect(bridge.downstream_variables).to contain_exactly(*expected_vars) + end + + context 'and feature flag is disabled' do + before do + stub_feature_flags(ci_prevent_file_var_expansion_downstream_pipeline: false) + end + + it 'expands the file variable' do + expect(bridge.downstream_variables).to contain_exactly({ key: 'EXPANDED_FILE', value: 'test-file-value' }) + end + end + end + context 'when recursive interpolation has been used' do before do - bridge.yaml_variables << { key: 'EXPANDED', value: '$EXPANDED', public: true } + bridge.yaml_variables = [{ key: 'EXPANDED', value: '$EXPANDED', public: true }] end it 'does not expand variable recursively' do expect(bridge.downstream_variables) - .to include(key: 'EXPANDED', value: '$EXPANDED') + .to contain_exactly(key: 'EXPANDED', value: '$EXPANDED') end end @@ -279,26 +321,82 @@ } end + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: 'PVAR1', value: 'PVAL1') + end + it 'returns variables according to the forward value' do expect(bridge.downstream_variables.map { |v| v[:key] }).to contain_exactly(*variables) end end context 'when sending a variable via both yaml and pipeline' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:options) do { trigger: { project: 'my/project', forward: { pipeline_variables: true } } } end before do - create(:ci_pipeline_variable, pipeline: pipeline, key: 'BRIDGE', value: 'new value') + bridge.yaml_variables = [{ key: 'SHARED_KEY', value: 'old_value' }] + create(:ci_pipeline_variable, pipeline: pipeline, key: 'SHARED_KEY', value: 'new value') end it 'uses the pipeline variable' do - expect(bridge.downstream_variables).to contain_exactly( - { key: 'BRIDGE', value: 'new value' } - ) + expect(bridge.downstream_variables).to contain_exactly({ key: 'SHARED_KEY', value: 'new value' }) + end + end + + context 'when sending a file variable from pipeline variable' do + let(:options) do + { trigger: { project: 'my/project', forward: { pipeline_variables: true } } } + end + + before do + bridge.yaml_variables = [{ key: 'FILE_VAR', value: 'old_value' }] + create(:ci_pipeline_variable, :file, pipeline: pipeline, key: 'FILE_VAR', value: 'new value') + end + + # The current behaviour forwards the file variable as an environment variable. + # TODO: decide whether to forward as a file var in https://gitlab.com/gitlab-org/gitlab/-/issues/416334 + it 'forwards the pipeline file variable' do + expect(bridge.downstream_variables).to contain_exactly({ key: 'FILE_VAR', value: 'new value' }) + end + end + + context 'when a pipeline variable interpolates a scoped file variable' do + let(:options) do + { trigger: { project: 'my/project', forward: { pipeline_variables: true } } } + end + + before do + bridge.yaml_variables = [{ key: 'YAML_VAR', value: '$PROJECT_FILE_VAR' }] + + create(:ci_variable, :file, project: pipeline.project, key: 'PROJECT_FILE_VAR', value: 'project file') + create(:ci_pipeline_variable, pipeline: pipeline, key: 'FILE_VAR', value: '$PROJECT_FILE_VAR') + end + + it 'does not expand the scoped file variable and forwards the file variable' do + expected_vars = [ + { key: 'FILE_VAR', value: '$PROJECT_FILE_VAR' }, + { key: 'YAML_VAR', value: '$PROJECT_FILE_VAR' }, + { key: 'PROJECT_FILE_VAR', value: 'project file', variable_type: :file } + ] + + expect(bridge.downstream_variables).to contain_exactly(*expected_vars) + end + + context 'and feature flag is disabled' do + before do + stub_feature_flags(ci_prevent_file_var_expansion_downstream_pipeline: false) + end + + it 'expands the file variable' do + expected_vars = [ + { key: 'FILE_VAR', value: 'project file' }, + { key: 'YAML_VAR', value: 'project file' } + ] + + expect(bridge.downstream_variables).to contain_exactly(*expected_vars) + end end end @@ -315,10 +413,66 @@ end it 'adds the schedule variable' do - expect(bridge.downstream_variables).to contain_exactly( + expected_vars = [ { key: 'BRIDGE', value: 'cross' }, { key: 'schedule_var_key', value: 'schedule var value' } - ) + ] + + expect(bridge.downstream_variables).to contain_exactly(*expected_vars) + end + end + end + + context 'when sending a file variable from pipeline schedule' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) } + let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } + + let(:options) do + { trigger: { project: 'my/project', forward: { pipeline_variables: true } } } + end + + before do + bridge.yaml_variables = [] + pipeline_schedule.variables.create!(key: 'schedule_var_key', value: 'schedule var value', variable_type: :file) + end + + # The current behaviour forwards the file variable as an environment variable. + # TODO: decide whether to forward as a file var in https://gitlab.com/gitlab-org/gitlab/-/issues/416334 + it 'forwards the schedule file variable' do + expect(bridge.downstream_variables).to contain_exactly({ key: 'schedule_var_key', value: 'schedule var value' }) + end + end + + context 'when a pipeline schedule variable interpolates a scoped file variable' do + let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) } + let(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } + + let(:options) do + { trigger: { project: 'my/project', forward: { pipeline_variables: true } } } + end + + before do + bridge.yaml_variables = [] + create(:ci_variable, :file, project: pipeline.project, key: 'PROJECT_FILE_VAR', value: 'project file') + pipeline_schedule.variables.create!(key: 'schedule_var_key', value: '$PROJECT_FILE_VAR') + end + + it 'does not expand the scoped file variable and forwards the file variable' do + expected_vars = [ + { key: 'schedule_var_key', value: '$PROJECT_FILE_VAR' }, + { key: 'PROJECT_FILE_VAR', value: 'project file', variable_type: :file } + ] + + expect(bridge.downstream_variables).to contain_exactly(*expected_vars) + end + + context 'and feature flag is disabled' do + before do + stub_feature_flags(ci_prevent_file_var_expansion_downstream_pipeline: false) + end + + it 'expands the file variable' do + expect(bridge.downstream_variables).to contain_exactly({ key: 'schedule_var_key', value: 'project file' }) end end end