Who doesn’t like to build new apps? As much as it’s fun building new things, we also need to maintain and upgrade apps over time.
Most people prefer to build new apps, and we can understand that. But we also like to maintain older apps. Make them fresh again.
Part of the lifecycle of an app is upgrading dependencies to newer ones, especially when they get vulnerable to some security issues, or when you use versions that are not maintained anymore.
We’ve been doing this kind of upgrades for many projects, and we developed a good framework on how to plan and execute them.
We just worked on such an upgrade project recently and we thought it was time for us to share our workflow for big Ruby on Rails upgrades. That said, it probably applies to many other frameworks and languages out there.
Let’s dive right in!
Why should we do a big upgrade in the first place?
It starts with a conversation with either your manager, team, a client or just yourself depending on the situation.
What are the goals of a big upgrade? Why should you do it in the first place? Great question, glad you asked!
We’re not fans of upgrading just for the sake of upgrading. In general, we will propose big upgrades when we need to. Sometimes when there are features in a major new version that we need or would make our life easier (hence, saving us time and money).
Sometimes it’s time to upgrade when technical debt is reaching a risk threshold, in terms of security, incompatibilities, hiring new developers, etc.
But when do we have to do big upgrades? When Ruby on Rails stops maintaining the version of the framework we are using.
For example, if you are using any version of Ruby on Rails before Rails 5, you need to upgrade.
AS PER OUR MAINTENANCE POLICY, THE RELEASE OF RAILS 5.1 MEANS THAT BUG FIXES WILL ONLY APPLY TO 5.1.X, REGULAR SECURITY ISSUES TO 5.1.X AND 5.0.X, AND SEVERE SECURITY ISSUES TO 5.1.X, 5.0.X, AND 4.2.X. THIS MEANS 4.X AND BELOW WILL ESSENTIALLY BE UNSUPPORTED!
In the case of this client, We pretty much needed to upgrade.
Sometimes, you need to do big upgrades due to a security vulnerability in a dependency you’re using that needs to be upgraded, and indirectly requires a framework upgrade.
Your main goal should be to make sure there are no known security vulnerabilities in your dependencies and that your dependencies are being actively maintained.
Where to start?
Now that we know we need to upgrade, what are the next steps?
You need to decide what kind of upgrade you want to do. Do you want to go to the latest release of Ruby on Rails, 5.1, or would 5.0 work? After all, it’s maintained too, right?
It depends™. I suggest to always go step by step. That is, don’t try to do an upgrade from Rails 4.1 to 5.1 directly. While it could work, it is unlikely to work and is highly likely to make your life much more complicated when you face a bug. How will you know which upgrade actually introduced the issue?
Fixing security issues first
You should aim at fixing security issues first. This should be your #1 goal at the moment. Fix them, and deploy that.
If you are not using Gemnasium, now would be a good time. Setup the project on Gemnasium, and you will know in a breeze if it’s affected by known security vulnerabilities, and how to upgrade fix them.
For each vulnerable gem, we will tell you which version fixes it. For example, look at version 2.7.0 of uglifier that has a known security vulnerability. As you can see on that page, we know the details of the issue, and we know that we need to upgrade to version 2.7.2. Now, what’s new in this version? Are there changes that require us to do change in our code or configuration? We show all that in the integrated changelog. There’s not much in that specific case, great!
bundle update uglifier replacing “uglifier” by the gem or list of gems you need to upgrade for security reasons.
Repeat for all gems with security issues.
Now, run your tests if you have any and get that through your usual QA and delivery process to make your app safe again.
Let’s do the big upgrade now and face the problems of big upgrades
Now that the app is safe, let’s proceed to the big upgrade itself.
The first step is to remove any and all version locks in the Gemfile, besides the Rails one.
In our case, we had to go from Rails 4.1, to 4.2, then to 5.0 and finally to 5.1. So, in the first upgrade cycle, I’ve had a
gem 'rails', '~> 4.2.8' in my Gemfile.
Next, I tend to upgrade to the latest Ruby version too or at least one that’s maintained. Change it in your Gemfile and any other files, depending on how your project is setup (RVM, Rbenv, chruby and what not).
You’re now ready to run
bundle update. If you’re lucky, that will work!
That said, it’s quite unlikely that’ll work on the first try, especially for brand new versions or if you have a lot of gems that are not very well maintained.
What should you do if it fails to install some gems or doesn’t find compatible gems?
Find alternatives when it goes wrong
That sucks, but we’ll figure out a solution. It’s not like we could decide to cancel the upgrade.
Typical reasons for this are:
- you are upgrading to a version that’s so new that there is no version of gem X that’s compatible with Rails Y, yet.
- the gem you are using is not so well maintained, or just isn’t maintained anymore.
For reason #1, if you’re lucky, there is a fix available, just not yet published. You should go see the repository and look in the last commits, issues or pull requests to find evidence that you’re not the only one. Typically, you will have to use the GitHub version. You can do that with a line like this in your Gemfile:
gem 'some-gem', github: 'someone/some-gem' if it’s master, or you can specify the branch.
Keep a note to go back and update to an official version later.
If it’s reason #2, this is where the fun begins. #not.
You need to find a replacement solution. It could be finding a fork of the same gem, a similar gem or rolling your own solution.
If you’re lucky, you’ll find someone else that had the same issue and fixed it. You can find that by going on GitHub and click on the number of forks at the top right. You’ll then be able to quickly visualize and spot forks that have newer commits.
Review the commits, and try upgrading to it. Always review the commits and make sure they are legit.
In some cases, I realize that the gem was not that useful at all and can be replaced in a few minutes. In that last upgrade we did for a customer, we saw a gem we never heard of before, that we could remove and replace by pure Ruby in about 5 minutes. Guess what we did?
Repeat for each gem that blocks at
bundle update, until it works. Be patient, this can take a while if you have an application that’s huge, old or was built by people with little experience maintaining applications over many years.
Review the change logs
Now that you upgraded your gems, we need to do the code changes required for those upgrades.
I always start by looking at the release notes of Rails. There is a release note for each version. It’s good to know what’s new and what changed. Next, go in the upgrading guide just here. There is an upgrade path for each version of Rails in there.
Not everything will apply to you, but you should check them all and do those that apply to your case.
This is for Rails, but what’s up for the other gems? After all, if you upgrade Devise 3.x to 4.x, for example, there will likely be some changes required on your side.
We tend to go on Gemnasium and look at each version bumps, and look at the integrated changelog just for this version. For example, devise here. You can also look at the changelog in each repository, but I generally find that more tedious as it requires you to go on many repositories, find how they keep their changelog and actually consume the changes and apply them.
You need to do that for every gem that you use directly. Look at all the changes to go from the version you were using to the new one. I focus mostly on finding breaking changes.
We should not care too much about the gems we use indirectly, as our direct gems should have taken care of their own dependencies, in theory.
Test, QA & deployment
You’re now at the point where you have an app that’s up to date. What’s next?
Getting it into production.
If you’re lucky, you have a pretty good test suite, and you can rely on it to catch all or almost all possible bugs.
Even if that’s the case, I highly suggest you test all your app properly. Get it fully manually tested on the browsers you support in an environment that’s as close as production as possible.
And then…ship it!
Once it’s deployed, it’s not all. Use the app immediately in production yourself. You need to keep a close eye on your error handling, your logs, and your various monitoring tools.
If all is fine after a few hours or days, it generally means that you did a good job, and you can celebrate 🎉
Our app is now safe and we’re ready for the next Rails version, are you?
If you have any questions or corrections, or with other suggestions, please let us know.