Rails 6.1 to 8.1 on Ruby 3.4 - #505
Open
OlegPhenomenon wants to merge 15 commits into
Open
OlegPhenomenon wants to merge 15 commits into
OlegPhenomenon wants to merge 15 commits into
Conversation
Preparation for Rails 7+: config.autoloader = :classic has no effect there, the option and the classic autoloader itself were removed in 7.0. Doing the switch here, still on 6.1, keeps autoloading failures apart from framework upgrade failures. The only file that was not zeitwerk compatible is the bounce concern. app/models/concerns is an autoload root of its own, so a file sitting at concerns/contact_request/bounceable.rb had to define ContactRequest::Bounceable, while it actually defined Concerns::ContactRequest::Bounceable - an extra namespace level that only the classic autoloader tolerated. Moved to concerns/bounceable.rb as a plain top level Bounceable, which is where a single concern belongs anyway; reopening the ContactRequest class from inside a second autoload root would have worked too, but it buys a namespace we have no other use for at the price of an explicit-namespace load order. rails zeitwerk:check runs in CI from now on, so the next mismatch is a failed build instead of a failed deploy.
Rails 6.1 stopped receiving security fixes on 1 October 2024. The obvious
target, 7.2, is not a target any more either: that series ended its
security-only window on 9 August 2026, twelve days ago, which is what the
open Snyk pull request would have upgraded us to. 8.1 is the only series
with a future - bug fixes until 10 October 2026, security fixes until
10 October 2027 - so this goes straight there.
Ruby needs no change: .ruby-version already says 3.4.9, which is both above
the 3.2 that Rails 8.1 requires and the version the guides recommend for it.
CI, however, was still pinned to 3.0.3 in the matrix, which is how a Rails
version the app cannot even boot on could have passed. It now reads
.ruby-version like the servers do.
Along the way:
* lib/tasks/db.rake called ActiveRecord::Base.clear_all_connections!, which
was a delegation to the connection handler that 6.1 still had and 7.1
removed. Nothing covers that task in tests - it migrates both production
databases - so it would have failed silently until a deploy ran it.
* mimemagic is gone. Active Storage depended on it up to Rails 5.2 and has
used marcel since 6.0; nothing in this application references it, and
nothing in the lockfile depends on it either.
* simplecov is unpinned. The 0.17.1 pin carried the comment "CC last
supported v0.17", but coverage has been going to qlty for a while now.
Ruby 3.4 also brings bundler 2.6 to the servers, which finally makes
force_ruby_platform understood there, so pg can stay on 1.6.x and be compiled
from source instead of being held back to 1.5.x.
minitest 6 extracted mocks and stubs into a gem of their own, and both test_helper and the mailer test use them.
The lockfile has always listed "ruby" as its only platform, which is what makes native extensions be compiled from source - the servers cannot use the published Linux binaries of pg anyway. Regenerating it inside a container had replaced that with the container's own platform.
* Date#to_s(:iso8601) - the formatting to_s was removed in Rails 7.0 in
favour of to_fs.
* config.action_dispatch.show_exceptions takes a symbol since 7.1; the
boolean it used to hold stopped meaning anything, so integration tests
got rendered error pages instead of raised exceptions.
* Net::SMTPFatalError now takes a mandatory argument, so raising the bare
class fails with ArgumentError before the code under test is reached.
config.active_support.parse_json_times said true and did nothing: on 6.1 the railtie only applies settings ActiveSupport already responds to, and it did not respond to this one that early, which is why a suite full of assertions on ISO strings has been green for years and why the views call .to_datetime on values that would already be times otherwise. Rails 8 does apply the setting, which turns every date in the whois payload into a TimeWithZone and changes how it is rendered on the public pages and in the json API. Written out as false so the upgrade keeps the output it had, with the trap spelled out for whoever reads the history and decides the true was lost by accident. Test merges are stringified for the same reason: symbol keys merged onto the string-keyed json hash produced literally duplicate keys in the encoded document, which json 3.0 will refuse outright.
The application was still initialising with the 6.0 defaults, so everything Rails changed its mind about between 6.0 and 8.1 was switched off. The 5.2 defaults file goes with it: every option in it was commented out, so it was a list of decisions nobody ever made.
update_request_secret ran ContactRequest#update(secret:) whenever the environment was test, which attr_readonly has always dropped on the way to the database - the reload right after it read the original secret back. Since the 7.1 defaults an assignment to a readonly attribute raises instead of being ignored, so the dead line started taking the confirmation flow down with it. The two model tests that asserted the silent no-op now assert the exception, which is the same guarantee stated out loud.
The 7.1 defaults refuse cross host redirects unless they say so explicitly. This one goes to main_page_url, which is server configuration and not anything the visitor can influence.
The deploy got past pg and died on nokogiri, loading it through sassc-rails: libm.so.6: version `GLIBC_2.29' not found (required by nokogiri-1.19.4-x86_64-linux-gnu/lib/nokogiri/3.4/nokogiri.so) Same story as pg: nokogiri's precompiled x86_64-linux-gnu gem requires glibc 2.29 or newer (nokogiri.org/tutorials/installing_nokogiri.html, checked via web 2026-08-27; the floor was raised in 1.18), and the deploy servers are older than that. force_ruby_platform makes Bundler ignore the binary and compile from source, which is what the gem's own error message recommends. nokogiri is a Rails dependency, not one we use directly, so it has to be named in the Gemfile just to carry the flag - and therefore also in DEPENDENCIES. Locking PLATFORMS to "ruby" was supposed to cover this, but it only holds if the install honours the lockfile: `bundle install` outside frozen/deployment mode adds the machine's own platform and re-resolves, which is where the native gem came from. Per-gem force_ruby_platform survives that, since it is a property of the Gemfile dependency rather than of the lockfile. Resolution is unchanged - the lockfile already held the ruby platform variant of nokogiri. Verified on Ruby 3.4.9 / Bundler 2.5.23 in a container: after `bundle lock --add-platform x86_64-linux`, ffi is the only gem left resolving to a native x86_64-linux-gnu build; nokogiri and pg stay on the ruby platform. ffi's binary needs no symbol above GLIBC_2.27, so it still loads on a 2.27/2.28 server, but it is next in line if the servers are older than that. Also brings DEPENDENCIES back in sync with the Gemfile: it asked for pg ~> 1.5.9 while both the Gemfile and the resolved version say 1.6.3, so the lockfile could not be satisfied as written.
OlegPhenomenon
force-pushed
the
upgrade/rails-8-1
branch
from
August 28, 2026 07:00
bf5c681 to
d1c1c57
Compare
The branch was force-pushed back onto the pre-rebase line, and with it came back
the state of four files as they were before the staging deploys of 2026-08-27.
CI stops on its very first step:
gem install bundler -v 2.6.9
ERROR: bundler-2.6.9 requires Ruby version >= 3.1.0.
The current ruby version is 3.0.3.157.
because .ruby-version went back to 3.0.3 while Gemfile.lock still says BUNDLED
WITH 2.6.9 - and the Gemfile asks for Rails 8.1, which needs 3.2 anyway. This is
the same trap the workflow comment warned about: the runner reads .ruby-version,
so a stale number there does not fail loudly, it fails before the suite runs.
Restored, all of it verified on staging before it was lost:
- .ruby-version back to 3.4.9, which is what the servers run
- gem "syslog", without which the app cannot boot on Ruby 3.4 - syslog is a
bundled gem now and Bundler hides it unless the Gemfile asks
- uglifier gone again, from the Gemfile and from production.rb: the app ships
no JavaScript, and uglify-js died on the staging JS runtime with an empty
Uglifier::Error during assets:precompile
- the workflow comment explaining why the matrix reads .ruby-version
Everything the force-push added - the contact request work, its tests, the
dependabot and renovate changes, the changelog - is left untouched.
Also untracks .repowise/sessions/sessions.db, a local index that slipped into
the commit; .gitignore already lists .repowise/, so it will not come back.
Lockfile regenerated rather than hand-edited: syslog 0.4.0 in, uglifier and
execjs out, nothing else moved. Verified on Ruby 3.4.9 in a container that
bundle lock --no-update reproduces it byte for byte, and that ffi remains the
only gem resolving to a native x86_64-linux build.
maricavor
approved these changes
Aug 28, 2026
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.
Rails 6.1.4.1 stopped receiving security fixes on 1 October 2024. The obvious target, 7.2, stopped being one too: that series ended its security-only window on 9 August 2026, which is where the open Snyk pull request #502 would have taken us. 8.1 gets bug fixes until 10 October 2026 and security fixes until 10 October 2027, so this goes straight there.
.ruby-versionneeded no change - master already says 3.4.9, above the 3.2 that Rails 8.1 requires. What did need changing is the CI matrix, which was pinned to 3.0.3 while the servers ran 3.4.9. That is how a Rails version the application cannot boot on could have passed CI, and it is the same gap that produced the last three failed deploys.What is in here
Each step is its own commit and can be reverted on its own.
config.autoloader = :classichas no meaning since Rails 7.0. The one file that was not compatible was the bounce concern: sitting underapp/models/concerns/contact_request/, which is an autoload root of its own, it had to defineContactRequest::Bounceableand actually definedConcerns::ContactRequest::Bounceable. It is now a plainBounceable.rails zeitwerk:checkruns in CI from now on.mimemagic(Active Storage replaced it with marcel back in 6.0 and nothing here references it) and the unpinning ofsimplecov(the 0.17.1 pin existed for Code Climate, coverage goes to qlty now).minitest-mock- minitest 6 moved mocks and stubs into a gem of their own.rubyplatform, so native extensions keep being compiled from source. The servers cannot load the published Linux binaries.to_s(:iso8601)becameto_fs,show_exceptionstakes a symbol,Net::SMTPFatalErrortakes a mandatory argument.parse_json_timeswritten out as false - see below.Two things worth a second look
config.active_support.parse_json_timessaidtrueand did nothing. On 6.1 the ActiveSupport railtie only applies settings the module already responds to, and it did not respond to this one that early in boot. So every date inside the whois json column has always reached the views and the json API as a plain string - which is why the suite is full of assertions on ISO strings and why the views call.to_datetimeon those values. Rails 8 does apply the setting, and turning it on would change the date format of the public whois output. That is a product decision, not something an upgrade should do on the side, so the line now saysfalsewith the trap written next to it.update_request_secretwas dead code. It calledupdate(secret:)whenever the environment was test, andattr_readonlyhad always dropped that on the way to the database. Since the 7.1 defaults such an assignment raises instead of being ignored, so the line started taking the whole confirmation flow down with it. Removed; the two model tests that asserted the silent no-op now assert the exception.Verification
CI is green: 162 runs, 423 assertions, 0 failures, 0 errors on Ruby 3.4.9, with
zeitwerk:checkpassing and the system tests rendering the pages through sprockets and sassc.What CI cannot answer is the deploy itself.
pgcarriesforce_ruby_platform: trueso it is compiled from source instead of pulling the binary linked against glibc 2.29 that the servers cannot load; the option needs bundler >= 2.3.18, which the servers only get now, with Ruby 3.4. Asset precompilation and Passenger against Rack 3 are the other two things only a real deploy can confirm.Note on #503
fix/contact-request-registry-syncholdspgat 1.5.9 and pins Ruby back to 3.0.3, because at the time bundler on the servers was too old to understandforce_ruby_platform. Merging both will conflict onGemfile,Gemfile.lockandrenovate.json. This branch is the state to keep in those files.