How I Upgrade Ruby on Rails

Published: December 20, 2020
Another year, another major Rails release. Rails 6.1 looks to be a solid one. Though, despite all the new features and fixes a new Rails version brings, there is always one looming problem for developers - the upgrade.

I have upgraded many Rails apps over the years. Some upgrades went easier than others. Sometimes whole features had to be rewritten. Gems were forked and patched. Temporary monkey-patching sprinkled in here and there were occasionally needed. While Rails upgrades are much less painless than they were back in the 2.x to 3.x days, there is still bumps along the way you should know about.

There is no standard path to the upgrading, but after going through "the process" many times over the years through various Rails releases, I have come up with a series of steps that I use when I upgrade an app. This can be applied to a single person side project or a monolithic app being worked on by a team. Your miles may vary; however, this process seems to work well for me.

This process assumes your application has a decent amount of test coverage. Good test coverage helps ensure a level of confidence that the app code works with the updated version of Rails. Without this barometer you will only have "randomly click around in the app locally" to test things. For simple and small apps, this might be ok. But if your app has any significant traffic and/or a long list of features, you really should have some automated test coverage.

If you are tasked with upgrading through multiple versions of Rails (for example Rails 5.1 straight to 6.1), it is certainly possible to do the following process in one go. However, I recommend stepping through each version of Rails and upgrading through those versions first. Meaning first upgrade from 5.1 to 5.2. Then 5.2 to 6.0. Finally, 6.0 to 6.1. To do that, you can follow the steps below in order, but loop through steps 4 through 8 for each major version of Rails.

Step 1: Consult the Upgrade Guide

Rails provides an official upgrade guide for every major release.

This document contains invaluable information on some of the high-level changes, depreciations, additions, and removals for each major release of the framework. For the version of Rails you are upgrading to, consult that version's section and get a good idea of what needs to change in your app or what may break.

Step 2: Update Your Current Rails to the Latest Minor Version

The process will go a bit smoother if the Rails version of your app is at least on the latest minor version. Meaning if your app is currently on Rails 6.0, it should be specifically 6.0.3.4. which is (at the time of this writing) the most recent version of Rails 6.0. If not, then this is the first order of business. Update the version, commit, and deploy.

Step 3: Audit Gems and Update/Remove Them

One of the biggest blockers for a Rails upgrade are third party dependencies (aka, gems). Start by opening your Gemfile. With the exception of the actual Rails gem, the database driver gem, and any of the other gems that are included by default with a Rails app (such as sass-rails, listen, webdrivers, etc) go through each entry in the file and ask yourself these questions:

  1. What is this gem used for?
  2. Does the app really need this gem?
  3. Is this the latest version?
  4. Is this gem compatible with the Rails version I wish to upgrade to?

What is this gem used for?

It is important that you understand what your app does. It is also important that you understand how things work internally. If you see bugsnag in your Gemfile and you do not know what it is for, now is a good time as ever to learn. The more you know about the dependencies of your app, the more you will understand about the capabilities and features you can create. This may also be a good time to leave a comment next to the gem to help remind future you (and the rest of your team) what it does.

Does the app really need this gem?

Sometimes a gem you added months or years ago just happens to stop being used in your app. Could be an experiment that didn't pan out or a feature change/removal that made the gem that used to be needed for it no longer needed. An unused gem is dead code. Dead code has one purpose – to be removed. If you don't need the gem, now is a good time to remove it. One less thing to worry about.

Is this the latest version?

Gems change over time. Features are added and bugs are fixed. It behooves you to be on the most recent version of your dependencies as possible. This is especially true for a Rails upgrade as gem that work with Rails internals such as ActionPack and ActiveRecord will commonly have to release new versions to add compatibility for newer versions of Rails. Upgrade the gem as far as you can and make sure your tests pass. This too can be done in your main branch, committed, and deployed. You may choose to do a bulk gem update for some of your more "minor" gems that don't change much. For more mission-critical gems such as devise and sidekiq you may want to have their updates in separate commits and separate deploys to ensure constant stability.

Is this gem compatible with the version of Rails I wish to upgrade to?

While looking through changelogs of gems you are updating, keep an eye out for any mention of Rails compatibility. I would say for ~70% of all gems, this should not be a problem. For those "critical" gems that also hook into Rails (devise, ransack, acts_as_list, papertrail, etc) you will need to do some due diligence and see if there will be any issues that will bring your upgrade project to a grinding halt. If the changelog or readme of the gem does not mention Rails compatibility, take a quick peek at any pull requests or issues. See if there are any rumblings of bugs with the latest version of Rails. There may be cases where a fix for the latest Rails was merged into the gem's main branch, but no new release has been rolled. In that case, keep a mental note for that gem for later. Also know that the lack of explicit information about compatibility does not necessarily mean no compatibility. Some gems will just work without modification on the latest Rails.

Step 4: Create a Baseline App

At this point (if not earlier), I like to do something that may seem a bit odd... I create another Rails app for the version the main app is to be updated to. This provides not only a clean app to compare configuration changes, but also a way to test out any gems that need to vetted or edited for compatibility without the overhead and business logic of the main app.

First, install the latest Rails gem using the gem command

gem install rails # use the -v flag to install a specific version instead of the latest
...
$ rails -v
Rails 6.1.0

Next, (in a directory outside of your main app) initialize a new Rails app. If your app is in API mode, use the --api option. You should also use the --database option to match the database used in your main app.

$ rails new rails61 --database=postgresql

If you need to make an app for a specific version of Rails (for example 6.0.3.4), you can use this varient of the command

$ rails _6.0.3.4_ new rails60 --database=postgresql

Step 5: Upgrade Rails and Related Gems

Up until this point, all of the work has been done in the app's main branch. Now, you probably will want to create a new branch for the actual Rails update and changes.

Open the Gemfile in your app and the Gemfile in the "baseline app" from the previous step. Here, compare and update the version requirements for the default gems for Rails. For example, the pg gem should be "~> 1.1", sass-rails now needs to be version ">= 6", listen is "~> 3.3", etc. Now can be a good opportunity to copy over any of the comments in the baseline Gemfile to the app's Gemfile to better annotate/describe what some of the default gems do as well. Don't forget to also bump the version of the rails gem!

For gems that have commits in their main repos that provide Rails compatibility but have not had a new release yet, at this time, you should point those gems in the Gemfile to their GitHub repos. Alternatively, you may be more comfortable with forking the gem, creating a branch based off of the most recent release, cherry-picking in the commit or commits that fixed compatibility, and then point your Gemfile to that repository and branch.

Consult the Rails Upgrade Guide for any additional gems or gems that should be swapped out.

Finally, with your updated Gemfile, install the new gems using bundler.

Step 6: Update the Default Configuration and Framework Files

Your Rails app is not just a handful of gems. It is a directory structure with Ruby code to bootstrap the framework and load the configuration for the environment. The config directory holds configuration, initializers, and Rails initialization code. The bin directory holds binstubs to help properly run gem-provided commands under the right context, such as puma, rails, and rake. The config.ru file starts and initializes the entire app under rack. These files most likely need to be updated, or at least revisited.

I used to use the baseline app to compare all the default files in a fresh Rails app with the existing files in the app I was upgrading. This was a rather tedious process as I had to manually eye-ball both files and see what new configuration options were added and what custom settings were set in the main app and merge them. I also had to check what new initializers were added and copy/paste them into the app.

Modern-day Rails provides a handy command to cut that time down a bit (it is still a manual process in the end, but it at least saves some steps). The command is rails app:update. Run that in your app and Rails will effectively reinitialize the configuration files on top of the existing application. Any new initializers or configuration files will be automatically added. For files that conflict (and this will be at least 90% of the files Rails tries to add), you are given the option to override the existing file with the default, see the diff of the existing file and default file, or keep the existing file. For config/routes.rb and (if you customized this file previously) bin/setup, keep the existing file and override all of the other conflicted files. This puts your app in a clean, default state.

Use the "power of git" to diff the files that were reset with the old versions. While you could use the terminal git diff to go through the files, I recommend either a git plugin for your text editor or any standalone visual diff program to compare the changes. For each file that was changed, go through the diff and reimplement any custom configuration you may have had in your app. This includes SSL settings, generator tweaks, log levels, cache system, default URL options for ActionMailer, and so on. This is probably a good time to put all non-default settings you set at the bottom of the environment files instead of interweaving them with the rest of the file. This allows for a visual clear separation of what is Rails configuration boilerplate and what is app specific. Also take this opportunity to move some configuration around. For example, if you have asset compilation settings in config/application.rb, move them to config/initializers/assets.rb. The same can be done for any log param filtering as there's now a dedicated initializer for that setting. Any new initializers should be reviewed as well so you have a better understanding on their purpose.

The update command should also have created an initializer called new_framework_defaults_X_Y.rb where X and Y is the target Rails version number. Also, config/application.rb should contain a line reading config.load_defaults X.Y where X and Y is the pervious version of Rails. Leave those alone for now. We will come back to them later.

With all that done, commit the changes to your branch.

Step 7: Get Stuff to Work

You have a Rails app reconfigured to boot with an updated version of Rails. Now, it is time to see if this works. I like taking a three pronged approach to kicking the tires on the upgraded app.

Get a Rails Console Working

Flat out try to run rails c. This is a good first step to verify that the app can at least boot into a console. This means the configuration files and initializers at least do not have any glaring issues and your gems load up. Start querying the local database through your models to verify the app can talk to the database. If there's any quick checks you can do in console to verify certain hot spot / important methods on certain classes function, do that here as well. Fix issues and exceptions as you run into them. At this point, note, but do not worry (yet) about any deprecation warnings.

Get Tests to Run and Pass

After verifying the app can boot through a console, invoke your test suite. In a perfect world, tests run and pass, but if not, fix failing tests as needed either by changing tests to reflect any changes needed in test helpers or fix app code that requires changes due to a change in Rails. Again, keep track of any deprecation warnings that may show up.

Poke Around in a Browser

The app can boot and tests can run and pass. The last thing that should be checked in started the Rails server locally and interacting with the app in a browser. Make sure logging in still works and mess with a couple pages. Hit endpoints of some of the more heavy trafficked pages as well as any mission critical ones. Again, fix exceptions if any happen. Check the development log and jot down any deprecation warnings that appear.

Step 8: Fix Deprecation Warnings

Deprecation warnings are just that - warnings. It is code that works now, but probably won't work in the future. Do your best at this time to fix as many deprecation warnings you came across while testing the app.

If you do not have time to fix all warnings or the changes involved would be rather substantial, at least make it a point to fix them after completing the upgrade. Preferably, right after the upgrade. A little extra work today can prevent a lot of extra work in the future when this code does break.

Step 9: Commit, Merge, and Deploy

At this point, your branch is in as good a state as it possibly can to upgrade the app. Put a bow on it and follow whatever process that is in place to get the branch into master. This includes pull request, code review, QA, discussion, etc. if you work with a team. When you are all ready, deploy the upgrade and monitor the app for exceptions in production. Fix errors if they happen, or (worse case) revert the upgrade commit if things go south.

And there you go. Your app (hopefully) is now running on the latest version of Rails! But, we're not quite done yet.

Step 10: Clean Up Forked Gems, Fix Remaining Deprecation Warnings, and Review and Implement New Defaults as Needed

We are assuming the Rails upgrade is in production and everything is stable. The last step is cleanup.

Clean Up Forked Gems

Keep an eye on any gems in your Gemfile that had to either be forked or pointing to a GitHub repo. As soon as those gems have updated released versions, swap those out.

Fix Remaining Deprecation Warnings

Any warnings that weren't fixed during the upgrade should be addressed now. Again, putting a little work today can prevent a lot more work in the future.

Review and Implement New Defaults as Needed

Back in step 6, a new_framework_defaults_X_Y.rb initializer was created. Now is the time to put it to use. This file contains all of the new defaults for the upgraded version of Rails. However, these defaults are actually not enabled. This is due to the config.load_defaults X.Y line in config/application.rb which sets all defaults in the framework to match that of the previous version of Rails.

The initializer lists out all of the new and changed defaults complete with a helpful comment explaining what it does. Uncommenting the line that sets the configuration value enables the new default. Go through the file and enable the defaults you feel most comfortable with. Eventually, you will either reach a point where all defaults are enabled, or you have manually set the configuration value for that default in config/application.rb to the opposite setting. Once that happens, delete the initializer file and change config.load_defaults to the current Rails version.

Conclusion

This blog post ended up being longer than I anticipated, but major upgrades for any framework are never a quick walk in the park (especially if your app has a lot of moving parts). This lengthy process should not scare people away from upgrading their apps. Yes, this is time consuming. Yes, there is a lot to consider and do. That said, keeping Rails updated is important for the longevity of your application.

I hope my experiences with upgrading Rails apps will help you out one day when you are faced with an upgrade project. Maybe you can improve on my methods. This is certainly not the only way to upgrade.

See you next year!

Tags: ruby, rails