Spurred by recent events (https://news.ycombinator.com/item?id=8244700), this is a quick set of jotted-down thoughts about the state of "Semantic" Versioning, and why we should be fighting the good fight against it.
For a long time in the history of software, version numbers indicated the relative progress and change in a given piece of software. A major release (1.x.x) was major, a minor release (x.1.x) was minor, and a patch release was just a small patch. You could evaluate a given piece of software by name + version, and get a feeling for how far away version 2.0.1 was from version 2.8.0.
But Semantic Versioning (henceforth, SemVer), as specified at http://semver.org/, changes this to prioritize a mechanistic understanding of a codebase over a human one. Any "breaking" change to the software must be accompanied with a new major version number. It's alright for robots, but bad for us.
SemVer tries to compress a huge amount of information — the nature of the change, the percentage of users that will be affected by the change, the severity of the change (Is it easy to fix my code? Or do I have to rewrite everything?) — into a single number. And unsurprisingly, it's impossible for that single number to contain enough meaningful information.
If your package has a minor change in behavior that will "break" for 1% of your users, is that a breaking change? Does that change if the number of affected users is 10%? or 20? How about if instead, it's only a small number of users that will have to change their code, but the change for them will be difficult? — a common event with deprecated unpopular features. Semantic versioning treats all of these scenarios in the same way, even though in a perfect world the consumers of your codebase should be reacting to them in quite different ways.
Breaking changes are no fun, and we should strive to avoid them when possible. To the extent that SemVer encourages us to avoid changing our public API, it's all for the better. But to the extent that SemVer encourages us to pretend like minor changes in behavior aren't happening all the time; and that it's safe to blindly update packages — it needs to be re-evaluated.
Some pieces of software are like icebergs: a small surface area that's visible, and a mountain of private code hidden beneath. For those types of packages, something like SemVer can be helpful. But much of the code on the web, and in repositories like npm, isn't code like that at all — there's a lot of surface area, and minor changes happen frequently.
Ultimately, SemVer is a false promise that appeals to many developers — the promise of pain-free, don't-have-to-think-about-it, updates to dependencies. But it simply isn't true. Node doesn't follow SemVer, Rails doesn't do it, Python doesn't do it, Ruby doesn't do it, jQuery doesn't (really) do it, even npm doesn't follow SemVer. There's a distinction that can be drawn here between large packages and tiny ones — but that only goes to show how inappropriate it is for a single number to "define" the compatibility of any large body of code. If you've ever had trouble reconciling your npm dependencies, then you know that it's a false promise. If you've ever depended on a package that attempted to do SemVer, you've missed out on getting updates that probably would have been lovely to get, because of a minor change in behavior that almost certainly wouldn't have affected you.
If at this point you're hopping on one foot and saying — wait a minute, Node is 0.x.x — SemVer allows pre-1.0 packages to change anything at any time! You're right! And you're also missing the forest for the trees! Keeping a system that's in heavy production use at pre-1.0 levels for many years is effectively the same thing as not using SemVer in the first place.
The responsible way to upgrade isn't to blindly pull in dependencies and assume that all is well just because a version number says so — the responsible way is to set aside five or ten minutes, every once in a while, to go through and update your dependencies, and make any minor changes that need to be made at that time. If an important security fix happens in a version that also contains a breaking change for your app — you still need to adjust your app to get the fix, right?
SemVer is woefully inadequate as a scheme that determines compatibility between two pieces of code — even a textual changelog is better. Perhaps a better automated compatibility scheme is possible. One based on matching type signatures against a public API, or comparing the runs of a project's public test suite — imagine a package manager that ran the test suite of the version you're currently using against the code of the version you'd like to upgrade to, and told you exactly what wasn't going to work. But SemVer isn't that. SemVer is pretty close to the most reductive compatibility check you would be able to dream up if you tried.
If you pretend like SemVer is going to save you from ever having to deal with a breaking change — you're going to be disappointed. It's better to keep version numbers that reflect the real state and progress of a project, use descriptive changelogs to mark and annotate changes in behavior as they occur, avoid creating breaking changes in the first place whenever possible, and responsibly update your dependencies instead of blindly doing so.
Basically, Romantic Versioning, not Semantic Versioning.
All that said, okay, okay, fine — Underscore 1.7.0 can be Underscore 2.0.0. Uncle.
(typed in haste, excuse any grammar-os, will correct later)
SemVer is a signaling mechanism by which a package vendor can inform the package consumer whether three types of integration issues may arise with a package update/upgrade:
a) Breaking Changes (major),
b) Non-breaking features (minor),
c) Non-breaking non-features (patches).
It is not about describing the actual "extent of impact" on the package consumer, as this would depends heavily on whether the consumer even uses that part of the vendor's code or how tightly that vendor code is integrated into the consumer's system. The vendor cannot know how a consumer will use their code or if the consumer employs a design pattern. The amount of re-work can vary significantly among different consumers. Therefore, it makes no sense to signal the "extent of impact". But only the type of impact.
The terms "major" and "minor" are often misunderstood as the "extent of impact." Perhaps we need other terms.
The consumer's update strategy (version range) determines which type of impact can occur. The extent of the impact must be determined by the consumer themselves. Currently, a tool (dependency manager) cannot determine this on its own.
In the end, the main reason why SemVer is widely used in our industry is that it has helped create a better "contract" (better understanding) of the signaling between vendor and consumer. It's not perfect yet, but it's better than it was in previous times.
If one were to include the "extent of impact" in SemVer, it would need a mechanism that calculates the extent for the specific consumer project, i.e., an individual calculation.
Possibly, future IDEs could handle this. However, tools like dependency managers are not only used by developers but also by build servers, for which the extent of impact is initially (technically) irrelevant. Why burden them with additional complexity?
If one would do that calculation, then a debate will arise on what is the threshold between "big extent of impact" and "low extent of impact" and maybe if we need more fine grained control. A mess.
I guess the future is to attribute all commits, e.g., to tag all git commits with binary flags:
a) "feature/non-feature,"
b) "breaking/non-breaking change."
Then a tool could aggregate all flags from commit "A" to commit "N." Evaluating if "N" contains a "breaking change" and/or a "feature" will be based on all tags between "A" and "N." In the end, this tool could assign the correct SemVer version to "N." An IDE could help correctly attribute (flag) all commits. That way, you could merge all your commits in different branches, do cherry-picking, etc., while still having "automatic SemVer numbers" in your package release process. This tool should warn, when this assessment for a commit is missing.
As SemVer is a "contract" between vendors and consumers, we should focus on trust and how to improve the quality of SemVer numbers (they should be correct at release time)!
We need to find a way for a consumer to figure out how good the quality process for releases is (spotting breaking changes, spotting features, creating reliable SemVer versions).
Nothing is worse than
a) a vendor silently changes the SemVer release number AFTER releasing the package,
b) a vendor did not spot that a breaking change/heavy bug was creeping into this release (rendering the SemVer guardrails useless).
These tool should help us to assess the quality standards and processes of the vendor and help on defining a good update strategy (version range) related to the assessed level of trust.
We may want to have another tool /IDE feature that helps with accessing the "extent of impact" and further support while changing the consumers code base to reflect that vendor change - but this should stay out of scope for a dependency management tool (or signaling mechanism like SemVer).
Its similar to vulnerability assessment (CVEs). CVEs can signal the potential security risk, but it can not automatic evaluate the "extent of impact" in each consumer package, as it may depend on your config, context or surrounding security measures - the CVE may not even apply to your situation. So signaling of security related information within a SemVer number may comes to mind. You may want to add security related info to SemVer:
a) introduces a security issue (specific CVEs), this release should be skipped (do not update to this version)
b) solves a security issue (specific CVEs)
Your update strategy may become even more complex (compared to SemVer-standalone version ranges):
a) skip release when it contains a vulnerability and alter strategy to: "update-to-last-known-good-version" that matches the version range (wait for a fix)
b) skip release when it contains a vulnerability and alter strategy to: "update-to-major-if-fix-requires-a-breaking-change"
c) if already updated to a problematic package: downgrade-to-last-known-good-version and "wait for fix" that matches the version range
d) if already updated to a problematic package: "update-to-major-if-fix-requires-a-breaking-change"
The above is maybe the "exception policy" to your "general update policy".
The very nature of security issues is that these are spotted AFTER release of a package. Same applies to some bugs. So a (SemVer) version number (at release time), can become obsolete when new discoveries arrive (after release).
I have seen vendors updating the packages content while keeping the exact same SemVer version number (breaking the semantic, when content now has new features or now include breaking changes).
I have also seen vendors revoking packages.
I have also seen vendors deleting / unpublishing packages (which i already installed in my project; causing my teams to become unable to reproduce stuff, if we would not cache external packages). Most of the time theses vendor actions were caused by "AFTER-release-events".
SemVer signaling mechanism comes to its limits when we need to handle "AFTER release events".
At least this info should become closer to the code - to the commit - by attributing (flagging) commits as "CVE-xyz-cause" and "CVE-xyz-fix" for documentation purpose (and to be used by IDEs features and future tools). This would help our tools to "suggest to flag a package as revoked" - to connect the vulnerabilty scanner world with the dependency management world even further.
Dependency manager is a class of tools - but being a dependency manager is a craftsmanship skill of a developer.