Last month I worked on upgrading GOV.UK’s biggest application (in terms of code, users and volume of content) from Rails 3 to Rails 4 (Here’s the monster pull request). I wish I could say it was a straightforward undertaking. In the end, it involved one Rails patch, three patches to other dependent gems, and a lot of sleuthing and debugging.
There’s plenty you can do to reduce the risk and the pain of the transition from Rails 3 to 4. The TL;DR? Apply as many of the required changes as you can directly to your Rails 3 code before the final gem bump to Rails 4. I’ve written up my notes below:
1. Understand the major changes
The Guide for Upgrading Ruby on Rails is a good place to start. It provides a good overview of the major changes between versions.
2. Backport as much as you can to the current version of your app
A lot of the changes you’ll make for the Rails 4 upgrade will be backwards-compatible with the Rails 3 version of your app, making it possible to backport some of the code to the current version. This will make the final upgrade to Rails 4 smaller, simpler and safer. Once you have a working Rails 4 branch, go through your commits and see which ones can be cherry-picked back onto the Rails 3 version. Having well-formed and atomic commits will make this task much easier.
3. Move to strong-parameters
Rails 4 deprecates both
attr_protected for mass-assignment protection. To help ease the transition to Rails 4, I recommend moving to the strong-parameters gem on the Rails 3 version of your app before performing the Rails 4 upgrade. In my experience, the move to strong-parameters can be a tricky one and is best done separately.
4. Audit your Gemfile
It’s almost certain that your Gemfile will contain out-of-date gems that will need updating to be compatible with Rails 4. As above, you can save yourself a lot of headache by updating as many of them as you can on the Rails 3 version of your app.
In our case, we went a step further and performed a complete gem audit, looking at all the gems that were outdated and figured out:
- Is the latest gem version compatible with Rails 3?
- How big are the changes between the current and latest version?
- How much risk is associated with updating the gem?
Armed with this data, we were able to update, test and roll out a whole bunch of gem updates on the existing Rails 3 code in a controlled manner. This meant there were fewer potentially risky gem updates in the final upgrade to Rails 4.
5. Don’t jump straight to the latest version of Rails
Going from Rails 3.2 to Rails 4.0 will be an order of magnitude easier than jumping straight to 4.2, or whatever the latest version is – you’ll get the benefit of a bunch of deprecation warnings, and thus fewer breaking changes. The subsequent upgrades from 4.0 to 4.1 and then to 4.2 will be simpler and less risky as a result. That said, once you’ve made it to Rails 4.0, don’t delay in getting to Rails 4.1 or later as 4.0 is now retired and is no longer be receiving security updates!
6. Good test coverage helps a lot, but poorly-formed tests are a pain
It’s times like this when a comprehensive test suite is super helpful. Having said that, some tests end up causing as much pain as they save. I’ve found this particularly the case with tests that make excessive and unnecessary use of stubs and mocks. A significant number of the commits in this upgrade were solely rewriting tests that had broken, not because the behaviour had changed, but because the tests were not robust enough and too tied to the internal implementation of the methods under test. Lesson here: avoid writing brittle, heavily stubbed tests!
7. Start at the lowest level and reduce the noise first
My focus was to get the unit tests passing first. Although this was the bulk of the work, it helped to make the whole process a little less overwhelming, and once the unit tests were green, getting the rest of the test suite to pass was much easier. Getting rid of the deprecation warnings as an early step also helped to build some momentum and reduce the amount of noise in the test output.
8. Avoid non-reversible migrations and session changes
Even with thorough testing, QA and preparation, there is always a small risk that you will need to roll back your Rails 4 code. Make this as easy as possible for yourself by avoiding changes that cannot be easily rolled back. The main things to avoid are:
- Destructive migrations that cannot be reversed
- Upgrading to the new cookie signing mechanism
The latter is particularly important, as user sessions that have been upgraded to the new signing code cannot be reversed. This means that if you have to roll back to Rails 3, you’ll break a bunch of user sessions. I would recommend not setting the new
secret_key_base and living with the deprecation warnings until you are confident that your Rails 4 code is stable and won’t be rolled back.