Decouple MR pipelines from RUBY_VERSION_NEXT before re-bumping next Ruby to 3.4

Problem

Part of the Ruby 3.4 epic ("Allow pipelines to run Ruby 3.4").

This happened for real on 2026-08-26. !244463 (merged) bumped RUBY_VERSION_NEXT from 3.3.12 to 3.4.10 (plus the matching OMNIBUS_GITLAB_CACHE_EDITION), merged at 07:39 UTC, and immediately switched every merge request to Ruby 3.4. That is the later epic row, "Make MRs build with Ruby 3.4 by default" — not an intended outcome of a next-version bump. It was reverted by !252027 (merged).

Root cause

RUBY_VERSION_NEXT is overloaded. It is supposed to mean "the version the ruby-next branch and the next-Ruby schedule exercise", but seven merge-request rules in .gitlab-ci.yml's workflow:rules also resolve to *next-ruby-variables:

Line Rule
95 Community contribution label
109 $CI_MERGE_REQUEST_IID && $GITLAB_USER_LOGIN =~ /project_\d+_bot/
123 AI principles distiller service account
129 *if-merge-request-security-canonical-sync
134 *if-merge-request-labels-run-with-rails-next
148 database label
154 $CI_MERGE_REQUEST_IID (the generic catch-all — this is the one that matters)

So the default/next split we built for the 3.2 → 3.3 transition is structurally collapsed: any bump of RUBY_VERSION_NEXT is a global MR flip, and pipeline:run-in-ruby3_3 acts as an opt-out rather than 3.4 being opt-in. The split was invisible while RUBY_VERSION_NEXT == RUBY_VERSION_DEFAULT == 3.3.12, which is why this was not caught in review.

The same collapse means we currently get no 3.4 signal at all from schedules: the ruby-next branch and its 2-hourly maintenance schedule are alive and syncing from master, but with both variables equal they just run 3.3 a second time.

Goal

Make bumping RUBY_VERSION_NEXT a safe, boring operation: it should change what the ruby-next branch and the next-Ruby schedule test, and nothing else. Ruby 3.4 on an MR should be opt-in via a label until we deliberately decide to flip the default.

Required before re-bumping RUBY_VERSION_NEXT to 3.4

1. Create the pipeline:run-with-ruby-next label

Done. The plan here was pipeline:run-in-ruby3_4, chosen so it would match the /pipeline:run-in-ruby\d+(_\d)*/ already in .if-ruby-branch. !252108 (merged) shipped pipeline:run-with-ruby-next instead, mirroring pipeline:run-with-rails-next, and widened that regex to match the new name. The label exists in gitlab-org/gitlab and gitlab-org/security/gitlab. Steps 2, 4, and 5 below still say pipeline:run-in-ruby3_4; read those as the shipped name. The stale pipeline:run-in-ruby3_2 is still around and can be deleted.

2. Add the opt-in rule to .gitlab-ci.yml

Placed after the existing pipeline:run-in-ruby3_3 || stable-branch rule at line 89, so MRs targeting a stable branch cannot be dragged onto 3.4:

- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby3_4/'
  variables:
    <<: [*next-ruby-variables, *default-merge-request-variables]
    PIPELINE_NAME: 'Ruby $RUBY_VERSION MR'

3. Flip the seven MR rules back to *default-ruby-variables

The seven lines in the table above. This is the bulk of the diff and the part that actually decouples the two variables.

The *if-rails-next-schedule rule (line 141) should arguably move to default too — that branch tracks Rails, not Ruby — but it is a scheduled rule, so it is a judgement call rather than a requirement.

Leave *next-ruby-variables on the two scheduled rules: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $BUILD_WITH_NEXT_RUBY_VERSION == "true" and $CI_COMMIT_BRANCH == "ruby-next". Only the second one produces coverage: no pipeline schedule on master sets BUILD_WITH_NEXT_RUBY_VERSION, so the default-branch rule never fires. Scheduled next-Ruby coverage comes from ruby-next alone, every odd-numbered hour at :10. A default-branch schedule that sets that variable is separate work, not something the bump gets for free.

4. Add a regression guard

This is the piece that was missing and is why the flip shipped unnoticed. Add a spec asserting that an unlabelled MR pipeline resolves RUBY_VERSION to RUBY_VERSION_DEFAULT, and that a pipeline:run-in-ruby3_4-labelled one resolves to RUBY_VERSION_NEXT.

spec/dot_gitlab_ci/ci_configuration_validation/pipeline_labels_spec.rb is the right home — it already builds real dry-run pipelines through Ci::CreatePipelineService — but note the existing contexts only assert on expected_job_name, so this needs a variables assertion rather than the existing job-name pattern.

With that guard in place, a future RUBY_VERSION_NEXT bump that leaks into MR pipelines fails CI on the MR that introduces it.

5. Specs and docs housekeeping

  • spec/dot_gitlab_ci/ci_configuration_validation/pipeline_labels_spec.rb:133-134 still exercises pipeline:run-in-ruby3_2; retarget to 3_4.
  • Same stale string in spec/dot_gitlab_ci/ci_configuration_validation/README:12,41.
  • doc/development/pipelines/_index.md ("Ruby versions testing", ~line 791): reword to "3.4 is opt-in via pipeline:run-in-ruby3_4", and update the ruby-next row of the compatibility table (~line 823) to 3.4 once the bump lands.
  • spec/scripts/pipeline/{pre_merge_checks,set_pipeline_name}_spec.rb fixtures say Ruby 3.3 MR and remain correct — PIPELINE_NAME interpolates $RUBY_VERSION.

Sequencing

  1. ✅ Revert the bump — !252027 (merged) (open at time of writing).
  2. Create the label (step 1) — no code change, unblocks step 3.
  3. One MR covering steps 2–5 — the decoupling, the opt-in rule, the guard, specs and docs. Green-lightable while RUBY_VERSION_NEXT is still 3.3.12; the opt-in rule is simply inert until the bump.
  4. Re-land the RUBY_VERSION_NEXT: "3.4.10" bump (a re-do of !244463 (merged)). With steps 2–4 in place this only affects ruby-next and the next-Ruby schedule, and makes pipeline:run-in-ruby3_4 live for anyone who wants it.

Step 3 must merge before step 4, otherwise we reproduce the incident.

Worth knowing: Ruby 3.4 itself looked fine

This issue is about control over the rollout, not about 3.4 being broken. Measured over the ~3h40m the bump was live on master (2026-08-26 07:39–11:20 UTC), against the identical clock window the previous day when every MR was still on 3.3.12:

Tier 3.4.10 3.3.12 (baseline)
1 76% (38 pass / 12 fail) 68% (47 / 22)
2 46% (6 / 7) 50% (6 / 6)
3 63% (31 / 18) 49% (26 / 27)
N/A (docs) 100% (13 / 0) 78% (7 / 2)
untagged 65% (43 / 23) 54% (36 / 31)
All 69% (131 pass / 60 fail) 58% (122 / 88)

290 Ruby 3.4.10 MR pipelines were created in that window and 131 finished green. Caveat: 68 were still running when the revert was raised, and long tier-3 e2e runs are over-represented in that pending set, so the headline 69% would have drifted down somewhat — the tier-matched rows are the fairer comparison.

The first Scheduled Ruby 3.4.10 ruby-next branch run (09:14 UTC) went red, but not for a Ruby reason: all 100 parent-pipeline jobs passed, all 39 gem child pipelines passed on 3.4, e2e:test-on-cng and e2e:test-on-gdk passed, and only e2e:test-on-omnibus-ee failed (ldap-tls, ldap-no-tls, smtp, ai-gateway). ruby-next was already chronically red on 3.3 — 8 of the 11 pre-revert runs failed, each on a different e2e bridge.

The 3.4 fallout raised during !244463 (merged) review (bundled-gem extractions, frozen-string-literal warnings from outdated gems such as unleash 3.2.2, and the shared-gem audit in &18009 (closed)) did not show up as pipeline failures in that window.

Prerequisites already satisfied

No gitlab-build-images work is required. The exact images for Ruby 3.4.10 already exist with the right tag:

  • ci/debian-trixie-slim-ruby-3.4.10-golang-1.25-node-22.12-python-3.12-postgresql-{16,17,18}:rubygems-3.6-git-2.51-lfs-2.9-chrome-151-yarn-1.22-graphicsmagick-1.3.36-poetry-2.2
  • ci/debian-trixie-slim-ruby-3.4.10, ci/debian-trixie-slim-ruby-3.4.10-node-22.12, debian-trixie-ruby-3.4.10
  • ci/ubi-9.7-ruby-3.4.10 (needed by cache:ruby-gems-ubi)

Also free:

  • .if-ruby-branch already matches the new label, so it automatically pulls in build-qa-image, assets-shared, determine-e2e-tests and e2e-blocking-base-after.
  • Caches are keyed on ${RUBY_VERSION} (.ruby-gems-cache, .rubocop-cache, .assets-cache in .gitlab/ci/global.gitlab-ci.yml), so 3.4 pipelines cannot poison 3.3 caches.

Risks / notes

  • First labelled MRs will be slow until the ruby-next scheduled pipeline has pushed a ruby-gems-…-ruby-3.4.10-… cache. cache:ruby-gems runs under if-default-refs, which includes ruby-next, so this resolves itself within a couple of hours — land the version bump before advertising the label.
  • Ruby skew between MRs and master. Once the label is in use, a 3.4-only failure can block a labelled MR without ever appearing on master. That is the intended trade-off of opt-in, and is far smaller than the skew we had while 3.4 was the MR-wide default.
  • as-if-foss child pipeline, worth an eyeball on the first labelled MR rather than a code change: it forwards RUBY_VERSION explicitly (.gitlab/ci/as-if-foss.gitlab-ci.yml), while the child's own $START_AS_IF_FOSS workflow rule sets *default-ruby-variables. Trigger variables should take precedence over workflow:rules:variables, and this arrangement worked during the 3.2/3.3 era.
Edited by 🤖 GitLab Bot 🤖