{"id":14886,"date":"2026-10-25T18:30:00","date_gmt":"2026-10-25T13:00:00","guid":{"rendered":"https:\/\/www.allerin.com\/blog\/?p=14886"},"modified":"2026-09-25T11:21:16","modified_gmt":"2026-09-25T05:51:16","slug":"rails-upgrade-5-2-6-x-to-8-1","status":"publish","type":"post","link":"https:\/\/www.allerin.com\/blog\/rails-upgrade-5-2-6-x-to-8-1\/","title":{"rendered":"Rails upgrade ladder from 5.2 and 6.x to 8.1"},"content":{"rendered":"<p>A Rails upgrade from 5.2 or 6.x to 8.1 has several finish lines. The application must boot, preserve its behavior, read existing data and survive a mixed deployment with the previous release. A successful bundle install establishes none of those by itself.<\/p>\n<p>The useful unit of work is one minor-version hop with evidence attached. Below is the route, a tested 7.0-to-7.1 example and the decisions that deserve separate changes. The examples are synthetic applications prepared for this article, not accounts of client upgrades. Versions and source claims were checked on 13 September 2026.<\/p>\n<nav style=\"margin: 24px 0; padding: 18px; background: #edf4f0; border-radius: 6px;\" aria-label=\"Article sections\">\n<p style=\"margin: 0 0 12px;\"><strong>Choose the relevant section<\/strong><\/p>\n<ul style=\"display: flex; flex-wrap: wrap; gap: 10px 24px; list-style: none; margin: 0; padding: 0;\">\n<li style=\"margin: 0;\"><a href=\"#plan-the-rails-upgrade-before-changing-the-gemfile\">Plan the Rails upgrade before changing the Gemfile<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#the-seven-rungs\">The seven rungs<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#choose-a-runtime-that-both-bundles-can-actually-use\">Choose a runtime that both bundles can actually use<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#a-hop-with-reviewable-commits\">A hop with reviewable commits<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#give-every-dependency-a-verdict\">Give every dependency a verdict<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#check-loading-assets-and-data-separately\">Check loading, assets and data separately<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#when-the-next-step-should-be-something-else\">When the next step should be something else<\/a><\/li>\n<li style=\"margin: 0;\"><a href=\"#what-to-do-this-week\">What to do this week<\/a><\/li>\n<\/ul>\n<\/nav>\n<h2 id=\"plan-the-rails-upgrade-before-changing-the-gemfile\" style=\"scroll-margin-top: 128px;\">Plan the Rails upgrade before changing the Gemfile<\/h2>\n<p>Start with the latest patch of the application&#8217;s current minor, then move through each minor release. Deprecations in that patched baseline identify work the next release expects you to have completed. The <a href=\"https:\/\/guides.rubyonrails.org\/upgrading_ruby_on_rails.html#the-upgrade-process\" target=\"_blank\" rel=\"noopener\">official upgrading guide<\/a> describes this sequence. Unsupported intermediate releases are rehearsal checkpoints; a green checkpoint is not a recommendation to expose another unsupported production deployment.<\/p>\n<p>Keep the existing <code style=\"overflow-wrap: anywhere; white-space: normal;\">config.load_defaults<\/code> value while changing the framework. Review the generated defaults file one setting at a time, with a test and a commit for each adopted change. Cookies, caches and serialized columns need reader\/writer compatibility checks and separate rollout decisions. Earlier 4.2 and 5.0 applications predate <code style=\"overflow-wrap: anywhere; white-space: normal;\">load_defaults<\/code>; inspect their individual initializers instead.<\/p>\n<p>Build a gem verdict list before cutting the upgrade branch. Keep both framework bundles running in CI so small compatibility fixes can merge while ordinary development continues. Change Ruby between Rails hops, using an actually resolved and tested overlap. Pin the existing asset pipeline during the hop. Finish the Zeitwerk conversion on 6.x, while the transition is still available, before attempting 7.0.<\/p>\n<p>These choices make a failure attributable. They also leave room to stop at a failed check without quietly accepting a different transaction, authorization or serialization behavior.<\/p>\n<h2 id=\"the-seven-rungs\" style=\"scroll-margin-top: 128px;\">The seven rungs<\/h2>\n<p>The labels match <a href=\"https:\/\/www.allerin.com\/services\/rails-upgrades\">Allerin&#8217;s upgrade ladder<\/a>. Grouped labels still contain separate minor upgrades. Ruby values below are declared minimums from the tagged gemspecs, not a promise that every later Ruby runs an old Rails release. The download contains all eleven patch checkpoints and five component checks per checkpoint.<\/p>\n<div style=\"overflow-x: auto;\" tabindex=\"0\" role=\"region\" aria-label=\"Rails upgrade ladder and Ruby minimums\">\n<table style=\"width: 100%; min-width: 620px; table-layout: auto; border-collapse: collapse; font-size: 16px; line-height: 1.55;\" aria-label=\"Rails upgrade ladder and Ruby minimums\">\n<thead>\n<tr>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Hop<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Ruby minimum at destination<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Work to verify<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Separate decision<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 4.2 to 5.0, 5.1, 5.2<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">2.2.2 throughout 5.x<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Model callbacks, parameters, historical migrations<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Credentials and attachment migration<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 5.2 to 6.0 to 6.1<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">2.5.0 throughout 6.x<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Zeitwerk, query semantics, removed APIs<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">JavaScript tooling and cookie formats<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 6.1 to 7.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">2.7.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Classic removal, explicit asset dependencies<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Ruby keywords, cookies, cache, variants<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 7.0 to 7.1<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">2.7.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Column coders, exceptions, callback order<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Message and cache formats<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 7.1 to 7.2<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">3.1.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Secrets removal, job tests, enum calls<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Runtime and queue policy<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 7.2 to 8.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">3.2.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Removed deprecations and dependency constraints<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Queue, cache and asset replacement<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Rails 8.0 to 8.1<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">3.2.0<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Schema dump and affected finder ordering<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Business ordering and unrelated rewrites<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<h3 id=\"rails-4-2-to-5-0-5-1-5-2\" style=\"scroll-margin-top: 128px;\">Rails 4.2 to 5.0, 5.1, 5.2<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/5_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">Rails 5.0 in 2016<\/a> introduced generated <code style=\"overflow-wrap: anywhere; white-space: normal;\">ApplicationRecord<\/code> bases and required <code style=\"overflow-wrap: anywhere; white-space: normal;\">belongs_to<\/code> defaults. Review callbacks returning <code style=\"overflow-wrap: anywhere; white-space: normal;\">false<\/code>, use <code style=\"overflow-wrap: anywhere; white-space: normal;\">throw(:abort)<\/code> for intentional halts, and inspect code treating controller parameters as a Hash. Migrate away from <code style=\"overflow-wrap: anywhere; white-space: normal;\">protected_attributes<\/code>. The removal of <code style=\"overflow-wrap: anywhere; white-space: normal;\">render :text<\/code> and <code style=\"overflow-wrap: anywhere; white-space: normal;\">:nothing<\/code> belongs to <strong>5.1<\/strong>, not 5.0.<\/p>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/5_1_release_notes.html\" target=\"_blank\" rel=\"noopener\">5.1 in 2017<\/a> added system tests and changed generated primary keys to bigint. <a href=\"https:\/\/guides.rubyonrails.org\/5_2_release_notes.html\" target=\"_blank\" rel=\"noopener\">5.2 in 2018<\/a> introduced credentials and Active Storage; installing it does not migrate existing secrets or attachments. Review <code style=\"overflow-wrap: anywhere; white-space: normal;\">use_authenticated_cookie_encryption<\/code>, including embedded expiry, before changing cookies read by older processes.<\/p>\n<h3 id=\"rails-5-2-to-6-0-to-6-1\" style=\"scroll-margin-top: 128px;\">Rails 5.2 to 6.0 to 6.1<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/6_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">6.0 in 2019<\/a> brought Zeitwerk, host authorization and parallel testing. Webpacker was a generated-app choice, not an instruction to replace an existing JavaScript build. The <code style=\"overflow-wrap: anywhere; white-space: normal;\">use_cookies_with_metadata<\/code> default adds purpose and expiry metadata, requiring its own compatibility decision.<\/p>\n<p>On <a href=\"https:\/\/guides.rubyonrails.org\/6_1_release_notes.html\" target=\"_blank\" rel=\"noopener\">6.1 in 2020<\/a>, inspect multi-attribute <code style=\"overflow-wrap: anywhere; white-space: normal;\">where.not<\/code> queries for the change to NAND semantics, removed <code style=\"overflow-wrap: anywhere; white-space: normal;\">update_attributes<\/code> calls and symbol access in <code style=\"overflow-wrap: anywhere; white-space: normal;\">config_for<\/code>. Review <code style=\"overflow-wrap: anywhere; white-space: normal;\">has_many_inversing<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">cookies_same_site_protection<\/code> and Active Storage&#8217;s required <code style=\"overflow-wrap: anywhere; white-space: normal;\">service_name<\/code> migration against actual behavior.<\/p>\n<h3 id=\"rails-6-1-to-7-0\" style=\"scroll-margin-top: 128px;\">Rails 6.1 to 7.0<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/7_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">7.0 in 2021<\/a> removes the classic autoloader. Retaining older defaults cannot restore it. Declare <code style=\"overflow-wrap: anywhere; white-space: normal;\">sprockets-rails<\/code> explicitly if the application uses it; installing Rails 7 does not require replacing Turbolinks or UJS with Turbo.<\/p>\n<p>The <a href=\"https:\/\/github.com\/rails\/rails\/blob\/v7.0.10\/railties\/lib\/rails\/application\/configuration.rb\" target=\"_blank\" rel=\"noopener\">7.0 defaults<\/a> select <code style=\"overflow-wrap: anywhere; white-space: normal;\">cookies_serializer = :json<\/code>, <code style=\"overflow-wrap: anywhere; white-space: normal;\">cache_format_version = 7.0<\/code>, SHA-256 digest\/key generation and <code style=\"overflow-wrap: anywhere; white-space: normal;\">variant_processor = :vips<\/code>. They also disable <code style=\"overflow-wrap: anywhere; white-space: normal;\">partial_inserts<\/code> and enable <code style=\"overflow-wrap: anywhere; white-space: normal;\">raise_on_open_redirects<\/code>. Separate data readers, native image processing and changed insert\/redirect behavior into independently tested decisions.<\/p>\n<h3 id=\"rails-7-0-to-7-1\" style=\"scroll-margin-top: 128px;\">Rails 7.0 to 7.1<\/h3>\n<p>The <a href=\"https:\/\/github.com\/rails\/rails\/blob\/v7.1.6\/railties\/lib\/rails\/application\/configuration.rb\" target=\"_blank\" rel=\"noopener\">2023 defaults<\/a> set <code style=\"overflow-wrap: anywhere; white-space: normal;\">default_column_serializer = nil<\/code>, select cache format 7.1 and message serializer <code style=\"overflow-wrap: anywhere; white-space: normal;\">:json_allow_marshal<\/code>, and enable <code style=\"overflow-wrap: anywhere; white-space: normal;\">use_message_serializer_for_metadata<\/code>. Selecting JSON output can leave Marshal enabled on reads. Write down the actual allowed reader formats.<\/p>\n<p>Test <code style=\"overflow-wrap: anywhere; white-space: normal;\">show_exceptions<\/code>&#8216; enum values, transaction callback ordering, the HTML5 sanitizer and any <code style=\"overflow-wrap: anywhere; white-space: normal;\">autoload_lib<\/code> adoption. The renamed local secret file affects development\/test; it is not a production key-rotation instruction. Address the deprecation of <code style=\"overflow-wrap: anywhere; white-space: normal;\">Rails.application.secrets<\/code> before the next rung.<\/p>\n<h3 id=\"rails-7-1-to-7-2\" style=\"scroll-margin-top: 128px;\">Rails 7.1 to 7.2<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/7_2_release_notes.html\" target=\"_blank\" rel=\"noopener\">7.2 in 2024<\/a> raises the Ruby floor to 3.1 and removes the secrets API. Test helpers now respect the configured Active Job adapter more consistently. Enqueue-after-commit behavior also depends on job configuration and adapter support; do not silently replace the queue backend.<\/p>\n<p>Review deprecated enum keyword definitions. YJIT&#8217;s default depends on the eligible Ruby runtime, and generated browser guards are application code, not an unavoidable rejection policy for every upgraded app. A Ruby minimum does not determine the application&#8217;s exact Psych or mail-library bundle.<\/p>\n<h3 id=\"rails-7-2-to-8-0\" style=\"scroll-margin-top: 128px;\">Rails 7.2 to 8.0<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/8_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">8.0 in 2024<\/a> is the next framework checkpoint after cleaning 7.2 deprecations. Solid Queue, Solid Cache, Propshaft and deployment tooling in new applications are separate adoption choices. Keep the current services and asset pipeline explicit while testing the framework change.<\/p>\n<h3 id=\"rails-8-0-to-8-1\" style=\"scroll-margin-top: 128px;\">Rails 8.0 to 8.1<\/h3>\n<p><a href=\"https:\/\/guides.rubyonrails.org\/8_1_release_notes.html\" target=\"_blank\" rel=\"noopener\">8.1 in 2025<\/a> sorts non-primary-key columns in <code style=\"overflow-wrap: anywhere; white-space: normal;\">schema.rb<\/code>. Its finder check warns or raises when the relation and model provide no usable ordering; primary-key ordering remains valid. Give business queries an explicit order where the result depends on one.<\/p>\n<h2 id=\"choose-a-runtime-that-both-bundles-can-actually-use\" style=\"scroll-margin-top: 128px;\">Choose a runtime that both bundles can actually use<\/h2>\n<p>The floors are historical constraints, not supported runtime recommendations. Ruby 2.7, 3.0, 3.1 and 3.2 have ended maintenance, as recorded on the <a href=\"https:\/\/www.ruby-lang.org\/en\/downloads\/branches\/\" target=\"_blank\" rel=\"noopener\">Ruby branch-status page<\/a>. Reproduce old combinations in isolated migration environments and plan a maintained destination.<\/p>\n<p>Where dependency resolution and tests establish an overlap, a runtime step can skip an obsolete intermediate Ruby. Do not turn that into a blanket claim that Rails 5.2 runs on Ruby 3.3. The concrete 7.0\/7.1 rehearsal here uses Ruby 3.3.12; the separate 8.0\/8.1 schema check uses 3.4.10. Neither establishes Ruby 4.0 readiness.<\/p>\n<p>Ruby 2.7 introduced warnings for the <a href=\"https:\/\/www.ruby-lang.org\/en\/news\/2019\/12\/12\/separation-of-positional-and-keyword-arguments-in-ruby-3-0\/\" target=\"_blank\" rel=\"noopener\">keyword-argument separation completed in Ruby 3.0<\/a>. Exercise argument-forwarding wrappers and their callers before that runtime step; changing Rails alone does not fix them.<\/p>\n<h2 id=\"a-hop-with-reviewable-commits\" style=\"scroll-margin-top: 128px;\">A hop with reviewable commits<\/h2>\n<p>The synthetic application keeps Ruby 3.3.12 fixed while moving from Rails 7.0.10 to 7.1.6. These are abbreviated hashes from its actual Git history, included in the download.<\/p>\n<pre style=\"overflow-x: auto; max-width: 100%;\"><code class=\"language-text\">8f5ee6f  Rails 7.1.6 with load_defaults 7.0\r\ne5291ef  Resolve app:update conflicts\r\n00a9d24  Fix deprecated calls; preserve tested data\r\n44bdad8  Begin individual defaults commits\r\nf405d66  Test non-local transaction behavior separately\r\n8ee9abb  Change message writer after compatibility check\r\n<\/code><\/pre>\n<p><code style=\"overflow-wrap: anywhere; white-space: normal;\">bin\/rails app:update<\/code> displayed real overwrite conflicts. Its proposed <code style=\"overflow-wrap: anywhere; white-space: normal;\">application.rb<\/code> already retained 7.0 defaults. The explicit resolution preserved test exception behavior as <code style=\"overflow-wrap: anywhere; white-space: normal;\">:none<\/code>, instead of copying <code style=\"overflow-wrap: anywhere; white-space: normal;\">:rescuable<\/code>.<\/p>\n<p>Ordinary tests passed with positional <code style=\"overflow-wrap: anywhere; white-space: normal;\">serialize<\/code> and Parameters\/Hash equality warnings. <code style=\"overflow-wrap: anywhere; white-space: normal;\">STRICT_DEPRECATIONS=1 bin\/rails test<\/code> exposed four errors. Explicit <code style=\"overflow-wrap: anywhere; white-space: normal;\">coder: YAML, type: Hash<\/code> and permitted <code style=\"overflow-wrap: anywhere; white-space: normal;\">.to_h<\/code> restored the tested contracts. Ten settings then received separate commits and behavior checks. The final suite passed 15 tests and 32 assertions. All 28 defaults have decisions; deferred items keep <code style=\"overflow-wrap: anywhere; white-space: normal;\">load_defaults<\/code> at 7.0.<\/p>\n<p>A standalone probe showed a non-local transaction exit rolling back before its flag and committing afterward. Signed-message tests exposed a different limit. Rails 7.0 read the 7.1 writer&#8217;s retained format, but rejected its new <code style=\"overflow-wrap: anywhere; white-space: normal;\">:json_allow_marshal<\/code> output with <code style=\"overflow-wrap: anywhere; white-space: normal;\">TypeError<\/code>. The new reader accepted both. That changes the rollback options; it proves nothing about cookie or cache compatibility.<\/p>\n<p>Use <a href=\"https:\/\/github.com\/fastruby\/next_rails\" target=\"_blank\" rel=\"noopener\">next_rails<\/a> to maintain separate lockfiles, and test both bundles on each relevant commit. Its compatibility report and <a href=\"https:\/\/www.railsbump.org\/\" target=\"_blank\" rel=\"noopener\">RailsBump<\/a> narrow the dependency investigation. In the synthetic assessment, next_rails 1.7.0 reported no declared Rails 8.1 incompatibilities, but its Ruby 3.4 check flagged the locked sqlite3 1.7.3 arm64-darwin package. The checked RailsBump pages classify the selected Paperclip, rails-observers and factory_bot versions as dependency-compatible with 8.1. Neither report verifies the application&#8217;s money paths, callbacks or authorization behavior.<\/p>\n<p>Compare <code style=\"overflow-wrap: anywhere; white-space: normal;\">app:update<\/code> conflicts with local decisions and <a href=\"https:\/\/railsdiff.org\/\" target=\"_blank\" rel=\"noopener\">RailsDiff<\/a>. Keep a deprecation baseline that fails on newly exercised warnings. Run deterministic seeds before bisecting a failing change. <a href=\"https:\/\/github.com\/Shopify\/deprecation_toolkit\" target=\"_blank\" rel=\"noopener\">deprecation_toolkit<\/a>, <a href=\"https:\/\/github.com\/rubysec\/bundler-audit\" target=\"_blank\" rel=\"noopener\">bundler-audit<\/a> and <a href=\"https:\/\/brakemanscanner.org\/\" target=\"_blank\" rel=\"noopener\">Brakeman<\/a> answer different questions; a clean security scan does not verify the new defaults. Choose tool versions compatible with the rung; current deprecation_toolkit 2.4.0 needs Ruby 3.3 and Active Support 7 or later. FastRuby also published an <a href=\"https:\/\/www.fastruby.io\/blog\/open-source-claude-code-skill-for-rails-upgrades.html\" target=\"_blank\" rel=\"noopener\">open-source upgrade workflow on 27 March 2026<\/a>; this article does not evaluate that workflow.<\/p>\n<h2 id=\"give-every-dependency-a-verdict\" style=\"scroll-margin-top: 128px;\">Give every dependency a verdict<\/h2>\n<p>Record the locked version, target Rails\/Ruby, evidence URL, used capability, verdict, owner and blocking test. <strong>Upgrade<\/strong> a maintained dependency with verified constraints. <strong>Fork<\/strong> only when a necessary patch has a maintenance and security owner. <strong>Vendor<\/strong> small code only with its license and update obligations understood. <strong>Replace<\/strong> when a successor fits the behavior. <strong>Delete<\/strong> only after checking transitive dependencies and dynamically loaded paths as well as application references.<\/p>\n<div style=\"overflow-x: auto;\" tabindex=\"0\" role=\"region\" aria-label=\"Older dependencies and upgrade decisions\">\n<table style=\"width: 100%; min-width: 620px; table-layout: auto; border-collapse: collapse; font-size: 16px; line-height: 1.55;\" aria-label=\"Older dependencies and upgrade decisions\">\n<thead>\n<tr>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Older dependency<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Decision to investigate<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/thoughtbot\/paperclip\" target=\"_blank\" rel=\"noopener\">Paperclip<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Archived upstream. A maintained fork or Active Storage requires an explicit attachment\/data plan.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/rails\/protected_attributes\" target=\"_blank\" rel=\"noopener\">protected_attributes<\/a>, <a href=\"https:\/\/github.com\/rails\/activerecord-deprecated_finders\" target=\"_blank\" rel=\"noopener\">deprecated finders<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Remove reliance on APIs Rails no longer supports.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/rails\/rails-observers\" target=\"_blank\" rel=\"noopener\">rails-observers<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Extracted gem, not simply \u201cgone\u201d. Inspect callbacks and target-version tests.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/rails\/sass-rails\" target=\"_blank\" rel=\"noopener\">sass-rails<\/a>, <a href=\"https:\/\/github.com\/rails\/coffee-rails\" target=\"_blank\" rel=\"noopener\">coffee-rails<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Review the actual compiler\/build. An old generator default is not evidence of runtime removal.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/rubyjs\/therubyracer\" target=\"_blank\" rel=\"noopener\">therubyracer<\/a>, <a href=\"https:\/\/github.com\/thoughtbot\/capybara-webkit\" target=\"_blank\" rel=\"noopener\">capybara-webkit<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Compare the required JS runtime or browser behavior with maintained alternatives.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/thoughtbot\/factory_bot\" target=\"_blank\" rel=\"noopener\">factory_girl<\/a>, <a href=\"https:\/\/github.com\/CanCanCommunity\/cancancan\" target=\"_blank\" rel=\"noopener\">cancan<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Follow the rename\/successor and verify application configuration.<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\"><a href=\"https:\/\/github.com\/mislav\/will_paginate\" target=\"_blank\" rel=\"noopener\">will_paginate<\/a>, <a href=\"https:\/\/github.com\/rubysherpas\/paranoia\" target=\"_blank\" rel=\"noopener\">paranoia<\/a><\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Pagy and Discard are choices, not mandatory drop-in replacements.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p><a href=\"https:\/\/github.com\/heartcombo\/devise\/blob\/v5.0.4\/devise.gemspec\" target=\"_blank\" rel=\"noopener\">Devise 5.0.4<\/a> requires Railties 7+, <a href=\"https:\/\/github.com\/rspec\/rspec-rails\/blob\/v8.0.4\/rspec-rails.gemspec\" target=\"_blank\" rel=\"noopener\">RSpec Rails 8.0.4<\/a> requires Rails components 7.2+, and <a href=\"https:\/\/github.com\/sidekiq\/sidekiq\/blob\/v8.1.7\/sidekiq.gemspec\" target=\"_blank\" rel=\"noopener\">Sidekiq 8.1.7<\/a> requires Rack 3.2+. <a href=\"https:\/\/github.com\/activeadmin\/activeadmin\/blob\/v3.5.2\/activeadmin.gemspec\" target=\"_blank\" rel=\"noopener\">ActiveAdmin 3.5.2<\/a> still depends on jquery-rails. These are exact-release constraints, not universal upgrade minimums. The downloadable dependency worksheet links their own metadata and records scope. Forking before checking a maintained release, or deleting a transitive dependency because a search found no constant, creates avoidable work.<\/p>\n<h2 id=\"check-loading-assets-and-data-separately\" style=\"scroll-margin-top: 128px;\">Check loading, assets and data separately<\/h2>\n<p>The initial ordinary suite passed five tests and nine assertions. <code style=\"overflow-wrap: anywhere; white-space: normal;\">bin\/rails zeitwerk:check<\/code> then reported that <code style=\"overflow-wrap: anywhere; white-space: normal;\">app\/services\/price_quote.rb<\/code> should define <code style=\"overflow-wrap: anywhere; white-space: normal;\">PriceQuote<\/code>; the file defined <code style=\"overflow-wrap: anywhere; white-space: normal;\">PriceQuotes<\/code>. Renaming the constant and adding a service-load test made the check pass. The green suite had only covered paths it loaded.<\/p>\n<p>Review acronyms, custom autoload roots, STI loading and initializer references separately. Reloadable constants may belong in <code style=\"overflow-wrap: anywhere; white-space: normal;\">to_prepare<\/code>; removing every <code style=\"overflow-wrap: anywhere; white-space: normal;\">require_dependency<\/code> blindly can hide engine-loading assumptions. The <a href=\"https:\/\/guides.rubyonrails.org\/v7.0\/classic_to_zeitwerk_howto.html\" target=\"_blank\" rel=\"noopener\">Zeitwerk migration guide<\/a> explains these cases.<\/p>\n<p>Retaining Sprockets means declaring its integration and, for Sprockets 4, keeping the required manifest. Retaining Webpacker needs its own maintenance decision. A later Turbo migration has event names, attributes, navigation and form behavior to test. Combining those changes with the framework makes a broken page harder to diagnose.<\/p>\n<p>Preserve historical migration semantics. Adding the appropriate compatibility superclass is different from rewriting old DDL. Test fresh database creation from the maintained schema. Choose <code style=\"overflow-wrap: anywhere; white-space: normal;\">structure.sql<\/code> when database-specific features require it, not because a Rails number automatically forces it. Bigint keys, timestamp nullability and datetime precision depend on the migration version and adapter.<\/p>\n<p>In the executed 8.0.5.1-to-8.1.3.1 SQLite rehearsal of the <a href=\"https:\/\/github.com\/rails\/rails\/blob\/v8.1.3.1\/activerecord\/lib\/active_record\/schema_dumper.rb\" target=\"_blank\" rel=\"noopener\">schema-dumper change<\/a>, the next migration had no application DDL. The resulting dump moved <code style=\"overflow-wrap: anywhere; white-space: normal;\">zeta<\/code> below <code style=\"overflow-wrap: anywhere; white-space: normal;\">updated_at<\/code>; physical columns, the check constraint and the stored row were unchanged. The schema header and migration version also changed. Review those separately before treating a large diff as formatting.<\/p>\n<div style=\"overflow-x: auto;\" tabindex=\"0\" role=\"region\" aria-label=\"Rollback evidence catalogue\">\n<table style=\"width: 100%; min-width: 620px; table-layout: auto; border-collapse: collapse; font-size: 16px; line-height: 1.55;\" aria-label=\"Rollback evidence catalogue\">\n<thead>\n<tr>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Rollback record<\/th>\n<th style=\"padding: 14px; border: 1px solid #d3dfdb; background: #edf4f0; text-align: left; vertical-align: top;\" scope=\"col\">Evidence needed before enabling it<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Cookie, message or cache writer<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Old process reads new output; new process reads retained data; expiry\/rotation policy<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Column conversion or destructive DDL<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Recoverable copy, concurrent-write behavior, restore procedure and owner<\/td>\n<\/tr>\n<tr>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Runtime or attachment change<\/td>\n<td style=\"padding: 14px; border: 1px solid #d3dfdb; text-align: left; vertical-align: top; overflow-wrap: anywhere;\">Exact old\/new readers and native dependencies tested against representative data<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>This catalogue makes rollback a tested property. Keeping old defaults alone does not guarantee that redeploying old code can read everything a new process has written.<\/p>\n<h2 id=\"when-the-next-step-should-be-something-else\" style=\"scroll-margin-top: 128px;\">When the next step should be something else<\/h2>\n<p>An application nobody can deploy needs a reproducible build, repository access, configuration and a restore rehearsal before a framework change. Without those, even returning to the current release is uncertain. If authorization, billing or ledger paths have no useful tests, first capture those behaviors. More framework work cannot supply the missing evidence.<\/p>\n<p>A funded replacement can justify a narrower maintenance plan when its retirement date, data migration, integrations and owner are credible. \u201cWe will replace it next year\u201d does not make unsupported software safe. Compare a scoped rebuild with the ladder using the behavior still required, identity, historical data and integration contracts. A smaller screen count alone is not a smaller migration.<\/p>\n<p>JRuby needs its own runtime, adapter and dependency compatibility assessment. A frontend-only goal needs a frontend plan. A hosting move needs a separate data\/configuration runbook. None becomes part of the Rails hop simply because an upgrade branch exists.<\/p>\n<h2 id=\"what-to-do-this-week\" style=\"scroll-margin-top: 128px;\">What to do this week<\/h2>\n<p>Choose one business-critical flow and capture its current results. Produce the gem verdict sheet and prove the patched baseline can deploy and recover. Then open the next-minor branch with both lockfiles, retained defaults and a deprecation check that can fail.<\/p>\n<p>Rails <a href=\"https:\/\/rubyonrails.org\/2005\/12\/13\/rails-1-0-party-like-its-one-oh-oh\" target=\"_blank\" rel=\"noopener\">1.0 arrived in December 2005<\/a>; <a href=\"https:\/\/rubyonrails.org\/2007\/12\/7\/rails-2-0-it-s-done\" target=\"_blank\" rel=\"noopener\">2.0 in 2007<\/a> strengthened REST conventions; <a href=\"https:\/\/guides.rubyonrails.org\/3_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">3.0 in 2010<\/a> brought the Merb integration, Bundler and Arel; <a href=\"https:\/\/guides.rubyonrails.org\/4_0_release_notes.html\" target=\"_blank\" rel=\"noopener\">4.0 in 2013<\/a> brought strong parameters and Turbolinks. That history matters because application assembly changed alongside framework APIs. The ladder separates those accumulated decisions so each can be explained and tested.<\/p>\n<p><a href=\"https:\/\/www.allerin.com\/downloads\/rails\/rails-upgrade-ladder-checks.zip\">Download the upgrade rehearsal and worksheets<\/a>. Source, lockfiles, limitations and a reproducible commit history are included without a form. The <a href=\"https:\/\/rubyonrails.org\/maintenance\" target=\"_blank\" rel=\"noopener\">Rails policy<\/a> currently lists 7 November 2026 as the end of 8.0 security fixes. The <a href=\"https:\/\/www.allerin.com\/blog\/rails-support-calendar-explained\/\">support calendar<\/a> explains the destination decision. Recheck both when planning the actual rollout.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Work through the Rails upgrade ladder with retained defaults, a tested 7.0 to 7.1 hop, a gem verdict sheet and explicit reader compatibility checks.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","_links_to":"","_links_to_target":""},"categories":[2037],"tags":[2041,2073,2038,2045,2072],"class_list":["post-14886","post","type-post","status-publish","format-standard","hentry","category-ruby-on-rails","tag-active-record","tag-rails-defaults","tag-rails-upgrades","tag-ruby-versions","tag-zeitwerk"],"_links":{"self":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14886","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/comments?post=14886"}],"version-history":[{"count":2,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14886\/revisions"}],"predecessor-version":[{"id":14894,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/posts\/14886\/revisions\/14894"}],"wp:attachment":[{"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/media?parent=14886"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/categories?post=14886"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.allerin.com\/blog\/wp-json\/wp\/v2\/tags?post=14886"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}