diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index af6cb74ca0d6334d0c00ad2e1115505fb7e7272e..8363de4247524d587c29d8c6e81a404b5cd3009a 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -5,6 +5,7 @@ class ProjectType < BaseObject
graphql_name 'Project'
include ::Namespaces::DeletableHelper
+ include Gitlab::Graphql::Authorize::AuthorizeResource
connection_type_class Types::CountableConnectionType
@@ -34,6 +35,10 @@ def self.authorization_scopes
authorize: :create_pipeline,
experiment: { milestone: '15.3' },
description: 'CI/CD config variable.' do
+ argument :fail_on_cache_miss, GraphQL::Types::Boolean,
+ required: false,
+ default_value: false,
+ description: 'Whether to throw an error if cache is not ready.'
argument :ref, GraphQL::Types::String,
required: true,
description: 'Ref.'
@@ -1027,9 +1032,13 @@ def ci_pipeline_creation_inputs(ref:)
response.payload[:inputs].all_inputs
end
- def ci_config_variables(ref:)
+ def ci_config_variables(ref:, fail_on_cache_miss: false)
result = ::Ci::ListConfigVariablesService.new(object, context[:current_user]).execute(ref)
+ if result.nil? && fail_on_cache_miss
+ raise_resource_not_available_error! "Failed to retrieve CI/CD variables from cache."
+ end
+
return if result.nil?
result.map do |var_key, var_config|
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 4abf36ac582f61dbce75eeb6b2fd82b15019c780..5c385f7488ccedbc1de83aca5fba38258690cac6 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -39867,6 +39867,7 @@ Returns [`[CiConfigVariable!]`](#ciconfigvariable).
| Name | Type | Description |
| ---- | ---- | ----------- |
+| `failOnCacheMiss` | [`Boolean`](#boolean) | Whether to throw an error if cache is not ready. |
| `ref` | [`String!`](#string) | Ref. |
##### `Project.ciPipelineCreationInputs`
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 2c1e783cdbc12ce611082471a33efaf8549cda30..51451b053ae0dce1675c8d52025fd67c65c7f52c 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -1785,4 +1785,108 @@
end
end
end
+
+ describe 'ci_config_variables field' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, developer_of: project) }
+ let(:ref) { project.default_branch }
+ let(:fail_on_cache_miss) { nil }
+
+ let(:query) do
+ fail_on_cache_miss_arg = fail_on_cache_miss.nil? ? '' : ", failOnCacheMiss: #{fail_on_cache_miss}"
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ ciConfigVariables(ref: "#{ref}"#{fail_on_cache_miss_arg}) {
+ key
+ value
+ description
+ }
+ }
+ }
+ )
+ end
+
+ subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
+
+ def mock_config_variables_service(return_value)
+ allow_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ allow(service).to receive(:execute).and_return(return_value)
+ end
+ end
+
+ context 'when service returns nil and fail_on_cache_miss is enabled' do
+ let(:fail_on_cache_miss) { true }
+
+ before do
+ mock_config_variables_service(nil)
+ end
+
+ it 'returns GraphQL error with expected message' do
+ expect(subject['errors']).to be_present
+ expect(subject['errors'].first['message']).to eq('Failed to retrieve CI/CD variables from cache.')
+ expect(subject['data']['project']['ciConfigVariables']).to be_nil
+ end
+
+ it 'includes error path information' do
+ expect(subject['errors'].first['path']).to eq(%w[project ciConfigVariables])
+ end
+ end
+
+ context 'when variables are successfully fetched' do
+ let_it_be(:ci_config_variables) do
+ {
+ KEY1: { value: 'val 1', description: 'description 1' },
+ KEY2: { value: 'val 2', description: '' },
+ KEY3: { value: 'val 3' }
+ }
+ end
+
+ before do
+ mock_config_variables_service(ci_config_variables)
+ end
+
+ it 'returns variables list' do
+ variables = subject.dig('data', 'project', 'ciConfigVariables')
+
+ expect(variables).to contain_exactly(
+ { 'key' => 'KEY1', 'value' => 'val 1', 'description' => 'description 1' },
+ { 'key' => 'KEY2', 'value' => 'val 2', 'description' => '' },
+ { 'key' => 'KEY3', 'value' => 'val 3', 'description' => nil }
+ )
+ end
+
+ it 'does not return any errors' do
+ expect(subject['errors']).to be_nil
+ end
+ end
+
+ context 'with fail_on_cache_miss argument' do
+ context 'when service is called with fail_on_cache_miss parameter' do
+ before do
+ mock_config_variables_service({})
+ end
+
+ shared_examples 'calls service with expected fail_on_cache_miss value' do |expected_value|
+ it "calls service with fail_on_cache_miss: #{expected_value}" do
+ expect_next_instance_of(::Ci::ListConfigVariablesService) do |service|
+ expect(service).to receive(:execute).with(ref)
+ end
+
+ subject
+ end
+ end
+
+ context 'with default value (not specified)' do
+ it_behaves_like 'calls service with expected fail_on_cache_miss value', false
+ end
+
+ context 'with fail_on_cache_miss: true' do
+ let(:fail_on_cache_miss) { true }
+
+ it_behaves_like 'calls service with expected fail_on_cache_miss value', true
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/ci/config_variables_spec.rb b/spec/requests/api/graphql/ci/config_variables_spec.rb
index 6a9dcfee9667ada0b38398c157414e7e7862c1d5..34bb4ba6261ffd770626f65a12461e92b3a658b3 100644
--- a/spec/requests/api/graphql/ci/config_variables_spec.rb
+++ b/spec/requests/api/graphql/ci/config_variables_spec.rb
@@ -15,12 +15,13 @@
let(:service) { Ci::ListConfigVariablesService.new(project, user) }
let(:ref) { project.default_branch }
+ let(:fail_on_cache_miss) { false }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
- ciConfigVariables(ref: "#{ref}") {
+ ciConfigVariables(ref: "#{ref}", failOnCacheMiss: #{fail_on_cache_miss}) {
key
value
valueOptions
@@ -40,19 +41,8 @@
end
context 'when the cache is not empty' do
- before do
- synchronous_reactive_cache(service)
- end
-
- it 'returns the CI variables for the config' do
- expect(service)
- .to receive(:execute)
- .with(ref)
- .and_call_original
-
- post_graphql(query, current_user: user)
-
- expect(graphql_data.dig('project', 'ciConfigVariables')).to contain_exactly(
+ let(:expected_ci_variables) do
+ [
{
'key' => 'KEY_VALUE_VAR',
'value' => 'value x',
@@ -71,7 +61,32 @@
'valueOptions' => ['env var value', 'env var value2'],
'description' => 'env var description'
}
- )
+ ]
+ end
+
+ shared_examples 'returns CI variables' do
+ it 'returns the CI variables for the config' do
+ expect(service)
+ .to receive(:execute)
+ .with(ref)
+ .and_call_original
+
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data.dig('project', 'ciConfigVariables')).to match_array(expected_ci_variables)
+ end
+ end
+
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ it_behaves_like 'returns CI variables'
+
+ context 'when failOnCacheMiss is true' do
+ let(:fail_on_cache_miss) { true }
+
+ it_behaves_like 'returns CI variables'
end
end
@@ -81,6 +96,20 @@
expect(graphql_data.dig('project', 'ciConfigVariables')).to be_nil
end
+
+ context 'when failOnCacheMiss is true' do
+ let(:fail_on_cache_miss) { true }
+
+ it 'returns an error' do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_errors).to include(
+ a_hash_including(
+ 'message' => 'Failed to retrieve CI/CD variables from cache.'
+ )
+ )
+ end
+ end
end
end
diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index 604abbe2edfd8efa57839ce37dd8ceba4b80e4dc..b364772b58dffbac4ed83f52935ad6e61a17de96 100644
--- a/spec/support/helpers/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
@@ -22,6 +22,8 @@ def synchronous_reactive_cache(subject)
allow(subject).to receive(:with_reactive_cache) do |*args, &block|
block.call(subject.calculate_reactive_cache(*args))
end
+
+ allow(subject).to receive(:within_reactive_cache_lifetime?).and_return(true)
end
def read_reactive_cache(subject, *qualifiers)