How to make your next Rails upgrade easier

15 Mar 2016

If you're a Rails developer, you know all about upgrading apps from one version to another; it's part of the job. These upgrades can range from simple gem version bumps of 4.2.5.2 to 4.2.6, to nightmarish leaps from 3.2 to 4.1. Those latter upgrades are challenging, but there are ways to make them easier.

First of all it's helpful to establish an overall strategic objective for these tasks - and that is, we want to get this upgrade done. Having a production app that's out of range for patch releases is a security risk, and while you can always backport the fixes and build a custom version, that's no fun. So we want to move with a purpose and complete the task so we can get back to implementing new features.

Strategy aside, tactically we want to make this upgrade as small as possible. Anything we can do on the current working branch, be it master or whatever git-flow variant that you're using, let's do it there. That way you reduce the chance of merge conflicts and you improve the application version that the rest of the team is using. In the worst case, if you get diverted to some other critical task and have to stop working on the upgrade, at least you've moved the ball forward a bit.

Another way to give yourself a good feeling around the upgrade is to, as the Rails upgrade guide suggests, add tests. If you've got good test coverage already that's great, but if not you can at least add tests that are hitting each controller action. Don't worry about backfilling 100% branch coverage, but instead cast a wide net over the application. You just want to exercise as much code as possible when you run your test suite. Tests of things that won't fail loudly are especially valuable - such as code that's executed via cron jobs. And of course those tests would go on master and then you should rebase your branch.

One of the best things you can do to make an upgrade easier is to reduce the surface area. Take a pass through the app and see what unused code you can remove. Sometimes there are old scopes and finder_sql usages and whatnot that are no longer used, especially if you're dealing with an application that's been around for a while and has been decomposed into a few backend services. Removing them is a delightful task if someone was good enough to write tests for these; it's great fun to delete a swath of unused scopes and their tests, knowing that you won't have to port these up to the next version. As before, these cleanups should be done on master and then rebased.

Along the same lines, look at the Gemfile. Are there gems that were added for some third party integration that's no longer used? Maybe this application used to generate reports and doesn't any longer - goodbye pdfkit and friends. Frequently, removing a gem will carry along with it a flurry of initializers and config/ directory entries and other such flotsam. All this helps to reduce the size and complexity of the application. Again, do these clean ups on master and then rebase your branch. For a more challenging task consider consolidating functionality. Do you really need curb, httparty, and rest-client in your Gemfile? If it's not too intrusive, try to boil that down to just one library. I've seen applications where httparty was brought in for a single API call and was easily replaced with the rest-client gem that the rest of the app was already using.

While you're in the Gemfile, see what gems can be upgraded. If you're on some old-ish version of will_paginate, move it forward as far as possible. The newer version may well work with the upgraded Rails right out of the box. It probably contains performance and security improvements anyway, and it just feels good to be using the latest code.

Look around your app and see if you're monkeypatching any core classes or any Rails libraries - ActiveSupport, ActiveRecord, etc. Reexamine these monkeypatches and see if they're still applicable. I recently reviewed an app and removed a couple of ActiveSupport monkeypatches that had been rendered unnecessary by some other code removals; that's a nice reduction in code fragility. If you're feeling advanced and are on a recent version of Ruby, you could think about replacing rarely used monkeypatches with refinements. But of course deleting them is best of all. Similarly, you can use the output of rails:update (more on how I use rails:update) to remove old snippets of code from various core Rails files like boot.rb and application.rb and such.

Moving to your upgrade branch, recall the overall objective - to get the upgrade done. To that end, disable deprecation warnings in your application.rb with ActiveSupport::Deprecation.silenced = true. Once you've got the upgrade complete and deployed, enable them in a feature branch and bang through the various fixes until you can remove that line. But don't let a flood of warnings steer you away from your primary goal.

There are a variety of helpful gems which contain code that was extracted from Rails; protected_attributes, activerecord-deprecated_finders, actionpack-xml_parser, and so forth. Use these! It feels a little dirty to have them in the Gemfile, like you're lying to yourself about the status of the upgrade. But you can be disciplined about following up on these once the upgrade is complete. The temporary compromise is worth it the first time a security patch comes out and you can just use the stock Rails gem. And after a while you may well find that some of the code using the old finders has been deleted or is trivial to upgrade.

Once you're in the thick of the upgrade, look at the changes that you make and see if any can be backported to master. This might even involve a temporary monkeypatch, but if it's a change that makes the diff smaller and can be removed as part of the upgrade, it's worth it. Ideally the switch from one version to another would just consist of Gemfile and Gemfile.lock changes - probably an unreachable goal, but it's something to shoot for.

As a final note, doing Rails upgrades and chasing down issues that originate deep within libraries may seem unpleasant. But it's a very different task then our usual work of getting data out of MySQL and rendering some HTML or JSON. This gives you a chance to dig into the gems that you've been using for years, to learn about code reloading and character encodings, and generally to deepen your understanding of Rails, Ruby, and your application's environment. As Mary Poppins would say, "In every job that must be done there is an element of fun." Happy upgrading!