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