Keeping Code Up To Date

Writing an application really is the start of your problems! Once in production, you need to monitor and maintain it, ensure that it stays secure and fix bugs.

At some point the libraries you use and your programming language of choice are going to include patches to fix their own bugs and security issues. You might discover your own bugs in your application that can best be fixed by upgrading a gem or the language version you are using. Eventually those languages and libraries are going to reach End Of Life, and will no longer be supported.

So, I’d like to present my preferred strategies for keeping your application code up to date, and my best practices for updating your software, and providing migration paths for users of your software who will in turn want to keep up to date.

Keeping Up To Date.

There are a couple of approaches to maintaining software. The most cautious approach that I see quite often to software updates is to avoid them as long as possible: You know your software works, why fix it if it ain’t broke? Additionally, updating a dependency has the chance of introducing a new bug, or requiring updating your code. Or, the most common reason: you are too busy and under pressure to deliver new features, and just don’t have time for such niceties.

Technical Debt Charges Interest

However, the longer you leave it, the harder it becomes. Usually, in a non-linear fashion. Documentation and migration advice often gets lost, leaving you to figure out what to do by yourself. You may also find that your migration path will include uncommon combinations of gem versions that interact in ways rarely seen in the community, and raise their own unique and interesting bugs!

Worst of all, though - you may come across a bug, or external reason (like a regulation change) that forces you to upgrade some part of your system. I’ve seen businesses forced to take their web site down until they have upgraded a library, and that upgrade has cascaded a slew of other upgrades. Then, the pressure is really on to upgrade your libraries immediately.

My Approach

My preferred option is small updates as often as possible. If you can try to include one gem update for every pull request you make, you’ll probably keep on top of things pretty well.

That plan doesn’t always work out though, but my general approach is always similar:

  1. Update your gems and libraries - minor versions only.
  2. Update gem major versions
  3. Update ruby, if possible
  4. Some gems might have later versions that required a new version of ruby, so go back to step 1.
  5. Update Rails.

Update your Gems and Libraries

Bundle now has a nice feature that makes it easy to see what gems can be updated: bundle outdated.

You can then use the bundle update options to update slowly and carefully.

--patch         # Prefer updating only to next patch version.
--minor         # Prefer updating only to next minor version.
--major         # Prefer updating to next major version (default).
--strict        # Do not allow any gem to be updated past
                # latest --patch | --minor | --major.
--conservative  # Use bundle install conservative update behavior and
                # do not allow shared dependencies to be updated.

Git commit before you get started, and after every update. That way you’ll always be ready to roll-back a change, if necessary, and you’ll be able to git bisect if a bug is discovered at a later date.

Start with easy wins. You can try a quick bundle update --minor and see if everything just works. For every step here, run all your tests, and check for deprecation warnings. I always have guard running, to get feedback as soon as possible.

Failing that, pick specific gems, and update one at a time. Commit often. If a gem update fails, it might be worth rolling back, and get with low hanging fruit first. I prefer updating one at a time because trying to track down when broke your code when you’ve just updated 20 gems all together is much harder!

Minor versions are generally easier to upgrade, since no breaking changes ought to be introduced. Take more care when 0.x.x versions of libraries are used, though, since there could be breaking changes at any point.

Major version changes ought to be undertaken one gem at a time, where possible, with bundle update --conservative --major. Always review documentation to see what impact changes will have. Generally speaking, the longer you leave it, the harder it is to find out that information.

Keep your test suite passing, and use something like guard to run your tests as much as possible whilst you are upgrading. Check for errors, warnings and deprecations.

After updating your gems to the latest minor version, where possible, move to the major versions. Continue to use the --conservative option. You may find that some gems can’t be updated one at a time, due to dependency conflicts. Start with the easiest updates, and do the hardest updates last.

You may reach the point where your dependency graph forces you to upgrade several gems at once. Do this when you have to. I avoid it as long as possible, because the more gems you have updated, the harder it is to track down the cause of a new bug.

You may also have to stop because a gem upgrade requires an updated version of ruby or rails. In which case, continue to the next phase, and remember to go back to step 1 later.

Update Ruby

Updating your version of ruby usually takes more effort that simple gem upgrades.

Depending on your configuration management, you may need to coordinate this with other teams. Container systems like Docker might make the process a bit easier.

The principle is similar, though. Check for deprecations; read the documentation.

Update Rails

I usually wait longest to upgrade rails. Particularly, MAJOR version upgrades are significant enough to warrant their own sprint time. Old rails versions often seem to be the biggest reason why the rest of your gems get out-of-date, and your Gemfile stagnates.

Do Unto Others

For your own software, try to provide the same information and techniques that make your job easier:

  1. Use semantic versioning.
  2. Provide useful patch notes detailing the changes you’ve made, and the consequences for your users.
  3. Provide deprecation warnings in the last MINOR version before a MAJOR version release.
  4. Keep your gem’s dependency constraints as loose as possible, to help others build a gem dependency graph that allows them an easier upgrade path.
Tagged: | bundler | ruby | idioms |