Pragmatic Versioning – An Alternative to Semver
github.com
github.com
That doesn't make sense to me.
With semantic versioning the way you communicate a LTS version of a package is you declare that in your documentation. Django (which doesn't fully use SemVer, but is still a good example here) does that on this page: https://www.djangoproject.com/download/#supported-versions
"In Pragmatic Versioning, this is solved with a BIGRELEASE version, which has meaning that can be controlled by the package author" - that doesn't appear to address the need for LTS communication at all. It feels like the situation is exactly the same as SemVer: if you want to do LTS you have to represent that outside of the version number.
Marketing departments really want to use that first number for Big Important Releases and only Big Important Releases.
I have seen products get their version incremented from 2.x.x 3.0.0 to indicate that an entirely different product (that could be deployed alongside the first but was otherwise unrelated) was being launched.
I find semver extremely helpful, both as an author and consumer. It may make me think a little for some packages about where I want to pin things, but at least it makes the mechanics of pinning them sensible.
Edit: packets -> packages
It has real drawbacks for on-premise software with commercial support obligations, and shouldn’t be used automatically just because it works well for libraries.
At least with semver they might be forced to say something. I've done that a few times where the marketing version number displayed in the app is effectively just 1.{semver major}.{semver minor} and "just tell us when you want to bump that 1 to a 2 or something" getting the response "well, it hasn't really changed all that much from 1.0, has it?" and then pulling out the receipts of something like 30+ semver breaking differences in X months.
Same project regularly shipped breaking changes in “feature” updates.
Literally calling the numbers 'breaking' and 'feature' would have made things more clear.
> BIGRELEASE version, which has meaning that can be controlled by the package author
So BIGRELEASE doesn't actually convey LTS at all. Meaning, Pragmatic Versioning does not solve LTS any more than Semver does.
There is no way to tell which ones in a set of equally major versions are LTS or not using version numbers because there are often too many; for example Java has major version numbers for major language features and standard library additions and removals, with reasonably relevant versions currently spanning from 8 (1.8 in the old numbering) to 21, three or four of them LTS and the others transitional.
There is also no way to tell obsolete LTS versions from current ones using version numbers, without explicit documentation committing to dates or making EOL announcement.
BIGRELEASE.MAJOR.MINOR[.PATCH]
That way one can lock the version to get only actual bugfixes, with no other change: gem 'foo', '~> 2.3.4.5'
would get 2.3.4.5 and up but none† of 2.3.5, and it's up to the project to decide if they want to do any release like that.† excluding any 2.3.5 prerelease as well
-- PVP summary: +-+------- breaking API changes
-- | | +----- non-breaking API additions
-- | | | +--- code changes with no API change
version: 0.0.0.1
[0]: https://pvp.haskell.org/vMAJOR.MINOR.PATCH[.TWEAK]
Indeed the TWEAK is only for hotfixes, though due to pushback from other devs using semver parsers, we've had to hold back on the tweaks. (Though it’s awkward in a hot fix situation.)
But I like the names the repo author proposes.
So, TFA's statement that "In Semantic Versioning, there is no way to release or communicate a LTS/"long term support" version of a package" does make sense to you after all.
You can't do it in the versioning, you need to do it in the docuementation.
I dissent, it is a pseudo-specification that looks like a proper specification but isn't. Many proper specification starts with a glossary because you can't get everyone to agree on common words that don't need a definition. Let me give some examples.
• SemVer never defines what the "increment" means---is an increment by more than one allowed or not? Can one release a minor version of given major version (say, 1.2.0) when a higher major version (say, 2.0.0) has been already released? If it's allowed, how should later versions (say, 2.1.0) be related to aforementioned versions?
• SemVer says the "contents" of once released version must not be modified, but unlike "API" which is a variable at the software's choice, it is never mentioned elsewhere---does the contents include a documentation or not, say?
• SemVer explains how to compare pre-release and build identifiers separated by dots, by dividing them into "identifiers consisting of only digits" and "identifiers with letters or hyphens". And then it mentions "numeric identifiers" and "non-numeric identifiers"---which one is which? The former should be numeric, okay, but the latter may still contain digits so is it "non-numeric" after all?
Imagine a project which consists of a library, a CLI tool, and perhaps a GUI.
* Is the library API part of the public API? That's for you to decide according to SemVer.
* Is removing/renaming a CLI flag a breaking change? Also for you to decide.
* Is removing major functionality from the GUI a breaking change?
..you get the picture. Every project defines 'their public API' for themselves.
This isn't a made-up example, many projects have a library and a CLI, with one considered to be 'the main project' while the other is something most people are not expected to use.
Heck, even following SemVer to the exact letter often doesn't communicate information how people expect. For example, say your library exposes a piece of metadata from the underlying OS: a function returning a `string`. But in Windows 10 -> Windows 11, they changed the API to return an `[]string` instead of a single `string`. If you want your software to run on Windows 11, you have no choice but to change your public API in a breaking, non-backwards-compatible way.
Cool, no problem, according to SemVer we bump our public API version from v1.0 to v2.0 now, right? But wait! 99% of our users do not even make use of this one-off weird function! In fact, we think we could remove it without anyone complaining but it's technically part of our public API..
SemVer has no way to express 'we had to make a breaking change that doesn't affect 99% of users', it just has a way to express 'breaking change'. If you follow SemVer, then that means your project needs to go from v1.0 -> v2.0 in that case, yes - and now you get to explain to your users why v1.0 -> v2.0 is not, in fact, a 'major change' despite it being defined as such semantically.
What most people think of as SemVer, in practice, is often _romantic versioning_ or _sentimental versioning_ and not _semantic versioning_.
Package A is SemVer 1.2.3, package B is PragVer 1.2.3. both A and B have big release with breaking changes and marketing hype, so now they're both at 2.0.0. It is later found that in version 1 of both packages there is a bug requiring a breaking change in order to fix. B (pragmatic) can apply the fix to version 1 branch and release it as 1.3.0. How does A (semantic) make a breaking change on version 1 and version it correctly?
TL;DR Pragmatic Versioning enables breaking changes on LTS versions.
If I do a big release, but API hasn’t changed, am I going from:
10.50.1
To
A) 11.50.1
B) 11.0.0
?
https://github.com/semver/semver/issues/771
SemVer is a product of Ruby community.
In 2000s Ruby library authors were breaking compatibility left and right, neglecting elementary compatibility practices. If you were working on an application, every time when you update dependencies, the application would break.
So (in 2011 ?) they came out with this "manifesto" (Why such a big name? This scheme of versioning was well established in linkers and sonames of Unix-like systems for decades - it goes back to at least 1987 paper "Shared Libraries in SunOS").
It's a good thing SemVer acknowledged finally that compatibility is a serious matter. Only that it's better to discourage compatibility breakages. An in cases when it's really needed (I agree such cases exists), there are things to take care of in addition to simply increasing major version num.
> Then commons-logging changes its API incompatibly and is released as commons-logging 2.0.1. Authentication adopts commons-logging 2.0.1 while other libraries still depend on 1.1.1
> Now my-application is broken, because the dependency tree includes two versions of commons-logging which share packages, class / functions names, and thus can not be loaded simultaneously.
I absolutely don't see how this is a problem with semver, it is not the responsibility of semver to tell a language how packages should be isolated and loaded. That is a problem of a) the language and b) dependency resolution in the package manager.
> SemVer is a product of Ruby community.
Bundler, by design, does not allow the above, instead having a flat, consistent vision of dependencies.
NPM though, allows that, allowing nested dependencies, by virtue of the ES6 module system importing to a variable in a lexical scope. Go also allows that, by virtue of its imports being scoped to a package (or file, I can't recall).
Ruby has side-effectful load/require decoupled from the open class system - which is what bundler is designed around -, but can do that kind of isolation too! In fact, I've done it: https://github.com/lloeki/pak
Unless packages leak to globals each version is oblivious to the one next to it. Unless package dependents communicate with one another using objects from the packages they can happily live in their own little isolated world. Now if they do, then it's like hitting a HTTP /api/v1 with an HTTP /api/v2 client and somehow wishing things will work. Either the package (which should not leak globals / disallow cross-version communication) or the language (which should not allow leaking globals / detect incompatible communication).
None of this is the responsibility of semver. In fact, semver would help the language provide tooling to detect that kind of "hey this instance is from foo-1.0 but you're trying to consume it in foo-2.0".
Strange to not see it. SemVer promises to solve dependency hell. In the example everyone correctly followed the sevmver and the app is broken by a dependency hell issue.
> it is not the responsibility of semver to tell a language how packages should be isolated and loaded. That is a problem of a) the language and b) dependency resolution in the package manager.
So semver only works for "good" languages?
> Bundler, by design, does not allow the above, instead having a flat, consistent vision of dependencies.
Ok, so what happens with the app when packages managed by Bundler get fragmented into sets depending on incompatible versions of sub-dependency (commons-logging 1.1.1 vs 2.0.1 as in the example)?
Also note, even for languages and tooling supporting multiple library versions loaded side by side, there are scenarios where things break.
For example, the "libc apocalypse" situation in Rust https://github.com/rust-lang/libc/issues/547
Here after the "libc" module released a major version, the definition for the `void` C type in two versions of the lib are considered by the compiler as two different types, resulting in breakages everywhere around the library ecosystem.
There are also scenarios for dynamic languages / runtime errors.
> None of this is the responsibility of semver. In fact, semver would help the language provide tooling to detect that kind of "hey this instance is from foo-1.0 but you're trying to consume it in foo-2.0".
And what's next after it detected the dependency hell? It's too late and the person suffering is not in the position to fix it. You have to upgrade to "authentication 1.1.2" for security compliance, because the version 1.1.1 has known vulnerabilities. But that breaks the application, because the maintainer of the lower level dependency "commons-logging" happily breaks compatibility, proud of himself following the engineering best practice SemVer and increasing the major version number. What would you do? Leave the vulnerable lib in the app? Patch the libraries?
The promise was to prevent dependency hell, not to detect it.
Quoting the ticket and reiterating the point of my first comment above:
> Once again, the point of this ticket is to:
> 1. Remove the false promise that SemVer solves dependency hell by simply increasing major version.
> 2. Discourage unnecessary compatibility breakages, when it's trivial to maintain compatibility.
> 3. Several sentences of advice for the cases when comparability breakage is really needed.
Bundler immediately yells at you, pointing out which transient dependencies are in conflict as where these dependencies come from, preventing the issue altogether.
> But that breaks the application, because the maintainer of the lower level dependency "commons-logging" happily breaks compatibility, proud of himself following the engineering best practice SemVer and increasing the major version number. What would you do? Leave the vulnerable lib in the app? Patch the libraries?
Either commons-logging backports the vulnerability on a 1.x-stable branch and does a release or it tells people to move to 2.x through and through - because many FOSS maintainers do maintain stuff on their spare time out of pure good will and cannot maintain stuff endlessly til the end of times; the dependency that continues depending on 1.x is a liability and should update to 2.x.
> Strange to not see it. SemVer promises to solve dependency hell
I would argue that it does when appropriately wielded by a competent dependency manager, I have not suffered dependency hell in Ruby for a long time. Sure there are occasional "oh there's this lib that needs an update oh wait I need to update this one oh wait it's depending on an older one oh wait this one is not compatible with ruby x.y" but I've not been hitting those for a long time, bundler doing a very good job at resolving things to compatible versions and people being largely diligent with semver (or variants and approximations thereof), and then the alternatives are:
- either you have a huge monolith dependency that does everything, IOW give up modularity
- or once a version of anything is released then breaking changes are forbidden forever, IOW give up major changes
SemVer does not solve dependency hell by itself, but it sure helps a ton in smoothing communication between library providers and consumers in a very synthetic way.
I don't care about the branding, I just want my code to work. (Disclaimer: I am obviously not in marketing.)
Even if an API stays exactly the same, something about what it returns could subtly change, so there's really no replacement for extensive testing.
This means the API did in fact change. But you're right of course, sometimes we pick the wrong number to bump (major vs minor vs patch).
For Typescript in particular, which is nothing _but_ a type system, every change is going to be a breaking change.
That is why you have major.minor.patch semantic versions. The documented and official contract is according to equal to major versions, and greater or equal to minor versions (and possibly the patch number as well); if you depend on a specific bug or internal function then you should use the exact full version number only, in order to avoid such a problem.
A bug would be one example, but another example might be the sqlite3_test_control function in SQLite.
[0]: With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
The result though is the same, semantic versioning or no: If something changed you must retest. This is why all that’s needed is a “do I need to retest?” flag. Major/minor/patch is irrelevant if you need to keep things working.
Where I work, we call that a breaking change. If a consumer of our API codes for a particular response, and we change that response, then we could break their application.
No one wants to have to validate deeply nested objects to test if they do indeed return the thing we promised to return in our contract...
The difference with bugfixes is that pre-bugfix the intended API contract - including behaviour, not just signature - was not respected by the implementation and post-bugfix the implementation becomes (more) correct WRT the intended contract.
IOW:
- On a semver patch bump the API contract does not change but the implementation of that contract was incorrect.
- On a semver minor bump the API contract receives an addendum, the previous contract is still in effect as a subset of the new contract.
- On a semver major bump the preexisting API contract is voided and is replaced by a new contract. The new contract may or may not include all or part of the old contract as a subset. Breaking change => major bump is an implication, not an equivalence: one is completely free to bump major purely for marketing reasons.
Of course, the difference between theory and practice is that they're the same in theory: the contract is an ideal, the implementation may come close but will always fall short in some way.
I feel like many of both semver detractors and advocates get lost in theoreticals, reality is fuzzy, just use the damn thing. Or don't and pick up another scheme, but be abundantly clear about how your versioning scheme works so that users can make sense of it and automate version requirement declarations in their dependency manager.
I think the confusion over what constitutes a breaking change stems from the fact that, in practice, contracts are often very underspecified, sometimes to the point of being omitted entirely ("This function just does the obviously correct thing"). A classic example would be whether an API that munges filesystem paths should resolve symlinks or not -- if this is unspecified, then changing that behaviour is technically not a breaking change, but is likely to upset some fraction of your users, who may have even done some experiments to find out what the code does, and assumed that that undocumented behaviour won't change.
(This is not an argument against semver at all -- just an observation that contracts are hard, and mostly need to be much more detailed than they typically are, and this "detail gap" seems unlikely to ever completely go away.)
Another example would be Ruby 2.4 changing String#upcase & friends from handling ASCII only to being unicode aware with a new argument to override and changing the default. Technically a breaking change but actually silently fixes a ton of bugs.
The best thing by far about semver, if accurately followed, is that it encourages strict and accurate versioning based on what changed.
I've seen teams use both approaches, side-by-side, the willy-nilly pick a version as you please group offered a significantly less stable product.
Looks to me like Semantic Versioning is the worst form of versioning except for everything else we've tried.
Personally, I do like the Haskell standard a lot. People tend to not use the extra number a lot, but just because it exists the other ones are much cleaner.
Or you could use the one on the article.
Most people are nowadays used to versioning that's something like BIG.SMALL.IRRELEVANT, where those words basically indicate how much has been changed in a release. To the layman, "version 3 has released" usually is accompanied by the question of "and what's the new stuff that made them make a version 3". BIG doesn't necessarily have to be the first number; OS X for example does all major changes on the second number, so when that goes up, new features are expected, while the last number is reserved for bugfixes.
In semver, this question is very boring - you have to bump MAJOR every time the public API changes, so most MAJOR changes don't mean much. To try and solve this, most projects have decided to just forgoing MINOR and PATCH entirely (or going for YEAR.MONTH versioning, and trying to keep breaking changes to the next year) or excessively holding back new features to then merge them all at once.
This tends to result in semver software feeling... rather stagnant (or on the other side, impossible to say anything about due to constant major changes) and is why for non-libraries, I don't think semver is all that useful. Pragmatic versioning seems like a closer way for how end-user software should be versioned.
There's also the fun situation going on with cargo where almost every package is still at 0.x because rust programmers love semantic versioning but want the escape hatch to do major breakage without bumping MAJOR. Which isn't necessarily indicative of the actual state of the project (most 0.x rust projects are afaict actually well maintained and ready for production use), but it does show how semver can really mess with expectations.
Version numbers are indeed for communicating with your users, and in particular where that communication is a one way broadcast. In my career I’ve found that, when version broadcasting is used inside a team, it’s a sign of dysfunctional communication. Even at FAANG scale I encourage groups of people to colocate their code and collaborate on trunk in preference to putting up hard boundaries between projects — namely having to cross repositories and integrate with each others versioned releases.
There’s no point hampering yourself with the one-way broadcast mode of communication unless you absolutely have to. When you have multiple teams one-way broadcasting at each other then, well, maybe it’s time we sat down for a chat instead?
1004.0.0 to 1005.0.0 is an api-major business-minor version bump (incompatible API). Semver-compatible.
1004.0.0 to 2000.0.0 is an api-major business-major version bump. There, the first number has changed, if that's important to you. Semver-compatible.
1004.0.0 to 1004.1.0 can be the "INCREMENT" thing they also mention.
As a bonus, your version number is soon going to be much higher than competitors. That's sure to impress clients. You can refer to the 9000-line easy enough.
* 1.4.0.0 to 1.5.0.0
* 1.4.0.0 to 2.0.0.0
* 1.4.0.0 to 1.4.1.0
There is no best tool for all jobs.
Write down your communication objectives and pick a versioning scheme which fulfills them. Want to communicate api compatibility? Use server. Want to communicate a new version worth at least checking out? Bump that major version.
{
“version”: “1.2.3”,
“href”: “https://host/1.2.3”,
“not_before”: “2000-01–01T00:00Z”
“not_after”: “2025-01-01T00:00Z”
}
The array lists the available versions of the api and the endpoint to use. Not before/after can be null to indicate infinite past or future.The contract for /versions is that the client must check it once every 24 hours for changes.
Allows deprecation of previous versions by changing a not after of null to a timestamp etc
This is so that you can specify ranges of api versions that your program could be built with. For reference, here's where I took my understanding: https://semver.org/
Pragmatic versioning reflects more closely what many mature projects do in practice. Feedback welcome!
With semver you live in this world where you are promised the versions to follow semantics, but human nature creeps in and we introduce breaking changes (like bugs!) without setting the version correctly.
With pragmatic versioning you give expectations but also convey that to upgrade you need to test that your software still works.
Wouldn't Pragmatic Versioning work for both?
For instance, semver increments field values when the state of the API or level of compatibility with previous versions changes, or maybe just when something about the build has changed.
This thing appears to aim to express changes in features and functionality. Less about the mechanics of software assembly and more about reasons the vendor would want users to update.
It describes issues with semver similarly in terms of marketing, which might explain what seems to be some cognitive disconnect among the different ways people seem to be interpreting both systems. Semver speaks developer language, while this thing seems to speak product manager language.
Not sure how it’s differentiated pragmatically, except perhaps for the practical purposes of a specific role in a vendor’s organization.
Marketing-Driven Development? Yuck.
1. There was a breaking change to the API.
2. The company wants to signal great new features.
With pure Semantic Versioning the second reason can not be used alone. And breaking changes in the API are actually not good news for the users.
I think semantic versioning is adopted broadly because great new features often also introduce breaking API changes. So that conflict does not need to be resolved.
Pragmatic versioning helps solving this conflict by splitting um the "major" version number into two new ones for breaking changes and great new features.
The main advantage Semantic Versioning has is that it is the default. So seeing a version tag with three numbers means Semantic Versioning today.
A is a big major release, a significant step forward in terms of how you interact with the product - the difference between 1.b.c and 2.b.c should be fairly apparent.
B is for Breaking changes within a release - if it used to work one way in a.1.c, you shouldn’t rely on it working the same way in a.2.c
C is for non-breaking new features and bug fixes - if you’ve been using a.b.1, it should be totally safe to switch to a.b.2
Not sure if that was the best approach, but it’s sort of what we settled on organically and nobody complained so
It helped that our users were mostly our client’s counterpart developers, so they were pretty looped into our release cycle anyway.
that never quite caught on. It's basically just a timestamp. It requires full backwards compatibility of public APIs and behaviors (side effects) for the lifetime of the package. Read about it on an interesting 2019 blog post about monorepos and versioning: https://hub.packtpub.com/why-dont-you-have-a-monorepo
You've released package foo version iv2019.01.01 and now need to make an incompatible change. You mark foo as deprecated and release foo2 version iv2023.12.06.
That is not an improvement.
The purist in me balks, but the pragmatic cynic is happy to have a tool in the belt.
Breaking.Planned.Hotfix.Publish
Breaking is for breaking changes.
Planned is for planned changes.
HotFix is for emergency unplanned changes.
Publish is internal, in the event getting the release out the door took multiple kicks at the build server and you still want to give those distinct builds their own tags. There's a lot of tools that are very opinionated that anything that hits public must have a distinct ID, and sometimes stuff happens and the pipeline doesn't go right on the first try.
One project I'm on has an internal version number of around 28-dot-something-something. This is the number for techies, and it conveys some information that end users are fully ignorant of. End users see "Edition 2" in stead (soon to become 3)
Modern software projects have too many dependencies, and too much noise coming from each dependency. Your project is likely just one of hundreds of libraries that got pulled into my dependency graph for some reason. I want to know if, and only if, there's a change that I need to be aware of. As for that shiny new feature that you're really proud of, I shouldn't even notice it unless it breaks an existing API.
This is about the version of the underlying implementation.
“LTS” of a semver api is driven by backwards compatibility, so it’s the second section (1.2 vs 1.3).
Declare the “not before” and “not after” of the semver and you have effectively defined lifetime.
(Now, why things that don't do Semver don't just do DateVer in lieu of that… IDK, really. Marketing likes to take advantage of it? …& then you get stuff like the OP here.)
Especially those which currently use the major.minor.patch format but don't care at all about what it usually means, that in itself can be fine but using such a version scheme is then annoyingly misleading.
What I’ve never seen, but seems useful, is to throw the date on the version number as the third or last number. The nice thing is you get the ordering back. Something like 4.3.20231204