Rails Application Upgrades: Hard Mode

Published 20 February 2018

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.

Hard Upgrades

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.

Stay Visible

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.