DEV Community

Cover image for rails-preflight: Find Rails Upgrade Blockers Before You Start
Syed Aslam
Syed Aslam

Posted on Originally published at syedaslam.com

rails-preflight: Find Rails Upgrade Blockers Before You Start

When upgrading an old Rails app, I want to know what will block the upgrade before I start changing versions.

I built rails-preflight to find those blockers by reading the app's files, without booting it.

Before releasing it, I ran it against real Rails upgrades in Mastodon, Discourse and Forem. All 28 gems it flagged as blockers were upgraded, replaced or dropped in the release that moved Rails.

rails-preflight reads a Rails app and tells you what stands between it and the Rails version you want: gems that won't install, APIs your code still calls that Rails removed, the Ruby each step needs, and what it couldn't check.

gem install rails_preflight
rails-preflight 7.2 /path/to/your/app
Enter fullscreen mode Exit fullscreen mode

It writes an HTML report into the app's directory without running the app or its tests, or modifying existing application files.

The rails-preflight report for Mastodon v3.3.0, Rails 5.2.4.4 to 7.0: 3 steps, 13 blockers, 9 of them gems, with the route through 6.0, 6.1 and 7.0 and a plan table
Mastodon v3.3.0, checked for Rails 7.0. (The full report.)

What it reads

The lockfile, .ruby-version, the Dockerfile, db/schema.rb and the app's config. Then every .rb, .erb, .haml, .slim and .rake file under app, config, db, lib, test and spec, against 118 rules I took from the Rails release notes for 5.0 to 8.1.

Each rule links back to the release note it came from. If the report says something is gone, you can check it in one click.

The report is a route. One step per Rails minor, since that's how Rails recommends upgrading, and each step lists its blockers, the Ruby range it needs, and a rough effort: low, medium or high.

Never hours. I don't know your team, and an estimate that pretends to is worse than no estimate.

Gems are where upgrades stall first

On most upgrades I've worked on, the first wall was a gem.

Bundler already knows when a gem caps Rails in its gemspec. rails-preflight reads those caps from the lockfile for every step of the route, so on Mastodon 3.3 you learn that devise-two-factor 3.1.0 stops at Rails 6.0 before you've started the 6.1 step.

The harder ones never declare a limit anywhere Bundler can see:

  • bullet raises an error on any Active Record minor it doesn't recognize. Version 7.1.6 boots fine on Rails 7.1 and refuses to load on 7.2.
  • Rails checks some gems itself when it loads a feature: the database adapters, listen for the evented file watcher, redis for Action Cable. An old pg installs fine and then fails at boot.
  • Some gems fall out of the bundle. Rails 7.0 stopped depending on sprockets-rails, so an app that got it only through Rails and still uses the asset pipeline breaks on boot.

These come from reading the Rails source on every X-Y-stable branch from 5.0 to 8.1, and from diffing the lockfiles of real upgrades to see which gems moved with Rails.

Checking it against upgrades that already happened

My unit tests checked examples I had chosen. I also needed to see what the rules flagged in real applications.

And a tool like this loses your trust on its first false alarm.

So before releasing it, I ran it on open-source apps whose upgrades already happened, where the right answer is public.

For Mastodon, Discourse and Forem, I took the release just before each Rails upgrade and asked rails-preflight about the Rails version the next release moved to. Then I read what the next release actually changed.

App Rails upgrades Gem blockers flagged Upgraded, replaced or dropped
Mastodon 5.1 to 8.1 17 17
Discourse 5.1 to 8.1 8 8
Forem 5.1 to 8.0 3 3

Every gem it called a blocker was upgraded, replaced or dropped in the release that moved Rails. (One Mastodon gem, nsa, moved to a GitHub fork.) In Mastodon, every removed API it flagged was gone in the next release as well.

It also found code that was already broken. Discourse still had render nothing: true, removed in Rails 5.1. Forem had render text: in an API controller, gone since 5.1, and ActiveRecord::Base.timestamped_migrations in a generator, gone since 7.1.

That's dead code, or code waiting to fail the first time someone runs it.

What it got wrong

Plenty, at first.

On Mastodon it flagged Chewy's tokenizer: strings as a removed Rails option, a service's own destroy_all(x) method, and an app method that happened to be called image_alt.

On Discourse it read a local variable named errors as ActiveModel::Errors, again and again, and did the same with a local called update_attributes. Fixing those dropped 128 false hits across the apps.

The worst one came from Coursemology, a Slim app: 85 "blockers" for div_for on the way to Rails 6.0. Rails 5.0 moved div_for into the record_tag_helper gem, and Coursemology had that gem locked the whole time. It still does, on Rails 8.1. My rule had the wrong version and didn't know the gem existed. It now checks for the gem, and those 85 blockers are 0.

Each fix came with a test that fails if the mistake comes back.

What it can't see

Some of an upgrade leaves no line to match. New framework defaults, changed query behavior, anything that only shows up at runtime.

It reads one line at a time, so a call split across lines slips past. It can't tell how much of your app the tests cover. And it can name your private gems but can't check them.

The report ends with a section for exactly these, with what to do instead. Read a clean step as "nothing found".

Where it fits

next_rails and RailsBump tell you which gem versions work with a Rails version. rubocop-rails autofixes a lot of deprecations. A coding agent can find deprecated calls if you ask.

rails-preflight comes before all of them. It gives you an initial upgrade plan, on one page, before anyone starts: your team, a consultant, or an agent.

For the agent case there's a Markdown format, a checklist per step with file:line under each finding:

rails-preflight --format markdown --stdout 7.2 /path/to/your/app
Enter fullscreen mode Exit fullscreen mode

And for CI, JSON plus an exit code:

rails-preflight --offline --format json --fail-on blockers 7.2
Enter fullscreen mode Exit fullscreen mode

Try it

It runs on Ruby 2.7 or newer, and the app it reads can be on any Ruby, including the old one your servers still run. It knows Rails 5.0 to 8.1.

--offline keeps it fully static. Without it, it asks rubygems.org about public gems only, never your private ones.

The code is on GitHub. If it gets something wrong on your app, open an issue with the finding and the line. Every fix above started as a finding like that.

What blocked your last Rails upgrade: a gem, a removed API, or something no tool could have seen? I'd like to know which rules to add next.


This post first appeared on syedaslam.com. It's part of a series on long-lived Rails systems, which started with the idea behind it.

Top comments (0)