Skip to content

Upgrade the backend to Ruby 3.4.10 - #917

Open
suttondemlong wants to merge 4 commits into
masterfrom
chore/ruby-3-4
Open

Upgrade the backend to Ruby 3.4.10#917
suttondemlong wants to merge 4 commits into
masterfrom
chore/ruby-3-4

Conversation

@suttondemlong

@suttondemlong suttondemlong commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Ruby 3.2 reached end of life on 2026-04-01, so the backend has been running unpatched since. This takes it to 3.4.10, the branch in normal maintenance that Rails 7.2 officially supports.

Going straight from 3.2 to 3.4 is what doing the Rails 7.2 bump first (#907) bought us: Rails 7.1 never officially supported Ruby 3.4, so an earlier Ruby bump would have had to stop at 3.3.12 and move again later.

Independent of #916 (the 7.2 defaults flip). Neither touches a file the other does, so they can merge in either order.

Commits

Ordered so the prerequisite lands first and is valid on Ruby 3.2.3 — reverting the version bump still leaves a working app.

  1. Add mutex_m to the Gemfile ahead of Ruby 3.4
  2. Upgrade to Ruby 3.4.10
  3. Update pry-doc so it recognises Ruby 3.4
  4. Update the docs for Ruby 3.4.10

Three things worth a reviewer's attention

1. mutex_m, and how badly it fails without this

Ruby 3.4 moved mutex_m out of the default gems and into the bundled gems. Bundled gems are not on the load path of a bundled application unless the Gemfile asks for them, so require "mutex_m" starts failing.

Something does require it: httpclient/auth.rb, and pusher depends on httpclient (~> 2.8). httpclient 2.8.3 is from 2016 and will not be fixing this.

The failure mode is the part worth reading. webmock's httpclient adapter wraps its require "httpclient" in a rescue LoadError, so the LoadError from auth.rb is swallowed — but httpclient.rb requires auth.rb on line 19, before the rest of the class body, so HTTPClient is left half-defined. The adapter then subclasses it and dies on alias_method:

NameError: undefined method 'do_get_block' for class 'WebMockHTTPClient'

That happens at require "webmock", which config/application.rb reaches through Bundler.require, so all 63 spec files fail to load and the suite reports 0 examples. Nothing in the message mentions mutex_m, Ruby 3.4 or httpclient.

Commit 1 is deliberately separate and lands the fix on Ruby 3.2.3, where mutex_m 0.3.0 is a no-op, so the version bump does not have to carry an unrelated fix.

Audited the rest of the extracted stdlib while there. Ruby 3.4 moved about a dozen libraries to bundled gems; base64, bigdecimal, benchmark, csv, drb, logger, ostruct and racc are already explicit lockfile entries via Rails and Mongoid. The only other gems in the tree that reach for one are drb/observer.rb (observer) and cgi/session/pstore.rb (pstore), and neither file is loaded — require "drb" and require "cgi" both succeed on 3.4.10 in this bundle, because cgi.rb's reference to pstore is inside a comment.

2. backend/.ruby-version is a symlink, so §2.4.2 of the plan is moot

The upgrade plan flags .ruby-version existing in two places as a trap. It turns out backend/.ruby-version is mode 120000 pointing at ../.ruby-version, and has been since the Ruby 3.0 upgrade — so the two cannot drift and one edit moves both. Five files carry the version, not six:

/.ruby-version        3.2.3 -> 3.4.10   (backend/.ruby-version symlinks here)
/.tool-versions       ruby 3.2.3 -> ruby 3.4.10
/backend/Gemfile      ruby "3.2.3" -> ruby "3.4.10"
/backend/Dockerfile   FROM ruby:3.2.3 -> FROM ruby:3.4.10
/backend/Gemfile.lock RUBY VERSION

The underlying trap was real, but it was the erb-lint job reading a different path than the others, and #889 fixed that.

No workflow file changes, deliberately. Every backend job resolves Ruby from backend/.ruby-version rather than a pinned ruby-version: input, so they pick this up on their own — and touching .github/workflows/** would trigger the frontend and native job sets too, since all three workflows list it as a path filter.

3. The lockfile moves by one line

bundle lock under 3.4.10 rewrote RUBY VERSION and re-resolved every gem to the version it already had. Nothing in the tree needed a bump to support Ruby 3.4. PLATFORMS stays ruby.

The one exception is pry-doc (commit 3), which is not a compatibility problem but a noise one: it ships the Ruby core docs it serves, keyed by interpreter version, and 1.5.0 predates 3.4, so it printed

ruby/3.4.10 isn't supported by this pry-doc version

on stdout at every boot and at the head of every rspec run. 1.7.0 covers 3.4. Development and test group only; the resolve is conservative and moves only pry-doc and its yard floor.

Verification

On Ruby 3.4.10, with the bundle rebuilt from this lockfile:

  • 331 examples, 0 failures, unchanged from master, in ~7s.
  • standardrb, erblint --lint-all and zeitwerk:check clean.
  • Boots in development, test and production.
  • Also ran the whole suite under --yjit (331 examples, 0 failures), because Enable the Rails 7.2 framework defaults #916 sets config.yjit = true and Ruby 3.4 is where that stops being inert. Confirmed separately on a YJIT-enabled build that RubyVM::YJIT.enabled? is true after a production boot.
  • ruby:3.4.10 exists on Docker Hub, so make build has a base image.

Not verified

  • The Heroku stack. The version is picked up from the Gemfile; please confirm 3.4.10 is available on the app's stack before merging.
  • Ran against PostgreSQL 15.19 and MongoDB 7.0.14 locally, not the 12.8 and 4.4.9 that docker-compose.yml and the rspec job pin. CI on the real pins is the gate.

🤖 Generated with Claude Code

Ruby 3.4 moved mutex_m out of the default gems and into the bundled gems.
Bundled gems are not on the load path of a bundled application unless the
Gemfile asks for them, so `require "mutex_m"` starts failing there.

Something does require it: httpclient/auth.rb, and pusher depends on
httpclient (~> 2.8). httpclient 2.8.3 is from 2016 and will not be fixing
this.

The way it fails is worth recording, because the message points nowhere near
the cause. webmock's httpclient adapter wraps its `require "httpclient"` in a
`rescue LoadError`, so the LoadError from auth.rb is swallowed -- but
httpclient.rb requires auth.rb on line 19, before the rest of the class body,
so `HTTPClient` is left half-defined. The adapter then subclasses it and dies
on `alias_method :do_get_block_without_webmock, :do_get_block`:

    NameError: undefined method 'do_get_block' for class 'WebMockHTTPClient'

That happens at `require "webmock"`, which config/application.rb reaches
through Bundler.require, so all 63 spec files fail to load and the suite
reports 0 examples. Nothing in the message mentions mutex_m, Ruby 3.4 or
httpclient.

Landing this before the Ruby bump rather than with it, per the plan's rule
about bumping inside the compatibility overlap: mutex_m 0.3.0 works on 3.2.3
as well, so this commit is a no-op today and the next one does not have to
carry an unrelated fix.

Audited the rest of the extracted stdlib while here. Ruby 3.4 moved a dozen
libraries to bundled gems; base64, bigdecimal, benchmark, csv, drb, logger,
ostruct and racc are already explicit entries in the lockfile via Rails and
Mongoid. The two other gems in the tree that reach for one are drb/observer.rb
(`observer`) and cgi/session/pstore.rb (`pstore`), and neither file is loaded
-- `require "drb"` and `require "cgi"` both succeed on 3.4.10 in this bundle,
because cgi.rb's reference to pstore is inside a comment.

Verified on Ruby 3.2.3: 324 examples, 0 failures; standardrb clean. The
lockfile gains two lines and nothing else moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ruby 3.2 reached end of life on 2026-04-01, so the backend has been running
unpatched since. 3.4 is the branch in normal maintenance that Rails 7.2
officially supports; going straight there rather than by way of 3.3.12 is what
doing the Rails 7.2 bump first bought us, since Rails 7.1 never officially
supported 3.4.

Five files carry the version. `backend/.ruby-version` looks like a sixth, but
it is a symlink to the root one and has been since the Ruby 3.0 upgrade, so
the two cannot drift and one edit moves both. That is worth knowing because
the upgrade plan flags them as separate files CI reads independently: the
underlying trap was real, but it was the erb-lint job reading a different path
than the others, and #889 fixed that by passing `working-directory: backend`
to ruby/setup-ruby.

No workflow file changes, deliberately. Every backend job resolves Ruby from
`backend/.ruby-version` rather than a pinned `ruby-version:` input, so they
pick this up on their own -- and touching .github/workflows/** would trigger
the frontend and native job sets too, since all three workflows list it as a
path filter.

The lockfile moves by one line. `bundle lock` under 3.4.10 rewrote RUBY
VERSION and re-resolved every gem to the same version it already had, which is
the outcome to want here: no gem in the tree needed a bump to support 3.4.
PLATFORMS stays `ruby`.

The one Ruby 3.4 incompatibility in the tree was mutex_m, handled in the
previous commit so this one is a version bump and nothing else.

Verified on 3.4.10: 324 examples, 0 failures; standardrb, erblint and
zeitwerk:check clean; boots in development, test and production; suite is
unchanged at ~7s.

Also ran the suite under `--yjit` (324 examples, 0 failures), because #907's
defaults flip sets `config.yjit = true` and Ruby 3.4 is where that stops being
inert -- `RubyVM::YJIT.enable` arrived in 3.3, and Rails guards the initializer
on it. Note that Rails only enables YJIT if the interpreter was built with it;
verify `RubyVM::YJIT.enabled?` on staging, since a Ruby without YJIT compiled
in will silently skip it rather than complain.

Heroku picks the version up from the Gemfile. Confirm the stack has 3.4.10
available before merging.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pry-doc ships the Ruby core documentation it serves, keyed by interpreter
version, and 1.5.0 predates 3.4. It does not fail -- it prints

    ruby/3.4.10 isn't supported by this pry-doc version

on stdout at every `require`, which means on every rails command, every boot
and the head of every rspec run. 1.7.0 (2026-01-14) covers 3.4.

Development and test group only, so the production bundle is unaffected. The
resolve is conservative: pry-doc 1.5.0 -> 1.7.0 and its yard floor from
~> 0.9.11 to ~> 0.9.21, which the locked yard 0.9.44 already satisfies.
Nothing else in the lockfile moves.

Separate from the version bump because it is the only gem the Ruby upgrade
forced, and a reviewer should be able to see the bump on its own.

Verified: 324 examples, 0 failures on 3.4.10, and the warning is gone from
both `rails runner` and rspec output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README.md's environment list and the one-line description of `backend/` in
CLAUDE.md both name the Ruby version, and both were stale.

CLAUDE.md also still said Rails 7.1, which #907 missed. Corrected here rather
than left for later, since it is the same line and the same class of mistake.

The PostgreSQL 12.8 and MongoDB 4.4.9 entries in README.md are accurate --
those are what docker-compose.yml and CI actually run -- so they stay as they
are. Both are past end of life and the upgrade plan tracks them as their own
piece of work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants