A very important part of software maintenance is to keep languages, libraries, and framework versions up to date.
I've experienced this many times while working on Ruby projects and I've collected some tips or patterns that helped me to deal with future cases. Pretty much everything presented here applies to different languages and dependency management systems.
First of all, we need to understand the motivation for an upgrade. There could be many reasons:
- We need a new feature and the current version does not provide it
- A security alert (like a known vulnerability) was reported
- A new version comes with a performance fix/refactor and we want to take advantage of it
- An EOL (end-of-life) support date is approaching (or it already happened). It’s wise to consider that in your project roadmap.
- We want to ensure the library’s stability by migrating from an unstable/development/alpha/beta version to a stable one (like moving from 0.x to 1.x)
- We want to keep doing regular upgrades to prevent the above situations
Should we upgrade?
Some questions that could help to make a decision:
- What could happen if our app breaks after the update? How hard would it be to recover? How bad could it be for the users of your system?
- A vulnerability is definitely something not good, but how likely is for some malicious user to exploit that vulnerability? For instance, if we know our users are internal employees of a company, it’s clearly not the same as having a public page that everyone can try to break
- Is this a good time to do the upgrade? How does it play with other commitments or deadlines? What are the costs of postponing it? Also, don't wait for the best/perfect time
- How much do we trust in our test suite or linting tools to catch problems early? How much can we test in an automated way vs. manual? Or in general, how many possible errors can we catch in environments before production?
- Are there references/upgrade guides (either official or non-official) for the changes that could break? Does the project say something about backwards compatibility?
- How is the history of the latest upgrades in the project? How did they go? If there was a problem, how much time did it pass until we detected it and how much time did it take to recover?
Things to have in mind
- Upgrade chains/trees: If a library has dependencies, it’s possible for them to need an upgrade as well. Also dependencies of the dependencies. It could be a tree full of upgrades. Start by the easy upgrades, the leaves on that tree, when possible.
- Changelog analysis: Review carefully CHANGELOG files, when they exist. Pay special attention to breaking changes or behavior changes on the features you use.
- Semantic Versioning: SemVer makes everything easier... when maintainers follow it. If the convention is not followed or not strictly respected, treat every version change as a potentially breaking one.
- Test strategy: How will you test the upgrade? I suggest measuring test coverage around the code affected by the upgrade. That way you’ll have a high-level view and you’ll know how much manual testing you will need.
- Deprecation warnings: New versions might introduce deprecations, which are things that are going to be removed in the next versions, but you can still use them, receiving warnings. I’d say an upgrade process is not fully complete until we fix them, because today’s deprecations are blockers for tomorrow’s upgrades.
- Allowing upgrades within a safe version range: We have `~>` in Ruby’s Bundler (the pessimistic operator), same thing in Python with Pip’s `~=` (compatible release clause). This plays nicely for projects following SemVer, you can make upgrades without changing your dependencies specs, while sticking into safe margins (like preventing major version upgrades).
- Check for outdated packages: Most package managers give commands that you can run and see which packages need upgrades, like NPM’s `npm outdated` or Bundler’s `bundle outdated`. This proactive approach can save you some headaches in the future.Reading others’ experiences: most of the time, an upgrade it’s not a task you need to figure out on your own. There are a lot of great articles about upgrade stories with different experiences, pay attention to them and the possible errors/solutions they offer. One great example of this is FastRuby, a company that specializes in Rails upgrades, their experiences and tools are very useful.
Upgrades are important, for sure, and there are many things to have in mind before jumping on a task like this. But if it is postponed, it becomes worse! As with many things in our field, the more practice we do, the better! And it needs to be done carefully, it’s not just changing a number, pushing it and seeing how it goes. Which were your most challenging upgrades? How will you approach the next one? I’d like to hear you!