From 15cd4dd9268b48f0a807b37003d47d93b1c9a908 Mon Sep 17 00:00:00 2001 From: Laura Montemayor Date: Tue, 25 Nov 2025 11:51:02 +0100 Subject: [PATCH 1/3] Add comprehensive integration spec for pipeline inputs Adds test coverage for ciPipelineCreationInputs GraphQL query with: - All input types (string, number, boolean) - String variations (description, options, regex) - Required vs optional inputs - Rules-based inputs - Verifies all fields are properly returned as hashes --- .../ci/pipeline_creation/input_spec.rb | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/spec/requests/api/graphql/project/ci/pipeline_creation/input_spec.rb b/spec/requests/api/graphql/project/ci/pipeline_creation/input_spec.rb index f3d8523b76e1c0..460dbb6e2bc62a 100644 --- a/spec/requests/api/graphql/project/ci/pipeline_creation/input_spec.rb +++ b/spec/requests/api/graphql/project/ci/pipeline_creation/input_spec.rb @@ -338,6 +338,173 @@ end end end + + context 'when inputs have mixed types and rules' do + let_it_be(:config_yaml_comprehensive) do + <<~YAML + spec: + inputs: + string_with_description: + type: string + description: 'A string input with description' + default: 'test-value' + string_with_options: + type: string + options: ['option1', 'option2', 'option3'] + default: 'option1' + string_with_regex: + type: string + regex: '^[a-z]+$' + default: 'abc' + number_input: + type: number + default: 42 + boolean_input: + type: boolean + default: true + required_input: + type: string + rules_based_input: + type: string + rules: + - if: '$[[ inputs.string_with_options ]] == "option1"' + options: ['a', 'b'] + default: 'a' + - if: '$[[ inputs.string_with_options ]] == "option2"' + options: ['c', 'd'] + default: 'c' + --- + job: + script: echo "All input types" + YAML + end + + let(:comprehensive_query) do + <<~GQL + query { + project(fullPath: "#{project.full_path}") { + ciPipelineCreationInputs(ref: "#{ref}") { + name + type + description + required + default + options + regex + rules { + if + options + default + } + } + } + } + GQL + end + + let(:ref) { 'feature-comprehensive' } + + before_all do + project.repository.create_file( + project.creator, + '.gitlab-ci.yml', + config_yaml_comprehensive, + message: 'Add comprehensive inputs CI', + branch_name: 'feature-comprehensive') + end + + context 'when feature flag is enabled' do + before do + stub_feature_flags(ci_dynamic_pipeline_inputs: project) + end + + it 'returns all input types correctly formatted as hashes' do + post_graphql(comprehensive_query, current_user: user) + + inputs = graphql_data['project']['ciPipelineCreationInputs'] + + expect(inputs).to contain_exactly( + a_hash_including( + 'name' => 'string_with_description', + 'type' => 'STRING', + 'description' => 'A string input with description', + 'default' => 'test-value', + 'required' => false, + 'options' => nil, + 'regex' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'string_with_options', + 'type' => 'STRING', + 'default' => 'option1', + 'options' => %w[option1 option2 option3], + 'required' => false, + 'regex' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'string_with_regex', + 'type' => 'STRING', + 'default' => 'abc', + 'regex' => '^[a-z]+$', + 'required' => false, + 'options' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'number_input', + 'type' => 'NUMBER', + 'default' => 42, + 'required' => false, + 'options' => nil, + 'regex' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'boolean_input', + 'type' => 'BOOLEAN', + 'default' => true, + 'required' => false, + 'options' => nil, + 'regex' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'required_input', + 'type' => 'STRING', + 'required' => true, + 'default' => nil, + 'options' => nil, + 'regex' => nil, + 'rules' => nil + ), + a_hash_including( + 'name' => 'rules_based_input', + 'type' => 'STRING', + 'required' => false, + 'rules' => contain_exactly( + a_hash_including( + 'if' => '$[[ inputs.string_with_options ]] == "option1"', + 'options' => %w[a b], + 'default' => 'a' + ), + a_hash_including( + 'if' => '$[[ inputs.string_with_options ]] == "option2"', + 'options' => %w[c d], + 'default' => 'c' + ) + ) + ) + ) + end + + it 'successfully queries all input fields' do + expect { post_graphql(comprehensive_query, current_user: user) }.not_to raise_error + expect(graphql_errors).to be_nil + end + end + end end context 'when current user cannot access the project' do -- GitLab From 9654475c2493dd39feb799cd03afb1511c00bda6 Mon Sep 17 00:00:00 2001 From: Laura Montemayor Date: Tue, 25 Nov 2025 13:10:20 +0100 Subject: [PATCH 2/3] Fix validation for rules-based inputs without explicit defaults When a rule matches but has options without a default value, auto-use the first option as the default. --- lib/ci/inputs/base_input.rb | 17 ++++++++-- spec/lib/ci/inputs/base_input_spec.rb | 48 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/lib/ci/inputs/base_input.rb b/lib/ci/inputs/base_input.rb index 65bf073e321517..7a5f1ddb5ef8aa 100644 --- a/lib/ci/inputs/base_input.rb +++ b/lib/ci/inputs/base_input.rb @@ -65,13 +65,20 @@ def type # For rules-based inputs, check if any rule provides a default value (this functionality is under a feature # flag). def required? - !default_value_present? + options_available? && !default_value_present? end def default_value_present? spec.key?(:default) || has_default_through_rules? end + def options_available?(current_inputs = {}) + return true unless rules + + opts = resolved_options(current_inputs) + opts.nil? || opts.any? + end + def default spec[:default] end @@ -107,7 +114,13 @@ def resolved_options(current_inputs = {}) def resolved_default(current_inputs = {}) return default unless rules - rules_evaluator(current_inputs).resolved_default || default + resolved_def = rules_evaluator(current_inputs).resolved_default || default + return resolved_def if resolved_def.present? + + resolved_opts = resolved_options(current_inputs) + return if resolved_opts.blank? + + resolved_opts.first end def rules_evaluator(current_inputs) diff --git a/spec/lib/ci/inputs/base_input_spec.rb b/spec/lib/ci/inputs/base_input_spec.rb index b0f1872fa373bd..6556a345a3f61b 100644 --- a/spec/lib/ci/inputs/base_input_spec.rb +++ b/spec/lib/ci/inputs/base_input_spec.rb @@ -156,4 +156,52 @@ def self.type_name end end end + + describe 'rules-based inputs without explicit defaults' do + let(:input) { described_class.new(name: :test_input, spec: spec) } + + context 'when fallback rule has options but no default' do + let(:spec) do + { + type: 'string', + rules: [ + { if: '$[[ inputs.env ]] == "prod"', options: %w[a b], default: 'a' }, + { options: %w[x y] } + ] + } + end + + it 'uses first option as default' do + allow(input).to receive(:rules_evaluator).and_return( + instance_double(Ci::Inputs::RulesEvaluator, resolved_options: %w[x y], resolved_default: nil) + ) + + expect(input.send(:resolved_default, {})).to eq('x') + end + end + + context 'when fallback rule has empty options array and no default' do + let(:spec) do + { + type: 'string', + rules: [ + { if: '$[[ inputs.deployment_type ]] == "canary"', options: %w[10 25 50], default: '25' }, + { options: [] } + ] + } + end + + it 'has no options available' do + allow(input).to receive(:resolved_options).with({}).and_return([]) + + expect(input.options_available?({})).to be false + end + + it 'is not required' do + allow(input).to receive(:resolved_options).with({}).and_return([]) + + expect(input).not_to be_required + end + end + end end -- GitLab From 99c4601a27fe8c64b722dd728668479498443a1d Mon Sep 17 00:00:00 2001 From: Laura Montemayor Date: Wed, 26 Nov 2025 12:29:04 +0100 Subject: [PATCH 3/3] Require explicit defaults for rules based inputs Changes rules-based inputs to require explicit defaults when they have options, matching the existing behavior for spec:inputs in root config. --- lib/ci/inputs/base_input.rb | 19 +++----------- spec/lib/ci/inputs/base_input_spec.rb | 37 +++------------------------ 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/lib/ci/inputs/base_input.rb b/lib/ci/inputs/base_input.rb index 7a5f1ddb5ef8aa..bb4cdc2fb990d1 100644 --- a/lib/ci/inputs/base_input.rb +++ b/lib/ci/inputs/base_input.rb @@ -65,20 +65,13 @@ def type # For rules-based inputs, check if any rule provides a default value (this functionality is under a feature # flag). def required? - options_available? && !default_value_present? + !default_value_present? end def default_value_present? spec.key?(:default) || has_default_through_rules? end - def options_available?(current_inputs = {}) - return true unless rules - - opts = resolved_options(current_inputs) - opts.nil? || opts.any? - end - def default spec[:default] end @@ -114,13 +107,7 @@ def resolved_options(current_inputs = {}) def resolved_default(current_inputs = {}) return default unless rules - resolved_def = rules_evaluator(current_inputs).resolved_default || default - return resolved_def if resolved_def.present? - - resolved_opts = resolved_options(current_inputs) - return if resolved_opts.blank? - - resolved_opts.first + rules_evaluator(current_inputs).resolved_default || default end def rules_evaluator(current_inputs) @@ -135,7 +122,7 @@ def has_default_through_rules? spec[:rules].any? do |rule| next false unless rule.is_a?(Hash) - rule.key?(:default) || !rule.key?(:if) + rule.key?(:default) end end diff --git a/spec/lib/ci/inputs/base_input_spec.rb b/spec/lib/ci/inputs/base_input_spec.rb index 6556a345a3f61b..f1ba9df8c86b6e 100644 --- a/spec/lib/ci/inputs/base_input_spec.rb +++ b/spec/lib/ci/inputs/base_input_spec.rb @@ -107,9 +107,9 @@ def self.type_name end end - context 'when there is a fallback rule' do + context 'when there is a fallback rule with a default' do it 'is not required' do - spec = { rules: [{ options: %w[a b] }] } + spec = { rules: [{ options: %w[a b], default: 'a' }] } input = described_class.new(name: :test, spec: spec) expect(input).not_to be_required end @@ -165,42 +165,13 @@ def self.type_name { type: 'string', rules: [ - { if: '$[[ inputs.env ]] == "prod"', options: %w[a b], default: 'a' }, { options: %w[x y] } ] } end - it 'uses first option as default' do - allow(input).to receive(:rules_evaluator).and_return( - instance_double(Ci::Inputs::RulesEvaluator, resolved_options: %w[x y], resolved_default: nil) - ) - - expect(input.send(:resolved_default, {})).to eq('x') - end - end - - context 'when fallback rule has empty options array and no default' do - let(:spec) do - { - type: 'string', - rules: [ - { if: '$[[ inputs.deployment_type ]] == "canary"', options: %w[10 25 50], default: '25' }, - { options: [] } - ] - } - end - - it 'has no options available' do - allow(input).to receive(:resolved_options).with({}).and_return([]) - - expect(input.options_available?({})).to be false - end - - it 'is not required' do - allow(input).to receive(:resolved_options).with({}).and_return([]) - - expect(input).not_to be_required + it 'is required' do + expect(input).to be_required end end end -- GitLab