Ruby on Rails

Rails 7.2 to 8.0 without replacing your stack

A Rails 7.2 to 8.0 upgrade should preserve the parts of an application that already work while making the required code and dependency changes visible. Treating every Rails 8 announcement as another migration turns a framework upgrade into several projects sharing one failure log.

Rails 7.2 left its published security-support window on 9 August 2026; Rails 8.0’s window ends 7 November 2026. Use 8.0 as a tested checkpoint on the way to 8.1. The maintenance schedule makes that destination a planning requirement, not a reason to combine every change in one deployment.

For this article, an AI-assisted experiment generated a Rails 7.0.10 application and carried it through 7.1.6, 7.2.3.2 and 8.0.5.1. It uses Sprockets, Sidekiq, a Redis cache, Devise and Rack::Attack. This is a small historical fixture with deliberate upgrade cases, not an Allerin client application or a simulation of years of production traffic.

Its source-version baseline passed 13 tests and 45 assertions. The final suite passed 15 tests and 47 assertions after adding malformed-request cases. A real Sidekiq worker also processed a queued shipment and persisted its new status.

Separate the framework change from the defaults

The Rails version in Gemfile.lock and the version passed to config.load_defaults control different things. Updating the bundle changes available APIs and gem requirements. Advancing load_defaults changes a collection of behaviors. Existing applications can take those decisions in separate commits, following the framework-defaults upgrade process.

The first column records changes observed in this fixture. The other columns separate optional adoption from work that would obscure this hop’s results.

changes for you does not change unless you opt in do not do during this hop
The old enum declaration raises ArgumentError on 8.0. Pass the enum name positionally and preserve its numeric mapping. Existing Sprockets can remain when its integration and processors are compatible. Propshaft is the new-app default. Replace the asset pipeline merely to match a newly generated application.
The pinned sqlite3 1.7.3 gem prevents the 8.0 SQLite adapter from loading. This fixture needed a gem satisfying >= 2.1. Sidekiq and the explicit Redis cache configuration remain in place. Solid backends require a separate adoption. Combine queue storage, cache storage and framework changes in one deployment.
Loading 8.0 defaults changes the fixture’s timeout, time-conversion and HTTP-freshness checks. The same Rails bundle passes with 7.2 defaults. require and permit remain available. expect changes request-shape handling when you adopt it. Bulk-convert optional parameter roots without testing missing and malformed input.
app:update proposes changes to locally configured files. Its proposed application configuration omitted the fixture’s explicit queue, cache and time-zone settings. Kamal, Thruster and the authentication generator are optional choices for an existing application. Replace deployment or Devise because Rails 8 offers alternatives.

Run this before you branch

  • Record the exact Ruby, Rails and dependency versions, the database adapter and server, and the effective defaults, job adapter and cache store.
  • Run the suite on the source version and preserve its warnings. Cover sign-in, failed sign-in, sign-out and password reset alongside your application’s authorization behavior.
  • Exercise a real queue and cache in an isolated environment. A fake job adapter or memory cache cannot establish that the deployed backend still works.
  • Precompile the actual asset manifests and load a page that references the compiled assets.
  • Check dependency advisories and resolve necessary gem updates in separately tested commits where both Rails versions permit them.

Ruby stayed at 3.3.12 throughout. Rails 8’s gemspec requires Ruby 3.2 or later, but Ruby 3.2 reached end of life on 1 April 2026. A minimum dependency version is not a current runtime recommendation. A move to Ruby 3.4 should have its own checks.

Check Devise reset-token continuity

Devise moved from 4.9.4 to 5.0.4 in a separately tested commit on Rails 7.2. Its changelog records Rails 8 integration work and changes to secret selection; current advisories also rule out presenting the old fixture pin as deployment advice. Keeping Devise means preserving the authentication architecture while reviewing its dependency changes.

The cross-version reset test found the reason for that review. Devise 4 used the fixture’s credentials-derived key; Devise 5 used the Rails application key. Supplying the same Rails environment secret did not make those effective Devise keys equal, and the old reset token was rejected.

In a controlled after-upgrade copy, preserving the earlier effective Devise key allowed that same token to reset the password once; reuse was rejected. This is conditional on the application’s key sources, not a failure every Devise upgrade will encounter. Record the effective key’s continuity without printing it.

Read app:update conflicts as changes to application behavior

After the targeted bundle update, run the generator and inspect the resulting diff.

bin/rails app:update
git diff -- config bin public
bin/rails test

The recorded Rails 8 update had 16 conflicting paths. Its proposed config/application.rb retained load_defaults 7.2, but omitted the fixture’s explicit Sidekiq adapter, Redis cache and New York time zone. Accepting that file wholesale would have removed application decisions unrelated to the framework version.

The final fixture kept eight custom configuration files and accepted eight reviewed files, including expanded parameter filtering, setup code and generated public pages. The earlier 7.1 step also changed test exception handling to :rescuable. Before/proposed copies and the final diff are included in the evidence, so those choices can be inspected.

Run the suite separately to collect deprecations. The update command silences Rails deprecators while it runs.

Fix enum definitions while the earlier release can warn

The fixture began with this declaration.

enum status: { queued: 0, processed: 1 }, _prefix: true

Rails 7.2 emitted its enum deprecation warning. Rails 8.0 raised ArgumentError: wrong number of arguments (given 0, expected 1..2) when the model loaded. This replacement passed the stored-value, predicate and scope checks.

enum :status, { queued: 0, processed: 1 }, prefix: true

The positional form arrived in Rails 7.0, so this repair can precede the Rails 8 deployment. Keep queued mapped to 0 and processed to 1; changing those values would reinterpret stored rows. Rails 8 still accepts keyword labels after the positional name and keyword options. A blanket instruction to remove all enum keyword arguments would be wrong.

Test the defaults that can change an otherwise green suite

Changing only config.load_defaults 7.2 to 8.0 produced three failing assertions. Those failures exposed different behavior, not three framework defects. The fixture then adopted the new behavior deliberately and recorded the revised expectations.

Ruby added Regexp.timeout in 3.2, released in 2022. Rails 8’s defaults implementation sets it to one second when the API exists and its value is nil. The fixture changed from nil to 1.0. An independent check confirmed that an explicit 0.25 remained unchanged. Test the patterns your application runs; the global setting alone does not measure their cost.

The time-conversion change preserves the zone instead of just its current UTC offset. In the fixture, a January value in America/New_York was converted with to_time, then advanced by 180 days. Retained defaults kept the offset at -05:00; 8.0 defaults produced -04:00 in summer. That is useful daylight-saving behavior, but code expecting a fixed offset needs attention. No stored database timestamp was rewritten.

The 8.0 defaults initializer also explains strict freshness. With both conditional headers present, If-None-Match takes precedence. A matching ETag and older If-Modified-Since previously entered the fixture controller’s render branch; strict freshness avoided it. Both full responses were 304 because Rack’s conditional middleware also participated. A status-only assertion would have missed the extra rendering work, so the test records a header set inside that branch.

params.expect is a separate request-handling change

GitHub’s March 2012 incident report described insufficient filtering of incoming attributes. Rails’ proposal that month moved mass-assignment filtering toward controllers; strong parameters shipped with Rails 4.0 in 2013. The point was to decide which attributes a particular request may set.

Rails 8’s expect change also checks the declared parameter shape. The fixture first retained this working filter.

params.require(:shipment).permit(:status)

It then adopted this version in a separate commit.

params.expect(shipment: [:status])

A valid shipment hash still returned 201. A scalar or array in place of that hash previously raised NoMethodError in the test environment; after the change each returned 400. An absent root returned 400 both before and after. That is a specific request-handling improvement, not a prerequisite for booting Rails 8.

Review other shapes individually. The tagged API uses double arrays for an array of parameter hashes. It requires the declared root, not every permitted field inside it. Replacing an optional fetch(:filters, {}).permit(...) with expect would also reject an absent filters root. The fixture exercised the shipment cases; it does not certify every controller conversion.

The asset and job backends have separate histories

Sprockets entered Rails 3.1 in 2011, bringing asset organization and processing into the framework. Rails 5.1 in 2017 offered Webpacker alongside that pipeline. Rails 7.0 in 2021 made browser import maps a default JavaScript path without requiring a Node build. These steps left applications with different manifests, processors and JavaScript dependencies.

The Rails 8 announcement in 2024 made Propshaft the new default while explicitly retaining Sprockets support for existing applications. That supports keeping a compatible Sprockets setup during this hop. It does not establish that an old processor, removed gem API or untested manifest will still compile.

Here, Sprockets Rails 3.5.2 compiled the assets on both ends of the upgrade. The final production Rack stack returned the fingerprinted CSS with status 200, the expected content type and a body matching the compiled file.

Active Job arrived in Rails 4.2 in 2014 with a common interface to job backends. Rails 5.2 in 2018 added its built-in Redis cache store. Rails 8’s database-backed Solid defaults pursue a different operational arrangement with fewer separate services. A team already operating Sidekiq and Redis must still account for retry behavior, queue delivery, cache eviction and connection capacity before changing those backends. Preserving them through the framework hop makes those later comparisons easier to interpret.

What to do this week

  1. Establish a passing source-version baseline, then make the required dependency changes visible before the Rails bump.
  2. Fix removed APIs and review app:update conflicts. Keep your queue, cache, assets, authentication and deployment configuration explicit.
  3. Enable the reviewed 8.0 defaults with tests for the behaviors your application uses. Retain the old/default comparisons in the upgrade record.
  4. Rehearse deployment and rollback with your own stored data and worker processes. This sample does not test your application’s migrations or a mixed-version rollout.
  5. Continue through the 8.0 to 8.1 upgrade path. The checkpoint is useful only if the supported destination is part of the work.

The fixture download provides source, lockfiles, recorded changes and reproduction instructions without registration. Research and execution were checked on 7 September 2026, using released tags. The environment used Ruby 3.3.12, Sidekiq 7.3.10, Redis client 5.4.1 and Redis server 8.0.2. Tests covered selected authentication flows, Redis cache reads and writes with a positive TTL, actual throttling, plus the behaviors discussed here. They did not establish retry guarantees, a production security audit or mixed-version deployment safety.

Allerin’s Rails upgrade work starts with the code, runtime and behavior the application actually has.