SemVer has failed us
jongleberry.com
jongleberry.com
You should be fast to get to 1.x.x. If your app breaks on a patch release (that was only a bugfix and provided that interfaces stayed consistent), maybe you shouldn't rely on buggy behavior and maybe you should fix your code.
This isn't a failing of semver--this is a failure of being responsible engineers.
EDIT:
The best example from the article is the bit talking about how people interpret 0.x vs 1.x vs 2.x. Folks, it's just a damned version number, and a number that only indicates you may have to refactor you application to handle changed interfaces. Any other psychological baggage you attach to it is just that, baggage. Aaaargh.
Yes, people _should_ get over changes in version number, but in reality they don't. So library authors cater to reality, not how they wish things should be. That is the problem, and why semvar is broken. Not in principle it isn't, but in how it actually functions in the hands of humans.
But there's a lot more to the world than package managers, and expecting people to do things the way that's most convenient for package managers is just a little bit silly, no? Package managers are there to serve us, not the other way around. So maybe instead of "fixing" version numbers so that they're the most convenient for package managers, maybe we can fix package managers so that they use something better suited to handling dependencies than version numbers, so that we can use version numbers to communicate with people, not just package managers.
But I do agree that package managers are there to serve us, not the other way around. The unfortunate thing is this issue is one of differences in human nature and how a given person prefers their system to be - rigid and concrete maximizing efficiency; or fluid and communicative adding overhead but increasing understandability.
- patch: forwards and backwards compatible – only bug fixes, no new features, no changing existing features.
- minor: backwards compatible – new features allowed, but old code should keep working.
- major: all bets are off.
This is pretty much what the SemVer spec says, and it's hard to imagine what else you could do. The significance of forwards compatibility for patch versions is that if you bump a patch version and find that it was problematic, you should be able to revert back to a lower patch version without breaking new code that's been written since the bump.
The thing is, it's still a judgement call what constitutes a bug fix. If you're fixing a segfault, that's a no brainer – no one (sane) was relying on something segfaulting. If you're changing a behavior from one potentially reasonable behavior to the one that was actually documented, then, yeah, that might not be "just a bug fix" – people might be relying on it. So we're super conservative about "fixing" even fairly bad behavior in patch releases. Still, the only way to make sure an application keeps working exactly the same is to use the same exact versions – down to the patch level.
The FerVer "spec" is barely coherent, I'm not sure how anyone could consider that something to follow. SlimVer basically just gets rid of things in SemVer that you don't have to use anyway. Pre-release versions – especially release candidates – are incredibly useful. You tag 1.2.3-rc1 and if no issues crop up in a week, then that becomes your 1.2.3 release (1.2.3-rc1 and 1.2.3 are exactly the same); if issues do crop up, then you fix them and tag 1.2.3-rc2 in a week and repeat.
I also think that pre-1.0 versions are useful: just treat the minor version like a major version. For 0.1.2 => 0.1.3 you only fix bugs but for 0.1.3 => 0.2.0 anything goes. There's no point in doing feature-only, backwards compatible releases while you're still working out the API for something, and 0.x releases are for precisely that kind of exploratory period.
So what should upstream do? After all, the premise must be that the upstream does not control or even know about all the clients! The entire point is to separate the concerns, so how could upstream know if a bug fix is a breaking change if it passes the internal regression tests?
Of course they can't know without asking downstream, and hence the use of release candidates.
I don't and wouldn't even really use it anyways. Easier to just follow semver and bump minor instead of patch if there's a remote possibility of anything breaking
Regarding patch releases, the only thing that has "absolutely no chance of breaking anything" is no change at all – keeping the exact same version. Deciding what constitutes a patch-release bug fix is a matter of exercising good judgement about what won't (reasonably) break people's code. That could definitely be clarified in the SemVer spec. Asking yourself "is there a chance that this change might screw someone over?" and if so, how crazy was what they were doing?
1. Don't depend on 0.x.x version modules unless you are willing to accept the pain of updating versions constantly.
2. Bump major versions liberally.
If enough people just get used to this reality, then everything will be fine. The problem is trying to impose artificially conceived notions of how things "ought" to work on top of this.
Until someone invents a viable system for checking invariants and interfaces on codes statically, semver is pretty much the only show in town for specifying compatibility between code modules.
For example, IIRC, Rails 2.3.5 introduced some API changes. That should have bumped the major version.
Rails 3.0 was a "rewrite your code" type bump; that should have been signified with a name change or other similar type of marketing codename. Rails+, for example.
I was really disappointed that Linus went with "Linux 3.0" rather than "Linux 40" as the version following 2.6.39 when he finally acknowledged that the '2.6' no longer had any meaning.
Unfortunately our software isn't well-specified enough to allow the computer to determine version numbers automatically yet. To make this a reality, you'd need Haskell-level type safety, along with specifying other constraints, like "this fn is O(log(n)) or better".
>If you're pinning versions, you're reducing the package manager to a glorified CURL
I'm actually quite happy with this. I realized it when a few weeks ago, bower broke for me. Turns out, a grandchild dependency of bower failed to follow semver. This means that it's not enough for you to follow semver, every single dependency of all of your dependencies have to follow semver as well.
Isn't this going to be impossible for the same reason the Halting Problem is unsolvable?
Is it possible to specify the complexity of every arbitrary function? Probably not. Is it possible (and useful!) to specify the complexity of most functions you use on a daily basis? Absolutely.
A stronger critique I heard once is that 'computer generated complexity analysis has too much noise in it', where you ask what is the complexity of this fn, and rather than giving you O(n^2), it gives you back O(n^2 * m * q * log(r) * s^4), but it turns out 's' is "always" a small number, and m & q are constants. You can improve (but not completely eliminate) this by doing empirical testing, i.e. "the statistical sample of 1000 test runs varying the input does appear to fit an O(n^2) curve".
These minor version bumps scare library developers, especially people that produce libraries that depend on libraries that depend on... and so on. So packages fail to build too often because of constraints. So the irony of this thorough process for package building is that humans have mucked it up and made it break builds. In almost every case in which semantic versions blocked some update from occurring and caused my work to stop, it was because I needed to bump the constraints on some third-party package. It's a boring process that I've only become too accustomed to:
1. try to build with updated libA 1.1.0-foobar
2. libB depended on libA < 1.1
3. download source for libB
4. update constraint and repackage libB locally
5. rebuild main application
This happens just about every time there's a new version of any major piece of Haskell software.
If we really want to move into The Future, we need to start versioning individual modules or functions, instead of whole suites of software. But the cognitive burden of doing this is very high, and the software to do it hasn't been written yet.
[1] The Haskell community doesn't technically use semver, but rather a related policy called the package versioning policy. That's a nitpick though, and doesn't affect my argument.
I don't think this problem is related to any cognitive burden, but is an artifact of how the package manager works and how easy it is to liberally publish packages. Consider npm, where people publish packages with little to no coordination and everything is mostly fine (whatever the author of the parent article says). Because it's so easy to make an npm account (anybody can `npm adduser` and start publishing immediately) and so easy to publish (just pick an unused name, make a package.json, and `npm publish`), people routinely make very granular packages, often just a single function. Importantly, these functions don't need to fit into a taxonomy namespace like in haskell which prevents namespace hierarchy turf wars for the lesser price of namespace landgrabs.
Better still, you can have multiple different versions of a library in the same application. In your example, libB would just get the version of libA that its constraints satisfy but elsewhere other libraries could depend on a version of libA that do not satisfy the constraints on libA that libB requires because every package gets their own copy of their dependencies, with automatic deduping when the constraints are satisfied to save space.
The first notion is needed for keeping our code stable but keeping up with bufixes and security patches. The second one is needed to deal with design issues: will this change require a lot of reworking? Will it change the underlying model or abstraction in the library?
Both of these are important. But because Semver requires bumping for any breaking changes—even if they are semantically minor—it can only address the first, low-level notion of versioning, not the second.
A possible solution? We can have two minor version numbers:
4.1.0.2
Patches and backwards compatible changes work as always. But if you are making a breaking change, you have two options. If it's not too big, just increment the first one: 4.2.0.0
But if you're really changing things up and creating a new generation of your library, go for 5.0.0.0
Of course, the difference between the two major numbers is not well-specified. But that's fine! From the more technical, low-level perspective, it's no different from normal Semver: any change in the major version (either of the two numbers) can be breaking. It just also lets library authors send an additional signal to developers about the scope of the change.For example, this scheme gives more information on how "stable" a library is. If the second major number is high, it means there are a lot of minor breaking changes; it would take a bit of active maintenance to keep up, but as long as the first major number is not changing, these steps won't be radical.
Personally, this approach makes a lot of sense to me. It's also more or less what Haskell packages use according to the "package versioning policy", and it seems to work pretty well. (There are some other pervasive issues with Haskell version numbers, but not related to how they treat major versions.)
For smaller projects and libraries, it works very poorly, because these are usually "itch-scratching" efforts that don't necessarily have a clear endpoint or goal. For these kinds of packages, I wish people would just use a commit number. I really hate having to agonize over SemVer for a 500 LOC miniproject.
That kind of project, incidentally, is the most likely to be in "0.X.X" limbo forever. See for example the Clojure ecosystem where 80% of packages are this way.
I propose 4Ver. Just add a marketing version to the beginning. Update that one whenever you want (may include breaking changes). Since you have 4 numbers, you can remove the exceptions for the 0th version as well, and make version 0 follow the same rules as everything else. This cleanly separates the marketing/naming aspect of versions from the semantic aspects.
For example: https://github.com/bower/bower/issues/1404#issuecomment-4876...
I unfortunately can only complain, I have no solution to offer.
Of course there will be lots of pain, it just takes one package to slightly violate SemVer and you'll have to hunt down the culprit.
What surprises me is that in the article they seem to think that switching to another versioning scheme will help with this problem. Especially as better tools would mitigate most of this problem.
This is just more of the same marking of territory that Node is kinda famous for.
When a product is not principally an implementation of a single interface, including notably the case where it exposes multiple interfaces that are somewhat orthogonal (as might be the case, e.g., with an RDBMS where the SQL language support, the TCP/IP interface, etc., are all interfaces which might sensibly be versioned), it probably makes sense to use separate semver-like versions for the individual interfaces and something else for the product.
Language implementations naively seem like really good SemVer candidates by that criteria, but the sibling comment [1] addressing binary vs. source compatibility, etc., reveals how such implementations can present multiple different perspectives that are problematic.
I worked in one team, and there was a contractual requirement that v 1.16 was the last version we would ship. So, where before we would increase the number at certain steps, we stopped at 1.16 and shifted to a third digit. Since we were a continuous build shop, we ended up with 1.16.110 very quickly. At that point, we shifted from 3 digit versions to a version number which was marketing controlled and a build number which was controlled by engineering (hashed from the tag+date).
This pattern repeated over and over. The version number would be communicated to the customer, a new version would be needed and so the version number would grow a digit. It ended up at X.Y.Z.A.B rev C. Yes, it was silly.
So, no matter what you use, make sure you keep your externally visible version numbers separate from the numbers you use to talk about them internally. :) Internal numbers should remain completely opaque to the customer.
In general, it's always better to have a computer enforced standard over a human enforced one. Semantic versioning can just become human readable sugar over a set of contractual expectations.
Other kinds of specification would seem very hard to test, for example rendering a 3D scene, if we used a bitmap as test data then we'd get false positives when the rendering is improved in some minor way, and the spec will fail, we'd need to use some perceptual metric for image similarity instead.
Version numbers aren't so bad, given their ease of use.
Semver is a way to communicate between people about compatibility expectations they should have between two versions of a package. When library maintainer says "this version changes from 1.2.3 to 1.3.0" that tells something useful to a human consumer of that package.
It absolutely does not communicate anything precise in the language of machines. It's not a formal system, and compatibility isn't formally defined. If you want it to be that, you're setting yourself up for hearthbreak.
Code reuse is a social process. Understand that semver is a tool at that level and you'll be pretty happy with it. In my mind, the most important feature of semver is that by associating pre-arranged semantics to versions, consumers can specify version ranges before those versions exist.
If I'm using foo 1.2.3, I can depend on foo >=1.2.3 <1.3.0 with decent confidence even though 1.2.4 may not even exist yet. That sort of forward compatibility makes the package ecosystem a lot more resilient to changes over time.
Part of the major issues with semver is that patchs could
break. A feature could be "fixed", but a consumer could
have relied on that very buggy feature, and patching it
would break their app.
This is true. One man's bug is another man's feature. If you want to be fully strict, all of the behavior of a program is significant which means you can never release anything that isn't a breaking change.Being that rigorous isn't useful. Instead, realize that most patches don't break most users. For the ones that do, that's why you pin. You are pinning, aren't you?
Versions < 1.0.0 do not have semantic versions according
to semver. These are considered libraries "in development"
and developers could version however they see fit.
This part is straight up annoying especially as the web moves to a world where damn near everything is <1.0.0. On the Dart team, we've adopted a policy that for sub 1.0.0 packages, we do semantically version them. We just shift the numbers one to the right.So, going from 0.1.0 to 0.2.0 is a breaking change, 0.1.0 to 0.1.1 is an API addition, and 0.1.0 to 0.1.0+1 is a patch.
It seems to work really well for us.
The current solution to the above problems is to pin all
dependencies. However, this is absolutely stupid and
annoying to me. ... When every library pins, you're going
to have a lot of duplicate dependencies, even if they're
the same version!
No no no no no.You don't pin versions in libraries, you pin them in applications.
Almost every package in the dependency graph should be using version ranges. That avoids version lock and makes it easier to satisfy shared dependencies. It also means libraries don't all have to bump patches that just change dependency versions.
But, in your application, in the root of the dependency graph, you pin everything that you're using right now. You check that in to ensure that everyone on your team and every machine gets the same versions of all of the dependencies.
You get good code reuse and deterministic builds.
This is exactly why Bundler separates out the lockfile from the Gemspec. It's unintuitive at first, but it works better than any other system I've seen once you grok it.
There is one think in semver that I think is totally inane. The latest version of the spec says build suffixes have no relative ordering. We don't do that in pub because it makes no sense. If a user has uploaded foo 1.0.0+a and foo 1.0.0+b, which version should an application get that depends on foo? Pick randomly?
In languages where that difference matters, let's say Java/Scala [1], there are way too many possibilities:
- bug fix: forward and backward binary compatible; basically a drop-in replacement. Package managers are supposed to resolve conflicts between those automatically,
- new feature, but backward binary compatible; just some new code alongside the old. Package managers should also be able to resolve a conflict between dependencies that depend on different versions of those.
- bug fix: forward and backward source compatible; here we have a problem: we can guarantee developers that they won't have to edit a single line of code to migrate, but we can't allow (binary) package managers to do anything. They have to rebuild their library, which means bumping their own version number. And does that means that their new version becomes binary-incompatible with the previous one, or just their dependencies?... who knows? Maybe it depends on if the consumer of that library calls into the conflicting dependency directly or only uses it indirectly? Maybe not? Can we tell?
- new feature, backward source compatibility, but no binary compatibility; same as above: package managers cannot do anything, and conflicting transitive dependencies on the new and the old may or may not be solved by recompiling one's library. The only safe bet is to wait for the dependencies to upgrade.
- all bets are off (possibly split in minor and major): here at least we know there is nothing to be hoped. But in practice after upgrading to the new API, you are faced with the same questions concerning that thing's dependencies...
And if every dependency manager within the same packaging eco-system doesn't enforce a single global versioning scheme, how can they handle any dependency version conflict anyway? They could if packages could auto-describe their semantic versioning, so basically they can't, so they don't, and we're back to source dependencies, with recompile and bump in version number for every dependency change (ok, sometimes they do try and we get JAR hell...). And since a single versioning scheme isn't enforced, people use hundreds of different ones (or just none), and the maintainer of a library has to make all of those decisions, recursively (except that below 1 level of dependency, he/she might not be able to do anything as long as even a single dependency keeps an old conflicting dependency around, but they still have to check, in case they are lucky and can do something), taking into account the different versioning semantics of each dependency. That's madness.
So in the end, if we get a conflict between dependencies far down in the dependency tree, it becomes something pretty ugly:
- if guaranteed binary compatibility, everything is fine,
- otherwise:
0) look at that dependency's official versioning scheme, usually find nothing (except for big famous libraries)
1) pick the highest common-one, try to recompile your library
2) if it works, try the tests (your library's tests, not your dependencies' tests, mind you. Even though those should probably be run, considering they directly depend on it, and use (and hopefully test) more stuff from that library)
3a) it it works, ship it and pray.
3b) it will probably fail somewhere since the dependencies in between have not be recompiled; then you can only wait for them to upgrade.
I know in the end this doesn't work so bad (well, everything is relative...), but it still makes me sad that upgrading dependencies is still human-based trial and error.
Oh. I guess this is a rant.
[1] that's just what I am the most familiar with, but you could say the same with C packages in a distro's package repo, for example.