Planning a Rails version upgrade (without burning a quarter)

Rob Bazinet, founder of RailsHealth.

By Rob Bazinet

· 6 min read

The thing that determines whether a Rails upgrade is a two-week project or a two-quarter project is almost never the Rails version itself. It’s the state of the codebase that’s about to receive it.

I’ve done this in both directions. I’ve moved an app from Rails 5.2 to 7.1 over a long weekend because the team had kept the codebase in good shape. I’ve watched a 6.0 → 7.0 upgrade take six months because nobody had touched the test suite in two years and we couldn’t tell if any individual change had broken anything. Same delta on paper; entirely different reality.

Here’s the work that lives upstream of bundle update rails.

Don’t start with the Rails version

Before you change a single line in the Gemfile, answer these questions:

  • How many gems in our Gemfile are unmaintained? Run bundle outdated. For each gem that’s significantly behind, check the upstream repo. Last commit in 2019 is a signal. “Looking for maintainer” in the README is a signal. If you depend on something that won’t follow Rails forward, the Rails upgrade has to bring that gem along too — or replace it — and that’s where the timeline blows up.
  • Does CI run on every push, and does it pass? Not “did it pass last month.” Right now, on main. If CI is broken, fix CI first. If you’re going to introduce a breaking change to the framework underneath you, you need a working feedback loop, and that loop is CI.
  • What’s the test coverage on the parts of the app the upgrade is most likely to break? ActionView template behavior, Active Record callbacks and validations, controller authentication, anything that uses ActiveSupport::Notifications or instrumentation. If those areas have thin tests, your upgrade is going to surface bugs in production that the test suite couldn’t catch.
  • Are there any gems that pin the current Rails version exactly? Search the Gemfile.lock for rails (= or rails (~> constraints in non-Rails gems. If you find any, those gems decide your timeline.

Most failed Rails upgrades I’ve been around were predictable from this list before they started. The upgrade itself isn’t the hard part; the surrounding ecosystem is.

The dual-boot pattern

If you’re going from one major Rails version to the next, set up dual-boot before you start changing code. The pattern in short: your Gemfile reads an env variable (usually NEXT) and conditionally loads the new Rails version. Both versions resolve in the same repo; CI runs the suite under both.

ruby # Gemfile rails_version = ENV["NEXT"] ? "~> 7.1.0" : "~> 7.0.0" gem "rails", rails_version

You’ll have two lockfiles (Gemfile.lock and Gemfile_next.lock). Some gems will need conditional version constraints. The setup is annoying for half a day; after that, every change you make can be tested against the current Rails version (so prod is safe) and the next Rails version (so you know if you’ve fixed or broken the upgrade). You ship deprecation fixes incrementally, on main, with no long-lived branch.

When CI is green under both Rails versions and you’ve worked through the production deprecation warnings, the actual cutover is one PR that flips the env variable to default and deletes the dual-boot scaffolding.

The dual-boot approach was popularized by discourse_rails_upgrade.md and Shopify’s engineering team has written about it; if you’ve never set it up, it’s worth learning before your next major version.

Major versions vs. minor versions

A Rails minor version bump (7.0 → 7.1, say) usually takes a day. Read the release notes once, run bin/rails app:update and accept the changes you want, run the test suite, fix the deprecation warnings, ship. Don’t overthink it.

A Rails major version bump (6.x → 7.0, 7.x → 8.0) is a project. The release notes are longer, the deprecations more substantive, and the gem ecosystem hasn’t fully caught up on day one of the major. Wait at least one patch release (x.0.1) before upgrading production — sometimes a major has a regression that gets fixed in x.0.1 and you don’t want to be the one who finds it.

When you skip majors — 5.2 → 7.x, say — don’t. Go through each major in sequence. 5.2 → 6.0 → 6.1 → 7.0 → 7.1. The pain compounds non-linearly if you try to do them all at once; the deprecations from each major are designed to be triaged in sequence. Skip a major and you’re triaging two majors’ worth of deprecations simultaneously, with no way to tell which one introduced which issue.

The exception is if you’re so far behind that some of the intermediate versions are themselves unsupported (Rails 5.0 → current, for instance). In that case, you’re not really doing a normal upgrade; you’re doing a migration, and the rules are different.

When to pay someone

There are firms whose entire business is Rails upgrades: FastRuby.io is the most well-known, OmbuLabs is the same group, Planet Argon does it among other things. They’re worth paying when:

  • You’re on an unsupported Rails major and you need to be on a supported one by a specific date (audit, compliance, customer requirement).
  • The codebase has unusual architectural choices — heavy use of engines, a hand-rolled ORM layer, a custom asset pipeline — and the upgrade interacts with them in ways that aren’t covered in the upgrade guide.
  • The team that built the app is no longer at the company, and nobody internal has enough context to know which parts of the codebase are load-bearing.

They’re not worth paying when you’re on a supported major and the team has the time. The work is well-understood, the upgrade guides are good, and you’ll learn things by doing it yourself that you won’t learn by reading someone else’s report.

What does RailsHealth do here

RailsHealth tells you which Rails and Ruby versions the app is on, whether they’re supported, and what the end-of-support dates are — i.e., when this becomes a billable emergency. It also flags gems with known CVEs that haven’t been patched in the version range you’re on, which is one of the more common upgrade-driving forces. It does not do the upgrade. Nobody should — there’s enough judgment involved that you don’t want it on autopilot.

What it gets you is the input to this conversation, with no time spent collecting the data manually. The actual planning work is still yours; it’s the part where the answer matters most.

Try RailsHealth

14-day trial. No credit card to start. Read-only GitHub access — we never run your code.

Connect your GitHub repo

Related guides

Feedback