From 6e08e1ac74d68703ec6ee2391fa77653f9e9454d Mon Sep 17 00:00:00 2001
From: Avielle Wolfe <awolfe@gitlab.com>
Date: Wed, 27 Jul 2022 03:42:20 +0000
Subject: [PATCH] Refactor GraphQL CI variables to use interfaces

There is a growing number of differences between the GraphQL
functionality for different variable types. Moving to an interface
pattern now will spare us from having to write a lot of specific logic
for checking the variable type
---
 .../graphql_shared/possible_types.json        |   6 +
 app/graphql/types/ci/group_variable_type.rb   |  16 ++
 .../types/ci/instance_variable_type.rb        |  24 ++
 app/graphql/types/ci/job_type.rb              |   2 +-
 app/graphql/types/ci/manual_variable_type.rb  |  24 ++
 app/graphql/types/ci/project_variable_type.rb |  16 ++
 ...variable_type.rb => variable_interface.rb} |  14 +-
 app/graphql/types/group_type.rb               |   2 +-
 app/graphql/types/project_type.rb             |   2 +-
 app/graphql/types/query_type.rb               |   2 +-
 doc/api/graphql/reference/index.md            | 227 ++++++++++++++----
 .../types/ci/group_variable_type_spec.rb      |   9 +
 .../types/ci/instance_variable_type_spec.rb   |   7 +
 .../types/ci/manual_variable_type_spec.rb     |   7 +
 .../types/ci/project_variable_type_spec.rb    |   9 +
 ...ype_spec.rb => variable_interface_spec.rb} |   6 +-
 16 files changed, 313 insertions(+), 60 deletions(-)
 create mode 100644 app/graphql/types/ci/group_variable_type.rb
 create mode 100644 app/graphql/types/ci/instance_variable_type.rb
 create mode 100644 app/graphql/types/ci/manual_variable_type.rb
 create mode 100644 app/graphql/types/ci/project_variable_type.rb
 rename app/graphql/types/ci/{variable_type.rb => variable_interface.rb} (70%)
 create mode 100644 spec/graphql/types/ci/group_variable_type_spec.rb
 create mode 100644 spec/graphql/types/ci/instance_variable_type_spec.rb
 create mode 100644 spec/graphql/types/ci/manual_variable_type_spec.rb
 create mode 100644 spec/graphql/types/ci/project_variable_type_spec.rb
 rename spec/graphql/types/ci/{variable_type_spec.rb => variable_interface_spec.rb} (73%)

diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 6f821935f4467..40a8da95ffa04 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -3,6 +3,12 @@
     "AlertManagementHttpIntegration",
     "AlertManagementPrometheusIntegration"
   ],
+  "CiVariable": [
+    "CiGroupVariable",
+    "CiInstanceVariable",
+    "CiManualVariable",
+    "CiProjectVariable"
+  ],
   "CurrentUserTodos": [
     "BoardEpic",
     "Design",
diff --git a/app/graphql/types/ci/group_variable_type.rb b/app/graphql/types/ci/group_variable_type.rb
new file mode 100644
index 0000000000000..efa6fb72a4ffb
--- /dev/null
+++ b/app/graphql/types/ci/group_variable_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop: disable Graphql/AuthorizeTypes
+    class GroupVariableType < BaseObject
+      graphql_name 'CiGroupVariable'
+      description 'CI/CD variables for a group.'
+
+      implements(VariableInterface)
+
+      field :environment_scope, GraphQL::Types::String, null: true,
+        description: 'Scope defining the environments that can use the variable.'
+    end
+  end
+end
diff --git a/app/graphql/types/ci/instance_variable_type.rb b/app/graphql/types/ci/instance_variable_type.rb
new file mode 100644
index 0000000000000..4a72eb949d8a5
--- /dev/null
+++ b/app/graphql/types/ci/instance_variable_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop: disable Graphql/AuthorizeTypes
+    class InstanceVariableType < BaseObject
+      graphql_name 'CiInstanceVariable'
+      description 'CI/CD variables for a GitLab instance.'
+
+      implements(VariableInterface)
+
+      field :environment_scope, GraphQL::Types::String, null: true,
+            deprecated: {
+              reason: 'No longer used, only available for GroupVariableType and ProjectVariableType',
+              milestone: '15.3'
+            },
+            description: 'Scope defining the environments that can use the variable.'
+
+      def environment_scope
+        nil
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb
index 42b55f47f9240..d0a56395154b7 100644
--- a/app/graphql/types/ci/job_type.rb
+++ b/app/graphql/types/ci/job_type.rb
@@ -70,7 +70,7 @@ class JobType < BaseObject
             description: 'Downstream pipeline for a bridge.'
       field :manual_job, GraphQL::Types::Boolean, null: true,
             description: 'Whether the job has a manual action.'
-      field :manual_variables, VariableType.connection_type, null: true,
+      field :manual_variables, ManualVariableType.connection_type, null: true,
             description: 'Variables added to a manual job when the job is triggered.'
       field :playable, GraphQL::Types::Boolean, null: false, method: :playable?,
             description: 'Indicates the job can be played.'
diff --git a/app/graphql/types/ci/manual_variable_type.rb b/app/graphql/types/ci/manual_variable_type.rb
new file mode 100644
index 0000000000000..09df1b7906e09
--- /dev/null
+++ b/app/graphql/types/ci/manual_variable_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop: disable Graphql/AuthorizeTypes
+    class ManualVariableType < BaseObject
+      graphql_name 'CiManualVariable'
+      description 'CI/CD variables given to a manual job.'
+
+      implements(VariableInterface)
+
+      field :environment_scope, GraphQL::Types::String, null: true,
+            deprecated: {
+              reason: 'No longer used, only available for GroupVariableType and ProjectVariableType',
+              milestone: '15.3'
+            },
+            description: 'Scope defining the environments that can use the variable.'
+
+      def environment_scope
+        nil
+      end
+    end
+  end
+end
diff --git a/app/graphql/types/ci/project_variable_type.rb b/app/graphql/types/ci/project_variable_type.rb
new file mode 100644
index 0000000000000..c3d083672aff3
--- /dev/null
+++ b/app/graphql/types/ci/project_variable_type.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Types
+  module Ci
+    # rubocop: disable Graphql/AuthorizeTypes
+    class ProjectVariableType < BaseObject
+      graphql_name 'CiProjectVariable'
+      description 'CI/CD variables for a project.'
+
+      implements(VariableInterface)
+
+      field :environment_scope, GraphQL::Types::String, null: true,
+        description: 'Scope defining the environments that can use the variable.'
+    end
+  end
+end
diff --git a/app/graphql/types/ci/variable_type.rb b/app/graphql/types/ci/variable_interface.rb
similarity index 70%
rename from app/graphql/types/ci/variable_type.rb
rename to app/graphql/types/ci/variable_interface.rb
index 63f89b6d207ee..b204e3ccde54d 100644
--- a/app/graphql/types/ci/variable_type.rb
+++ b/app/graphql/types/ci/variable_interface.rb
@@ -2,8 +2,9 @@
 
 module Types
   module Ci
-    # rubocop: disable Graphql/AuthorizeTypes
-    class VariableType < BaseObject
+    module VariableInterface
+      include Types::BaseInterface
+
       graphql_name 'CiVariable'
 
       field :id, GraphQL::Types::ID, null: false,
@@ -26,15 +27,6 @@ class VariableType < BaseObject
 
       field :raw, GraphQL::Types::Boolean, null: true,
         description: 'Indicates whether the variable is raw.'
-
-      field :environment_scope, GraphQL::Types::String, null: true,
-        description: 'Scope defining the environments in which the variable can be used.'
-
-      def environment_scope
-        if object.respond_to?(:environment_scope)
-          object.environment_scope
-        end
-      end
     end
   end
 end
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 52e9f80806667..85bbc66a286db 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -195,7 +195,7 @@ class GroupType < NamespaceType
           resolver: Resolvers::GroupsResolver
 
     field :ci_variables,
-          Types::Ci::VariableType.connection_type,
+          Types::Ci::GroupVariableType.connection_type,
           null: true,
           description: "List of the group's CI/CD variables.",
           authorize: :admin_group,
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index f13063d6f7022..4851becf4311d 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -221,7 +221,7 @@ class ProjectType < BaseObject
           resolver: Resolvers::Ci::ProjectPipelineCountsResolver
 
     field :ci_variables,
-          Types::Ci::VariableType.connection_type,
+          Types::Ci::ProjectVariableType.connection_type,
           null: true,
           description: "List of the project's CI/CD variables.",
           authorize: :admin_build,
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 6036a34cdc4b9..93e051776df31 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -124,7 +124,7 @@ class QueryType < ::Types::BaseObject
           description: "Find runners visible to the current user."
 
     field :ci_variables,
-          Types::Ci::VariableType.connection_type,
+          Types::Ci::InstanceVariableType.connection_type,
           null: true,
           description: "List of the instance's CI/CD variables."
 
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 23dc964687aed..bb0a021e16930 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -91,7 +91,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
 
 List of the instance's CI/CD variables.
 
-Returns [`CiVariableConnection`](#civariableconnection).
+Returns [`CiInstanceVariableConnection`](#ciinstancevariableconnection).
 
 This field returns a [connection](#connections). It accepts the
 four standard [pagination arguments](#connection-pagination-arguments):
@@ -6170,6 +6170,52 @@ The edge type for [`CiGroup`](#cigroup).
 | <a id="cigroupedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="cigroupedgenode"></a>`node` | [`CiGroup`](#cigroup) | The item at the end of the edge. |
 
+#### `CiGroupVariableConnection`
+
+The connection type for [`CiGroupVariable`](#cigroupvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cigroupvariableconnectionedges"></a>`edges` | [`[CiGroupVariableEdge]`](#cigroupvariableedge) | A list of edges. |
+| <a id="cigroupvariableconnectionnodes"></a>`nodes` | [`[CiGroupVariable]`](#cigroupvariable) | A list of nodes. |
+| <a id="cigroupvariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiGroupVariableEdge`
+
+The edge type for [`CiGroupVariable`](#cigroupvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cigroupvariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cigroupvariableedgenode"></a>`node` | [`CiGroupVariable`](#cigroupvariable) | The item at the end of the edge. |
+
+#### `CiInstanceVariableConnection`
+
+The connection type for [`CiInstanceVariable`](#ciinstancevariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciinstancevariableconnectionedges"></a>`edges` | [`[CiInstanceVariableEdge]`](#ciinstancevariableedge) | A list of edges. |
+| <a id="ciinstancevariableconnectionnodes"></a>`nodes` | [`[CiInstanceVariable]`](#ciinstancevariable) | A list of nodes. |
+| <a id="ciinstancevariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiInstanceVariableEdge`
+
+The edge type for [`CiInstanceVariable`](#ciinstancevariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciinstancevariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="ciinstancevariableedgenode"></a>`node` | [`CiInstanceVariable`](#ciinstancevariable) | The item at the end of the edge. |
+
 #### `CiJobArtifactConnection`
 
 The connection type for [`CiJobArtifact`](#cijobartifact).
@@ -6230,6 +6276,29 @@ The edge type for [`CiJob`](#cijob).
 | <a id="cijobedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="cijobedgenode"></a>`node` | [`CiJob`](#cijob) | The item at the end of the edge. |
 
+#### `CiManualVariableConnection`
+
+The connection type for [`CiManualVariable`](#cimanualvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cimanualvariableconnectionedges"></a>`edges` | [`[CiManualVariableEdge]`](#cimanualvariableedge) | A list of edges. |
+| <a id="cimanualvariableconnectionnodes"></a>`nodes` | [`[CiManualVariable]`](#cimanualvariable) | A list of nodes. |
+| <a id="cimanualvariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiManualVariableEdge`
+
+The edge type for [`CiManualVariable`](#cimanualvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cimanualvariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="cimanualvariableedgenode"></a>`node` | [`CiManualVariable`](#cimanualvariable) | The item at the end of the edge. |
+
 #### `CiMinutesNamespaceMonthlyUsageConnection`
 
 The connection type for [`CiMinutesNamespaceMonthlyUsage`](#ciminutesnamespacemonthlyusage).
@@ -6276,6 +6345,29 @@ The edge type for [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage
 | <a id="ciminutesprojectmonthlyusageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="ciminutesprojectmonthlyusageedgenode"></a>`node` | [`CiMinutesProjectMonthlyUsage`](#ciminutesprojectmonthlyusage) | The item at the end of the edge. |
 
+#### `CiProjectVariableConnection`
+
+The connection type for [`CiProjectVariable`](#ciprojectvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciprojectvariableconnectionedges"></a>`edges` | [`[CiProjectVariableEdge]`](#ciprojectvariableedge) | A list of edges. |
+| <a id="ciprojectvariableconnectionnodes"></a>`nodes` | [`[CiProjectVariable]`](#ciprojectvariable) | A list of nodes. |
+| <a id="ciprojectvariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `CiProjectVariableEdge`
+
+The edge type for [`CiProjectVariable`](#ciprojectvariable).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciprojectvariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="ciprojectvariableedgenode"></a>`node` | [`CiProjectVariable`](#ciprojectvariable) | The item at the end of the edge. |
+
 #### `CiRunnerConnection`
 
 The connection type for [`CiRunner`](#cirunner).
@@ -6348,29 +6440,6 @@ The edge type for [`CiStage`](#cistage).
 | <a id="cistageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
 | <a id="cistageedgenode"></a>`node` | [`CiStage`](#cistage) | The item at the end of the edge. |
 
-#### `CiVariableConnection`
-
-The connection type for [`CiVariable`](#civariable).
-
-##### Fields
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="civariableconnectionedges"></a>`edges` | [`[CiVariableEdge]`](#civariableedge) | A list of edges. |
-| <a id="civariableconnectionnodes"></a>`nodes` | [`[CiVariable]`](#civariable) | A list of nodes. |
-| <a id="civariableconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
-
-#### `CiVariableEdge`
-
-The edge type for [`CiVariable`](#civariable).
-
-##### Fields
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="civariableedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
-| <a id="civariableedgenode"></a>`node` | [`CiVariable`](#civariable) | The item at the end of the edge. |
-
 #### `ClusterAgentActivityEventConnection`
 
 The connection type for [`ClusterAgentActivityEvent`](#clusteragentactivityevent).
@@ -9939,6 +10008,40 @@ Represents the total number of issues and their weights for a particular day.
 | <a id="cigroupname"></a>`name` | [`String`](#string) | Name of the job group. |
 | <a id="cigroupsize"></a>`size` | [`Int`](#int) | Size of the group. |
 
+### `CiGroupVariable`
+
+CI/CD variables for a group.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cigroupvariableenvironmentscope"></a>`environmentScope` | [`String`](#string) | Scope defining the environments that can use the variable. |
+| <a id="cigroupvariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="cigroupvariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="cigroupvariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="cigroupvariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="cigroupvariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="cigroupvariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="cigroupvariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
+### `CiInstanceVariable`
+
+CI/CD variables for a GitLab instance.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciinstancevariableenvironmentscope"></a>`environmentScope` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.3. No longer used, only available for GroupVariableType and ProjectVariableType. |
+| <a id="ciinstancevariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="ciinstancevariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="ciinstancevariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="ciinstancevariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="ciinstancevariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="ciinstancevariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="ciinstancevariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
 ### `CiJob`
 
 #### Fields
@@ -9960,7 +10063,7 @@ Represents the total number of issues and their weights for a particular day.
 | <a id="cijobid"></a>`id` | [`JobID`](#jobid) | ID of the job. |
 | <a id="cijobkind"></a>`kind` | [`CiJobKind!`](#cijobkind) | Indicates the type of job. |
 | <a id="cijobmanualjob"></a>`manualJob` | [`Boolean`](#boolean) | Whether the job has a manual action. |
-| <a id="cijobmanualvariables"></a>`manualVariables` | [`CiVariableConnection`](#civariableconnection) | Variables added to a manual job when the job is triggered. (see [Connections](#connections)) |
+| <a id="cijobmanualvariables"></a>`manualVariables` | [`CiManualVariableConnection`](#cimanualvariableconnection) | Variables added to a manual job when the job is triggered. (see [Connections](#connections)) |
 | <a id="cijobname"></a>`name` | [`String`](#string) | Name of the job. |
 | <a id="cijobneeds"></a>`needs` | [`CiBuildNeedConnection`](#cibuildneedconnection) | References to builds that must complete before the jobs run. (see [Connections](#connections)) |
 | <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
@@ -10001,6 +10104,23 @@ Represents the total number of issues and their weights for a particular day.
 | ---- | ---- | ----------- |
 | <a id="cijobtokenscopetypeprojects"></a>`projects` | [`ProjectConnection!`](#projectconnection) | Allow list of projects that can be accessed by CI Job tokens created by this project. (see [Connections](#connections)) |
 
+### `CiManualVariable`
+
+CI/CD variables given to a manual job.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="cimanualvariableenvironmentscope"></a>`environmentScope` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.3. No longer used, only available for GroupVariableType and ProjectVariableType. |
+| <a id="cimanualvariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="cimanualvariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="cimanualvariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="cimanualvariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="cimanualvariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="cimanualvariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="cimanualvariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
 ### `CiMinutesNamespaceMonthlyUsage`
 
 #### Fields
@@ -10023,6 +10143,23 @@ Represents the total number of issues and their weights for a particular day.
 | <a id="ciminutesprojectmonthlyusagename"></a>`name` | [`String`](#string) | Name of the project. |
 | <a id="ciminutesprojectmonthlyusagesharedrunnersduration"></a>`sharedRunnersDuration` | [`Int`](#int) | Total duration (in seconds) of shared runners use by the project for the month. |
 
+### `CiProjectVariable`
+
+CI/CD variables for a project.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="ciprojectvariableenvironmentscope"></a>`environmentScope` | [`String`](#string) | Scope defining the environments that can use the variable. |
+| <a id="ciprojectvariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="ciprojectvariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="ciprojectvariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="ciprojectvariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="ciprojectvariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="ciprojectvariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="ciprojectvariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
 ### `CiRunner`
 
 #### Fields
@@ -10136,21 +10273,6 @@ GitLab CI/CD configuration template.
 | <a id="citemplatecontent"></a>`content` | [`String!`](#string) | Contents of the CI template. |
 | <a id="citemplatename"></a>`name` | [`String!`](#string) | Name of the CI template. |
 
-### `CiVariable`
-
-#### Fields
-
-| Name | Type | Description |
-| ---- | ---- | ----------- |
-| <a id="civariableenvironmentscope"></a>`environmentScope` | [`String`](#string) | Scope defining the environments in which the variable can be used. |
-| <a id="civariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
-| <a id="civariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
-| <a id="civariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
-| <a id="civariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
-| <a id="civariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
-| <a id="civariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
-| <a id="civariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
-
 ### `ClusterAgent`
 
 #### Fields
@@ -11922,7 +12044,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
 | <a id="groupallowstalerunnerpruning"></a>`allowStaleRunnerPruning` | [`Boolean!`](#boolean) | Indicates whether to regularly prune stale group runners. Defaults to false. |
 | <a id="groupautodevopsenabled"></a>`autoDevopsEnabled` | [`Boolean`](#boolean) | Indicates whether Auto DevOps is enabled for all projects within this group. |
 | <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
-| <a id="groupcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the group's CI/CD variables. (see [Connections](#connections)) |
+| <a id="groupcivariables"></a>`ciVariables` | [`CiGroupVariableConnection`](#cigroupvariableconnection) | List of the group's CI/CD variables. (see [Connections](#connections)) |
 | <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
 | <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean!`](#boolean) | Includes at least one project where the repository size exceeds the limit. |
 | <a id="groupcrossprojectpipelineavailable"></a>`crossProjectPipelineAvailable` | [`Boolean!`](#boolean) | Indicates if the cross_project_pipeline feature is available for the namespace. |
@@ -15236,7 +15358,7 @@ Represents vulnerability finding of a security report on the pipeline.
 | <a id="projectcicdsettings"></a>`ciCdSettings` | [`ProjectCiCdSetting`](#projectcicdsetting) | CI/CD settings for the project. |
 | <a id="projectciconfigpathordefault"></a>`ciConfigPathOrDefault` | [`String!`](#string) | Path of the CI configuration file. |
 | <a id="projectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI Job Tokens scope of access. |
-| <a id="projectcivariables"></a>`ciVariables` | [`CiVariableConnection`](#civariableconnection) | List of the project's CI/CD variables. (see [Connections](#connections)) |
+| <a id="projectcivariables"></a>`ciVariables` | [`CiProjectVariableConnection`](#ciprojectvariableconnection) | List of the project's CI/CD variables. (see [Connections](#connections)) |
 | <a id="projectcodecoveragesummary"></a>`codeCoverageSummary` | [`CodeCoverageSummary`](#codecoveragesummary) | Code coverage summary associated with the project. |
 | <a id="projectcomplianceframeworks"></a>`complianceFrameworks` | [`ComplianceFrameworkConnection`](#complianceframeworkconnection) | Compliance frameworks associated with the project. (see [Connections](#connections)) |
 | <a id="projectcontainerexpirationpolicy"></a>`containerExpirationPolicy` | [`ContainerExpirationPolicy`](#containerexpirationpolicy) | Container expiration policy of the project. |
@@ -21279,6 +21401,27 @@ Implementations:
 | <a id="alertmanagementintegrationtype"></a>`type` | [`AlertManagementIntegrationType!`](#alertmanagementintegrationtype) | Type of integration. |
 | <a id="alertmanagementintegrationurl"></a>`url` | [`String`](#string) | Endpoint which accepts alert notifications. |
 
+#### `CiVariable`
+
+Implementations:
+
+- [`CiGroupVariable`](#cigroupvariable)
+- [`CiInstanceVariable`](#ciinstancevariable)
+- [`CiManualVariable`](#cimanualvariable)
+- [`CiProjectVariable`](#ciprojectvariable)
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="civariableid"></a>`id` | [`ID!`](#id) | ID of the variable. |
+| <a id="civariablekey"></a>`key` | [`String`](#string) | Name of the variable. |
+| <a id="civariablemasked"></a>`masked` | [`Boolean`](#boolean) | Indicates whether the variable is masked. |
+| <a id="civariableprotected"></a>`protected` | [`Boolean`](#boolean) | Indicates whether the variable is protected. |
+| <a id="civariableraw"></a>`raw` | [`Boolean`](#boolean) | Indicates whether the variable is raw. |
+| <a id="civariablevalue"></a>`value` | [`String`](#string) | Value of the variable. |
+| <a id="civariablevariabletype"></a>`variableType` | [`CiVariableType`](#civariabletype) | Type of the variable. |
+
 #### `CurrentUserTodos`
 
 Implementations:
diff --git a/spec/graphql/types/ci/group_variable_type_spec.rb b/spec/graphql/types/ci/group_variable_type_spec.rb
new file mode 100644
index 0000000000000..3680f6611982e
--- /dev/null
+++ b/spec/graphql/types/ci/group_variable_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiGroupVariable'] do
+  specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+
+  specify { expect(described_class).to have_graphql_fields(:environment_scope).at_least }
+end
diff --git a/spec/graphql/types/ci/instance_variable_type_spec.rb b/spec/graphql/types/ci/instance_variable_type_spec.rb
new file mode 100644
index 0000000000000..e534f69ea2dbc
--- /dev/null
+++ b/spec/graphql/types/ci/instance_variable_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiInstanceVariable'] do
+  specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+end
diff --git a/spec/graphql/types/ci/manual_variable_type_spec.rb b/spec/graphql/types/ci/manual_variable_type_spec.rb
new file mode 100644
index 0000000000000..2884c818a525d
--- /dev/null
+++ b/spec/graphql/types/ci/manual_variable_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiManualVariable'] do
+  specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+end
diff --git a/spec/graphql/types/ci/project_variable_type_spec.rb b/spec/graphql/types/ci/project_variable_type_spec.rb
new file mode 100644
index 0000000000000..5883a397d9fad
--- /dev/null
+++ b/spec/graphql/types/ci/project_variable_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['CiProjectVariable'] do
+  specify { expect(described_class.interfaces).to contain_exactly(Types::Ci::VariableInterface) }
+
+  it { expect(described_class).to have_graphql_fields(:environment_scope).at_least }
+end
diff --git a/spec/graphql/types/ci/variable_type_spec.rb b/spec/graphql/types/ci/variable_interface_spec.rb
similarity index 73%
rename from spec/graphql/types/ci/variable_type_spec.rb
rename to spec/graphql/types/ci/variable_interface_spec.rb
index a81e6adbab6cf..9bdce46eafe2b 100644
--- a/spec/graphql/types/ci/variable_type_spec.rb
+++ b/spec/graphql/types/ci/variable_interface_spec.rb
@@ -3,9 +3,9 @@
 require 'spec_helper'
 
 RSpec.describe GitlabSchema.types['CiVariable'] do
-  it 'contains attributes related to CI variables' do
+  specify do
     expect(described_class).to have_graphql_fields(
-      :id, :key, :value, :variable_type, :protected, :masked, :raw, :environment_scope
-    )
+      :id, :key, :value, :variable_type, :protected, :masked, :raw
+    ).at_least
   end
 end
-- 
GitLab