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-134still exercisespipeline:run-in-ruby3_2; retarget to3_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 viapipeline:run-in-ruby3_4", and update theruby-nextrow of the compatibility table (~line 823) to 3.4 once the bump lands.spec/scripts/pipeline/{pre_merge_checks,set_pipeline_name}_spec.rbfixtures sayRuby 3.3 MRand remain correct —PIPELINE_NAMEinterpolates$RUBY_VERSION.
Sequencing
✅ Revert the bump — !252027 (merged) (open at time of writing).- Create the label (step 1) — no code change, unblocks step 3.
- One MR covering steps 2–5 — the decoupling, the opt-in rule, the guard, specs and docs. Green-lightable while
RUBY_VERSION_NEXTis still3.3.12; the opt-in rule is simply inert until the bump. - Re-land the
RUBY_VERSION_NEXT: "3.4.10"bump (a re-do of !244463 (merged)). With steps 2–4 in place this only affectsruby-nextand the next-Ruby schedule, and makespipeline:run-in-ruby3_4live 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.2ci/debian-trixie-slim-ruby-3.4.10,ci/debian-trixie-slim-ruby-3.4.10-node-22.12,debian-trixie-ruby-3.4.10ci/ubi-9.7-ruby-3.4.10(needed bycache:ruby-gems-ubi)
Also free:
.if-ruby-branchalready matches the new label, so it automatically pulls inbuild-qa-image,assets-shared,determine-e2e-testsande2e-blocking-base-after.- Caches are keyed on
${RUBY_VERSION}(.ruby-gems-cache,.rubocop-cache,.assets-cachein.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-nextscheduled pipeline has pushed aruby-gems-…-ruby-3.4.10-…cache.cache:ruby-gemsruns underif-default-refs, which includesruby-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 onmaster. 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-fosschild pipeline, worth an eyeball on the first labelled MR rather than a code change: it forwardsRUBY_VERSIONexplicitly (.gitlab/ci/as-if-foss.gitlab-ci.yml), while the child's own$START_AS_IF_FOSSworkflow rule sets*default-ruby-variables. Trigger variables should take precedence overworkflow:rules:variables, and this arrangement worked during the 3.2/3.3 era.