diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index cc2db5803f61bb6c6e6e22f9380cf04e7208e787..d244fb1aaf09b4fad4e7212d523f4109235c26ac 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -250,13 +250,7 @@ def locking_enabled?
   end
 
   def group_name
-    # [\b\s:] -> whitespace or column
-    # (\[.*\])|(\d+[\s:\/\\]+\d+) -> variables/matrix or parallel-jobs numbers
-    # {1,3} -> number of times that matches the variables/matrix or parallel-jobs numbers
-    #          we limit this to 3 because of possible abuse
-    regex = %r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+))){1,3}\s*\z}
-
-    name.to_s.sub(regex, '').strip
+    Gitlab::Utils::Job.group_name(name)
   end
 
   def supports_canceling?
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 629dec943bdf1154ffbf846c5af2b8bb804058ca..eec9cee2b7801d51da65fd6881c1719095c571c0 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -81,6 +81,7 @@ Predefined variables become available at three different phases of pipeline exec
 | `CI_ENVIRONMENT_TIER`                           | Pipeline     | The [deployment tier of the environment](../environments/_index.md#deployment-tier-of-environments) for this job. |
 | `CI_GITLAB_FIPS_MODE`                           | Pre-pipeline | Only available if [FIPS mode](../../development/fips_gitlab.md) is enabled in the GitLab instance. `true` when available. |
 | `CI_HAS_OPEN_REQUIREMENTS`                      | Pipeline     | Only available if the pipeline's project has an open [requirement](../../user/project/requirements/_index.md). `true` when available. |
+| `CI_JOB_GROUP_NAME`                             | Pipeline     | The shared name of a group of jobs, when using either [`parallel`](../yaml/_index.md#parallel) or [manually grouped jobs](../jobs/_index.md#group-similar-jobs-together-in-pipeline-views). For example, if the job name is `rspec:test: [ruby, ubuntu]`, the `CI_JOB_GROUP_NAME` is `rspec:test`. It is the same as `CI_JOB_NAME` otherwise. Introduced in GitLab 17.10. |
 | `CI_JOB_ID`                                     | Job-only     | The internal ID of the job, unique across all jobs in the GitLab instance. |
 | `CI_JOB_IMAGE`                                  | Pipeline     | The name of the Docker image running the job. |
 | `CI_JOB_MANUAL`                                 | Pipeline     | Only available if the job was started manually. `true` when available. |
diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb
index 6b070224b01bfe28437b7200203461831f5918a6..0e356cd05117b17665677a56bccf3208a10c8221 100644
--- a/lib/gitlab/ci/variables/builder.rb
+++ b/lib/gitlab/ci/variables/builder.rb
@@ -198,6 +198,7 @@ def predefined_variables(job, environment)
           Gitlab::Ci::Variables::Collection.new.tap do |variables|
             variables.append(key: 'CI_JOB_NAME', value: job.name)
             variables.append(key: 'CI_JOB_NAME_SLUG', value: job_name_slug(job.name))
+            variables.append(key: 'CI_JOB_GROUP_NAME', value: Gitlab::Utils::Job.group_name(job.name))
             variables.append(key: 'CI_JOB_STAGE', value: job.stage_name)
             variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action?
             variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request
@@ -219,6 +220,7 @@ def predefined_variables_from_job_attr(job_attr, environment, trigger_request)
           Gitlab::Ci::Variables::Collection.new.tap do |variables|
             variables.append(key: 'CI_JOB_NAME', value: job_attr[:name])
             variables.append(key: 'CI_JOB_NAME_SLUG', value: job_name_slug(job_attr[:name]))
+            variables.append(key: 'CI_JOB_GROUP_NAME', value: Gitlab::Utils::Job.group_name(job_attr[:name]))
             variables.append(key: 'CI_JOB_STAGE', value: job_attr[:stage])
             variables.append(key: 'CI_JOB_MANUAL', value: 'true') if ::Ci::Processable::ACTIONABLE_WHEN.include?(job_attr[:when])
             variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if trigger_request
diff --git a/lib/gitlab/utils/job.rb b/lib/gitlab/utils/job.rb
new file mode 100644
index 0000000000000000000000000000000000000000..154b025b6493a7dfa34e785f22f98df599439be2
--- /dev/null
+++ b/lib/gitlab/utils/job.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module Utils
+    module Job
+      class << self
+        def group_name(job_name)
+          # [\b\s:] -> whitespace or column
+          # (\[.*\])|(\d+[\s:\/\\]+\d+) -> variables/matrix or parallel-jobs numbers
+          # {1,3} -> number of times that matches the variables/matrix or parallel-jobs numbers
+          #          we limit this to 3 because of possible abuse
+          regex = %r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+))){1,3}\s*\z}
+
+          job_name.to_s.sub(regex, '').strip
+        end
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
index 3023eae70d754d8c61585bad841b920fda7bf660..ec25a854ddfaeabc276b29278f8fe92f03d7151e 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -39,6 +39,7 @@
       is_expected.to include('CI_PIPELINE_IID'     => pipeline.iid.to_s)
       is_expected.to include('CI_PROJECT_PATH'     => project.full_path)
       is_expected.to include('CI_JOB_NAME'         => 'some-job')
+      is_expected.to include('CI_JOB_GROUP_NAME'   => 'some-job')
       is_expected.to include('YAML_KEY'            => 'yaml_value')
       is_expected.to include('CI_NODE_INDEX'       => '1')
       is_expected.to include('CI_NODE_TOTAL'       => '2')
@@ -79,6 +80,25 @@
       end
     end
 
+    context 'when job is an instance of parallel:matrix' do
+      let(:seed_attributes) do
+        {
+          name: 'some-job: [ruby, ubuntu]',
+          options: {
+            instance: 1,
+            parallel: { total: 2 }
+          }
+        }
+      end
+
+      it 'returns a collection of variables' do
+        is_expected.to include('CI_JOB_NAME' => 'some-job: [ruby, ubuntu]')
+        is_expected.to include('CI_JOB_GROUP_NAME' => 'some-job')
+        is_expected.to include('CI_NODE_INDEX' => '1')
+        is_expected.to include('CI_NODE_TOTAL' => '2')
+      end
+    end
+
     context 'when environment and kubernetes namespace include variables' do
       let(:seed_attributes) do
         {
diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb
index ecacdf69ad6b96bef61114090258bda7ef92b4c6..3fb4dd564045e4d89674024d0f927b4cdaecc85d 100644
--- a/spec/lib/gitlab/ci/variables/builder_spec.rb
+++ b/spec/lib/gitlab/ci/variables/builder_spec.rb
@@ -127,6 +127,8 @@
             value: 'rspec:test 1' },
           { key: 'CI_JOB_NAME_SLUG',
             value: 'rspec-test-1' },
+          { key: 'CI_JOB_GROUP_NAME',
+            value: 'rspec:test 1' },
           { key: 'CI_JOB_STAGE',
             value: job.stage_name },
           { key: 'CI_NODE_TOTAL',
@@ -244,6 +246,40 @@ def var(name, value)
       end
     end
 
+    context 'with instance of parallel job' do
+      let(:job) do
+        create(:ci_build,
+          name: 'rspec:test 2/3',
+          pipeline: pipeline,
+          user: user
+        )
+      end
+
+      subject { builder.scoped_variables(job, environment: environment_name, dependencies: dependencies) }
+
+      it 'returns CI_JOB_NAME and CI_JOB_GROUP_NAME' do
+        expect(subject.to_hash).to include('CI_JOB_NAME' => 'rspec:test 2/3')
+        expect(subject.to_hash).to include('CI_JOB_GROUP_NAME' => 'rspec:test')
+      end
+    end
+
+    context 'with instance of parallel:matrix job' do
+      let(:job) do
+        create(:ci_build,
+          name: 'rspec:test: [ruby, rust]',
+          pipeline: pipeline,
+          user: user
+        )
+      end
+
+      subject { builder.scoped_variables(job, environment: environment_name, dependencies: dependencies) }
+
+      it 'returns CI_JOB_NAME and CI_JOB_GROUP_NAME' do
+        expect(subject.to_hash).to include('CI_JOB_NAME' => 'rspec:test: [ruby, rust]')
+        expect(subject.to_hash).to include('CI_JOB_GROUP_NAME' => 'rspec:test')
+      end
+    end
+
     context 'with schedule variables' do
       let_it_be(:schedule) { create(:ci_pipeline_schedule, project: project) }
       let_it_be(:schedule_variable) { create(:ci_pipeline_schedule_variable, pipeline_schedule: schedule) }
@@ -364,6 +400,8 @@ def var(name, value)
             value: 'rspec:test 2' },
           { key: 'CI_JOB_NAME_SLUG',
             value: 'rspec-test-2' },
+          { key: 'CI_JOB_GROUP_NAME',
+            value: 'rspec:test 2' },
           { key: 'CI_JOB_STAGE',
             value: 'test' },
           { key: 'CI_NODE_TOTAL',
@@ -505,6 +543,36 @@ def var(name, value)
       end
     end
 
+    context 'with instance of parallel job' do
+      let(:job_attr) do
+        {
+          name: 'rspec:test 1/2',
+          stage: 'test',
+          **extra_attributes
+        }
+      end
+
+      it 'returns CI_JOB_NAME and CI_JOB_GROUP_NAME' do
+        expect(subject.to_hash).to include('CI_JOB_NAME' => 'rspec:test 1/2')
+        expect(subject.to_hash).to include('CI_JOB_GROUP_NAME' => 'rspec:test')
+      end
+    end
+
+    context 'with instance of parallel:matrix job' do
+      let(:job_attr) do
+        {
+          name: 'rspec:test: [ubuntu, ruby]',
+          stage: 'test',
+          **extra_attributes
+        }
+      end
+
+      it 'returns CI_JOB_NAME and CI_JOB_GROUP_NAME' do
+        expect(subject.to_hash).to include('CI_JOB_NAME' => 'rspec:test: [ubuntu, ruby]')
+        expect(subject.to_hash).to include('CI_JOB_GROUP_NAME' => 'rspec:test')
+      end
+    end
+
     context 'with schedule variables' do
       let_it_be(:schedule) { create(:ci_pipeline_schedule, project: project) }
       let_it_be(:schedule_variable) { create(:ci_pipeline_schedule_variable, pipeline_schedule: schedule) }
diff --git a/spec/lib/gitlab/utils/job_spec.rb b/spec/lib/gitlab/utils/job_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e594d7b3d6d2df2e9028dcb478de8f97950e3414
--- /dev/null
+++ b/spec/lib/gitlab/utils/job_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Utils::Job, feature_category: :continuous_integration do
+  describe '#group_name' do
+    using RSpec::Parameterized::TableSyntax
+    where(:name, :group_name) do
+      'rspec1'                                              | 'rspec1'
+      'rspec1 0 1'                                          | 'rspec1'
+      'rspec1 0/2'                                          | 'rspec1'
+      'rspec:windows'                                       | 'rspec:windows'
+      'rspec:windows 0'                                     | 'rspec:windows 0'
+      'rspec:windows 0 2/2'                                 | 'rspec:windows 0'
+      'rspec:windows 0 test'                                | 'rspec:windows 0 test'
+      'rspec:windows 0 test 2/2'                            | 'rspec:windows 0 test'
+      'rspec:windows 0 1 2/2'                               | 'rspec:windows'
+      'rspec:windows 0 1 [aws] 2/2'                         | 'rspec:windows'
+      'rspec:windows 0 1 name [aws] 2/2'                    | 'rspec:windows 0 1 name'
+      'rspec:windows 0 1 name'                              | 'rspec:windows 0 1 name'
+      'rspec:windows 0 1 name 1/2'                          | 'rspec:windows 0 1 name'
+      'rspec:windows 0/1'                                   | 'rspec:windows'
+      'rspec:windows 0/1 name'                              | 'rspec:windows 0/1 name'
+      'rspec:windows 0/1 name 1/2'                          | 'rspec:windows 0/1 name'
+      'rspec:windows 0:1'                                   | 'rspec:windows'
+      'rspec:windows 0:1 name'                              | 'rspec:windows 0:1 name'
+      'rspec:windows 10000 20000'                           | 'rspec:windows'
+      'rspec:windows 0 : / 1'                               | 'rspec:windows'
+      'rspec:windows 0 : / 1 name'                          | 'rspec:windows 0 : / 1 name'
+      'rspec [inception: [something, other thing], value]'  | 'rspec'
+      '0 1 name ruby'                                       | '0 1 name ruby'
+      '0 :/ 1 name ruby'                                    | '0 :/ 1 name ruby'
+      'rspec: [aws]'                                        | 'rspec'
+      'rspec: [aws] 0/1'                                    | 'rspec'
+      'rspec: [aws, max memory]'                            | 'rspec'
+      'rspec:linux: [aws, max memory, data]'                | 'rspec:linux'
+      'rspec: [inception: [something, other thing], value]' | 'rspec'
+      'rspec:windows 0/1: [name, other]'                    | 'rspec:windows'
+      'rspec:windows: [name, other] 0/1'                    | 'rspec:windows'
+      'rspec:windows: [name, 0/1] 0/1'                      | 'rspec:windows'
+      'rspec:windows: [0/1, name]'                          | 'rspec:windows'
+      'rspec:windows: [, ]'                                 | 'rspec:windows'
+      'rspec:windows: [name]'                               | 'rspec:windows'
+      'rspec-windows: [name, other, context]'               | 'rspec-windows'
+      'rspec_windows: [name,  ]'                            | 'rspec_windows'
+      'rspec windows & linux: [ruby, 3.5]'                  | 'rspec windows & linux'
+    end
+
+    with_them do
+      it "#{params[:name]} puts in #{params[:group_name]}" do
+        expect(described_class.group_name(name)).to eq(group_name)
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 13bd92993c89ecc535f02f4a12c0aac5e2f8d96d..ff19254e4cd0429926e76c93e389053b1aaf0c45 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2555,6 +2555,7 @@
           { key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true },
           { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false },
           { key: 'CI_JOB_NAME_SLUG', value: 'test', public: true, masked: false },
+          { key: 'CI_JOB_GROUP_NAME', value: 'test', public: true, masked: false },
           { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false },
           { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
           { key: 'CI', value: 'true', public: true, masked: false },
@@ -3266,6 +3267,30 @@ def insert_expected_predefined_variables(variables, after:)
             { key: 'CI_NODE_TOTAL', value: total.to_s, public: true, masked: false }
           )
         end
+
+        it 'includes CI_JOB_GROUP_NAME' do
+          is_expected.to include(
+            { key: 'CI_JOB_GROUP_NAME', value: 'test', public: true, masked: false }
+          )
+        end
+      end
+
+      context 'when parallel is a matrix' do
+        let(:config) do
+          {
+            matrix: [
+              {
+                STACK: %w[ruby python],
+                DB: %w[postgresql mysql]
+              }
+            ],
+            total: 4
+          }
+        end
+
+        it_behaves_like 'parallelized jobs config' do
+          let(:total) { 4 }
+        end
       end
 
       context 'when parallel is a number' do