Rails Application Upgrades: Hard Mode
Last month Luke Francl published a great article titled “Upgrading a Rails application incrementally”. In it he lays out an approach for performing Rails version upgrades, specifically discussing his experience upgrading an app from Rails 3.2 to 4.2.
There’s a lot more to it, but his strategy hinges on parameterizing the dependencies of the application so that both the application and the test suite can run against multiple versions of Rails.
Application developers might not realize that having one codebase that runs against multiple versions of a gem is an option to them. On the other hand, gem maintainers are probably all aware of this. The Solidus extension ecosystem is an awesome example of a suite of gems that work against a variety of both Rails and Solidus versions.
Note: If you haven’t read Luke’s article yet, you should probably do so now before continuing. It’s a really great read.
Solidus 1.0 was released in the summer of 2015. The project sought to continue the development of the Spree platform after Spree Commerce was acquired by First Data and announced they were stepping away from their maintenance role.
While a dedicated team eventually stepped up to continue working on Spree, Solidus gained great initial momentum and continues to be the more active project by commit frequency. Because of the activity on the platform and the backing from many respected community members there’s been a steady stream of projects making the jump from Spree to Solidus.
When people show up in the Solidus Slack channel asking questions about migrating their application from Spree to Solidus they’re immediately pointed at a short upgrade guide on the Solidus wiki. Because Solidus forked from Spree around the 2.4 release, applications currently running Spree 2.2 to 2.4 are in the best position to upgrade.
Spree 2.2 depends on Rails 4.0, and 2.4 depends on Rails 4.1. The 1.0 release of Solidus requires Rails 4.2, so upgrading from either of these relatively desirable (as far as ease of migration) Spree versions requires upgrading both Rails and Spree/Solidus at the same time. This introduces some trickiness into the upgrade.
The biggest blocker in getting a Spree/Solidus app to run against another version is that there are schema changes. Any two versions of Spree/Solidus are going to expect a different database schema. I don’t know of any reasonable way to handle this within Luke’s approach. I can think of ways to accomplish it, but none that would be worth the time and effort.
If you find yourself in a situation where you can’t do an incremental upgrade, don’t despair. A long-lived feature branch is a liability, but there are some great strategies for reducing the risk.
It’s worth noting that Solidus has taken a strategy of cutting releases that contain no changes other than a Rails major version bump and associated fixes. As such any project upgrading within Solidus (rather than from Spree to Solidus) should be able to use Luke’s incremental upgrade strategy. In general, upgrades between Solidus versions are quite easy and don’t require special planning or effort.
Contribute Back To Master
Try to frame every change you need to make as a chance to fix the issue upstream. Rather than adding conditionals based on which Rails version you’re on, try to write code that will run on both versions. Of course many (probably most) changes won’t be candidates for this, but every time you make a change in master instead of the upgrade branch you’re avoiding unnecessary divergence. (More on why divergence needs to be avoided later.)
Issues with ActiveRecord are prime candidates. You’ll almost certainly run into queries on your upgrade branch that no longer produce valid SQL due to changes in ActiveRecord. Take these as a chance to rework the code so it works on both versions of ActiveRecord. Not only will you be avoiding diverging from master unnecessarily, but you’ll also be driving your project to use the more stable parts of the API, potentially avoiding headaches further down the road.
Rebase, Rebase, Rebase
Not all team’s are comfortable with it, but git’s rebase feature allows you to periodically rewrite your long-lived branch history on top of the latest master, fixing conflicts as you go. This offers a variety of benefits:
- It gives the upgrade team more visibility on any conflicting changes that other teams are making.
- It avoids additional divergence. As regular development continues, master will be diverging from the upgrade branch (usually) faster than the upgrade branch is diverging from master. This keeps that in check my regularly bring the upgrade branch in sync with master.
- If you’re contributing changes back to master, it brings those changes in your upgrade branch.
- It keeps related changes together in the right commits. If you merge master into your upgrade branch instead, you’ll have to fix any conflicts. Those “conflict fixes” are logically part of whatever commit in your branch introduced the conflict, not the merge itself.
If your team doesn’t want to use rebases, then regularly merging master back into your upgrade branch will still offer some of the benefits. In my experience with long-lived Ruby codebases it’s much easier to follow and understand the history (something you’ll probably spend a good amount of time doing while performing this kind of large application upgrade) without all those unnecessary merge commits, but it isn’t the end of the world.
When performing these kinds of application upgrades, one of your biggest liabilities will be the people continuing to make changes to the application. Every change you make is a vector for new conflicts. The rest of the team isn’t going to be keeping your “future” changes to the application in mind while they’re doing their day-to-day coding. Even if they’re trying to be mindful of the upgrade work, that’s not an easy task.
The upgrade team must regularly communicate about where they are making changes to avoid unnecessary conflicts. They may even need to sit in on meetings where upcoming features are being planned to steer teams away from areas that the upgrade has changed significantly.
Of course you won’t be able to avoid those situations altogether, but do your best to help decision-makers understand the costs of any overlapping work.
Avoid Invasive Changes
The most important way you can maximize the success of your upgrade is to avoid making changes that are going to conflict with changes made upstream. This also might be the hardest part of doing these upgrades. There are two main objectives:
- Don’t make any unnecessary changes.
- Make the most dangerous changes last.
As I mentioned before, every change made on the upgrade branch is a conflict waiting to happen. Given enough time someone will eventually make a change upstream that will conflict with the upgrade branch and the upgrade team needs to be mindful of this.
Pull out stats on which files and classes change the most frequently. Code change frequency is called “churn.” Files/classes that churn a lot usually indicate deeper design issues, but in the context of an upgrade they are just huge liabilities; changing them is asking for conflict. If you can, avoid changing these files at all or at least save changing them for last.
It’s All About Risk
Running a long-lived upgrade branch is always a risk. Most projects should avoid them at all costs. Unfortunately, sometimes they are the only option. As with anything, once you’re forced into a bad situation it becomes all about managing risk.
Getting through an upgrade efficiently is an exercise in minimizing the friction from reconciling the two changing views of the codebase. That means keeping those branches as close to each other as you can for as long as you can.
I hope these strategies can help out teams that are facing hard upgrades. If you’ve already kicked off an upgrade, it’s not too late. Re-prioritize your work based on code churn and upcoming plans, and look at your existing upgrade commits and contribute some back to master.
Thoughts and feedback? Let me know on Twitter!
I provide technical leadership, risk assessment, project planning, training, and additional development to software teams. If you need help taking a project from idea through execution, get in touch.