Upgrade the backend to Ruby 3.4.10 - #917
Open
suttondemlong wants to merge 4 commits into
Open
Conversation
This was referenced Sep 9, 2026
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>
suttondemlong
force-pushed
the
chore/ruby-3-4
branch
from
September 10, 2026 00:01
455f551 to
7068a19
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Add mutex_m to the Gemfile ahead of Ruby 3.4Upgrade to Ruby 3.4.10Update pry-doc so it recognises Ruby 3.4Update the docs for Ruby 3.4.10Three things worth a reviewer's attention
1.
mutex_m, and how badly it fails without thisRuby 3.4 moved
mutex_mout 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, sorequire "mutex_m"starts failing.Something does require it:
httpclient/auth.rb, andpusherdepends onhttpclient (~> 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 arescue LoadError, so the LoadError fromauth.rbis swallowed — buthttpclient.rbrequiresauth.rbon line 19, before the rest of the class body, soHTTPClientis left half-defined. The adapter then subclasses it and dies onalias_method:That happens at
require "webmock", whichconfig/application.rbreaches throughBundler.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_m0.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,ostructandraccare already explicit lockfile entries via Rails and Mongoid. The only other gems in the tree that reach for one aredrb/observer.rb(observer) andcgi/session/pstore.rb(pstore), and neither file is loaded —require "drb"andrequire "cgi"both succeed on 3.4.10 in this bundle, because cgi.rb's reference to pstore is inside a comment.2.
backend/.ruby-versionis a symlink, so §2.4.2 of the plan is mootThe upgrade plan flags
.ruby-versionexisting in two places as a trap. It turns outbackend/.ruby-versionis mode120000pointing 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:The underlying trap was real, but it was the
erb-lintjob reading a different path than the others, and #889 fixed that.No workflow file changes, deliberately. Every backend job resolves Ruby from
backend/.ruby-versionrather than a pinnedruby-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 lockunder 3.4.10 rewroteRUBY VERSIONand re-resolved every gem to the version it already had. Nothing in the tree needed a bump to support Ruby 3.4.PLATFORMSstaysruby.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 printedon 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:
master, in ~7s.standardrb,erblint --lint-allandzeitwerk:checkclean.--yjit(331 examples, 0 failures), because Enable the Rails 7.2 framework defaults #916 setsconfig.yjit = trueand Ruby 3.4 is where that stops being inert. Confirmed separately on a YJIT-enabled build thatRubyVM::YJIT.enabled?is true after a production boot.ruby:3.4.10exists on Docker Hub, somake buildhas a base image.Not verified
Gemfile; please confirm 3.4.10 is available on the app's stack before merging.docker-compose.ymland therspecjob pin. CI on the real pins is the gate.🤖 Generated with Claude Code