Let's talk about SemVer

Semantic versioning explained with examples

Ah, Semantic Versioning. The concept sounds simple enough, but it's been the source of untold lost hours of confusion and disagreements. I'd like to offer up a small change in perspective that I hope will help settle such issues once and for all.

The root of the problem lies in the fact that we generally live under the illusion that words have clear and unambiguous inherent meaning. From there, we draw the mistaken conclusion that we can unilaterally assign meaning to version numbers as well.

However, meaning is a common understanding between two or more participants in a conversation. SemVer, therefore, cannot be understood without considering the parties in the conversation. Specifically, a semantic version number is a message from a package's author to its consumer. We can resolve many questions surrounding semantic versioning by considering the intention of the speaker (the package author), and the conclusions they want the listener to draw.

So given a version number <major>.<minor>.<patch>, let's consider what conclusions a package's author wants its consumer to draw when they increment:

  • …the patch version number: that the author expects the consumer to be able to blindly upgrade from the previous version. If they were waiting for a specific bug to be fixed, they may want to check the release notes to see if they can remove any workarounds they have put in place.
  • …the minor version number: that the author expects the consumer to be able to blindly upgrade, but would recommend checking the release notes. It might contain relevant new features, or introduce the deprecation of existing functionality and its replacement, the migration to which the consumer will probably want to start planning.
  • …the major version number: that the author expects that this new release might break something for the consumer, and therefore suggests reviewing the release notes before attempting the upgrade. Ideally, most of the required changes involve functionality that was deprecated in an earlier release, and have therefore already been dealt with on the consumer's own schedule. Unfortunately, this is not always possible.

The assumption with all of the above expectations is that the consumer has stuck to the publicly documented API. It is hard enough to predice the impact of changes as it is; it would be nigh impossible if unforeseen use cases would also need to be taken into account.

With this new perspective in hand, let us finish up this post by considering a couple of questions that commonly arise regarding SemVer, both from the perspective of a package's author as well as from its consumer. Get in touch if you have any to add!

Package author dilemma's

"Is it a breaking change if I change around my UI?"

Is there someone who is going to consciously make decisions based on your version number? In other words, is your version number actually a conversation, or is there no listener? For many user-facing projects, there actually is no person making decisions based on your version number, in which case SemVer doesn't actually add value — so why bother?

"Is this bugfix a breaking change? It changes my package's behaviour."

Is the buggy behaviour part of your publicly documented API? If not, your consumers should not be relying on it — if they find a discrepancy between the documentation and what actually happens, they should file an issue.

"But lots of my users rely on this undocumented behaviour!"

Well, if you know they do, then by all means, increment the major version! You're trying to help your users deal with your new version, and while a major version increment is more work to evaluate, unexpected breakage is even worse.

If you think the risk of breaking changes in bug fixes is particularly large, you could even consider forgoing patch version increments altogether.

"I've added a major new feature. Do I increment the major version number?"

While forcing your users to look at your release notes is a good way to advertise new features, it greatly increases the burden of the upgrade. So unless you have particularly engaged users, consider whether they really are best served with a major version increment.

"My API is super-generic, is it really a breaking change if I break just some of the use cases?"

As with internal API's, adding too much abstraction to your external API makes it harder to change. If you want your users to be able to trust your version numbers, then you should increment the major version number if you expect it to break for them.

"So I can just introduce as many breaking changes as I want, as long as I increment the major version number?"

SemVer makes it easier for your users to judge the amount of work required to upgrade, but it doesn't decrease that amount of work. Avoiding breaking changes as much as possible, providing migration paths if doable, will still be greatly appreciated by your users.

"What if I don't want to do SemVer?"

Then don't do it :) That said, if you're in an ecosystem in which SemVer is common, it would be courteous to state that upfront to avoid people making incorrect assumptions.

Package consumer dilemma's

"Who do I sue if a minor or patch upgrade broke for me?"

Notice how often I used the word "expect", before? There is no way for a package author to know your codebase, so these version numbers communicate their best human judgment — which could be wrong. Caveat emptor; proper QA infrastructure will always be important.

"There is undocumented functionality that would really be useful to me. Can I use it?"

Sure, but then all bets are off: even patch increments could introduce unforeseen breakage in your code, so you would do well to inspect the changelog even for non-major upgrades. You might also want to configure your package manager not to depend on a version range.

"Can I expect a project to be stable once it reaches version 1.0.0?"

SemVer only says that the above increments will be observed from v1 onwards — not that no more breaking changes will be released at all. In general, I'd expect a young project to still be figuring out its API, whereas an older project is more likely to be stable.

For example, React.js iterated through its first major versions relatively quickly, but has been on version 26 for over two years at the time of writing.

License

This work by Vincent Tunru is licensed under a Creative Commons Attribution 4.0 International License, with the exception of the comic by Randall Munroe, licensed under a Creative Commons Attribution-NonCommercial 2.5 License.


4 June 2020Engineering Practices