Here's a sample plan that was provided for a recent, successful Rails upgrade.
We start these proposals by capturing a snapshot of the current state of the system.
The application is deployed on the Heroku-16 stack, which reached end-of-life on May 1st, 2021. It's no longer possible to deploy code changes to applications running on this stack. Getting to a supported stack should be the highest priority. This requires upgrading to Ruby 2.5.9.
The application depends on a large number of stale dependencies, some of which have been abandoned and do not support newer versions of Rails. We'd use the
metric, which is a measure of how up to date your dependencies are. It essentially sums [date of most recent release - date when the version you're using came out] for each of the libraries your app uses. Your application has a libyear of 496. Anything over 100 is extremely concerning.
Rails 5.0 hasn't received support or security updates since April 2018. Upgrading Rails versions below 5.2 is significantly more difficult than recent upgrades.
About half of the controllers and 2/3 of the models have no test coverage. While we'll do manual testing of a few specific critical flows that you provide testing instructions for, we can't guarantee that untested code paths will upgraded properly.
lib/ext contains a bunch of extensions to core Ruby classes. This metaprogramming makes upgrading riskier.
The dockerfile runs on debian jessie, which was released in 2015. It will receive security updates until 2015, but it's not a good base to be developing in. For example, there are no official ruby 3 docker images targeting debian jessie.
explains new major features. We'll go through every item in the upgrade guide and check the application.
The right way to upgrade Rails is to go minor version by minor version and to make as many small backwards compatible PRs as possible to reduce risk. Rails releases a new minor or major version each year. You're currently on 5.0, so to get to the latest Rails we need to upgrade to 5.1, 5.2, 6.0, 6.1, and finally 7.0. We'll follow the following series of steps for each version.
Upgrade Ruby to Latest Compatible Version
We'll start each Rails version upgrade by first upgrading Ruby to the compatible version. For Rails 5.0 that's Ruby 2.4.
"Dual boot" is the ability to switch between different versions of your application dependencies without changing branches. The big benefit is that you can test the same code under both versions. This allows us to make individual small PRs against master that are tested against the next version of Rails (so we know they work) and also pass against the current version (so they can be merged immediately). This reduces the need for long running branches and big diffs.
that I'd encourage anyone working on the app to read.
We dual boot via a shell script which swaps between two different "Gemfile.lock" files built off of a single Gemfile. You write something like this in your Gemfile:
gem 'rails', '~> 6.0'
gem 'rails', '~> 5.2'
Then you use a shell script which we add to the repo:
$ rspec # current behavior
$ script/next.sh rspec # new behavior
Your Gemfile.lock is untouched, which reduces the chance of messing something up in production. We'll introduce a new Gemfile.next.lock which tracks the dependency versions for the upgraded app.
There are other ways to dual boot (like Shopify's bootboot bundler plugin), but our recommendation is the above.
We'll address all deprecation warnings thrown by the application. There are a number of deprecation warnings thrown when running the test suite. Once we're able to deploy to Heroku we'll also hook in to the runtime deprecation logging to log deprecations in production that might occur in untested codepaths. This is implemented through the Rails notification subsystem.
Next we get the current version of Rails running with dependencies upgraded such that bundle update rails targeting the next version succeeds.
The following gems need to be upgraded to get to 5.1 (~15 more need to be upgraded on the way to 7.0):
The following gems are no longer maintained and need to be migrated away from:
paperclip (should likely migrate to kt-paperclip)
test_after_commit (replace with built-in Rails functionality)
Test and Deploy
Pass Test Suite under Both Versions
The safest upgrade path is to make small backwards compatible fixes until we can pass the test suite under both versions simultaneously. Then the upgrade deployment is as simple as bumping the rails version in the Gemfile.
Review Application Manually
Along the way we'll keep a log of changes we make to application code. We'll then review the entire codebase to try to find untested code paths that hit the same breakage.
Deploy the Upgrade
Do the upgrade! Assuming we succeed in our series of small PRs the only diff in this deployment will be versions in the Gemfile/.lock and new framework defaults.
Remove Compatibility Code
Sometimes it's impossible to support two versions of Rails with the exact same code. There might be code like this sprinkled through the app:
if Rails::VERSION::MAJOR == 6
We'll remove this code once the upgrade is successful.
Review New Framework Defaults
Each version of Rails changes some of the default configs and adds new ones. The initial upgrade will retain the former version's defaults for safety.
Once we're on the new version we'll consider each of the new defaults and take the good ones.
Repeat until Rails 7.0
Redo all these steps to move to 5.2, 6.0, 6.1, and 7.0.