So far, I believed in SemVer. In a way, I still do, but I developed another interpretation of its rules. It turns out, there isn’t a “strict SemVer”, since the spec allows library authors to use their best judgement when it comes to breaking changes in patch versions.
Breaking changes in patch releases are the worst problem we experience with dependency upgrades. I thought library authors were to blame, until it happened to me too. I broke application code in production by releasing a new patch version of one of my libraries, which happened to contain a tiny breaking change. We released xstream v6.4.1 with a bug fix to a helper function that happened to change its API usage for TypeScript users.
I won’t try to defend myself, it was my mistake to release it as a patch, I should have released it as major. But it left me intrigued: “why isn’t it upfront more obvious how to choose the version update?” xstream itself has a rather neat system for detecting breaking changes and suggesting which version update to do. Why did we let this one slip through our system? In retrospective, the commit message did not have “BREAKING CHANGE” keywords. How can we get better as humans in deciding what is a breaking change? (yes I know semantic-release exists)
Consider the following. Your library has a bug: it behaves in unexpected ways for a corner case. You believe almost no one among library users experiences that corner case. If you release a bug fix for that bug, is it a patch or a major? If it is a patch and some of your library users happen to have unknowingly relied on the buggy behavior, their application code will break if they update automatically without testing.
According to SemVer, you used your best judgement and did the right decision. What you should do next is release a fix for the bug fix, either rolling back the breaking change or releasing a major version while deprecating the previous patch. It doesn’t eliminate the guideline of using best judgement regarding a bug fix version to happen on patch. Which doesn’t eliminate the possibility of this “breaking change on a patch” episode happening again. Which leads to mistrusting patch versions, and locking all versions of dependencies.
To mitigate (not eliminate, unfortunately) this problem, I’m releasing ComVer: a simplified SemVer where patch versions don’t exist.
ComVer: Compatible Versioning
With ComVer, if a change to your library is entirely backwards compatible (even observable buggy behaviors remain, a.k.a., “bugwards compatible”), it is a
minor change. Otherwise, it is a
major change. ComVer eliminates the decision paralysis around “small bug fix or new feature or large important API changes?” and instead focuses library authors on answering just one question: “will this new version stay entirely backwards compatible with the previous version?” You only need to consider compatibility.
ComVer is backwards compatible with SemVer itself, so we can reuse any SemVer-compliant system or tool. In a way, ComVer is just a strict interpretation of SemVer. And this isn’t something I am pushing for other libraries to adopt. It is what I will do myself for libraries that I roll out. If other libraries still use traditional SemVer 2.0, then I’ll just avoid using version ranges that make the patch number implicit. It’s nicer to adopt ComVer, though, because when you run
npm install lodash --save, by default it will save it in
package.json with the hat version
^4.16.4 which is vulnerable to breaking changes on patch versions. With ComVer libraries, this won’t happen.
Because ComVer eliminates the patch version, by definition it conveys less information than SemVer does. Both bug fixes and new features would happen on minor updates. To know which one of those happened, you would need to read the Change Log. This is okay, since in many cases with SemVer, you also need to read the Change Log anyway. If a library updates from
2.4.0, what does that tell you? Something new was added and there may have been bug fixes. But which features were specifically added and which bugs (if any) were fixed are unknown until you read the Change Log.
Like SemVer requires the API to be well documented, ComVer requires the changes to be well communicated to humans via a Change Log. I believe version numbers should indicate backwards compatibility only, while other means can be used to communicate progress, such as codenames (e.g. Om Next) or Change Logs (preferably, both). We need to get over the “v2.0” effect, otherwise we will keep on releasing breaking changes on patch updates. Beyond just SemVer or ComVer, we need to get over our hauptversionsnummernerhöhungsangst and look for a reliable and predictable versioning system.
If you liked this article, consider sharing (tweeting) it to your followers.