Skip to content

Instantly share code, notes, and snippets.

@jashkenas
Last active November 22, 2024 04:13
Show Gist options
  • Save jashkenas/cbd2b088e20279ae2c8e to your computer and use it in GitHub Desktop.
Save jashkenas/cbd2b088e20279ae2c8e to your computer and use it in GitHub Desktop.
Why Semantic Versioning Isn't

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)

@RobSmyth
Copy link

If your package has a minor change in behavior that will "break" for 1% of your users, is that a breaking change?

Yes. I do not understand how the % is important. It is a break for some users and the release notes will describe who are impacted. As you bring up the issue of severity of breaking change, % impacted etc, then the version bump highlights that there is a breaking change drawing closer attention to the release notes. I treat a path bump very different to a major bump.

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.

I wonder if this is merging product versioning and software artifact release versioning. I see them as very different and I use SemVer for software artifact versioning for what the software team deems to be a release which is a different to product management releasing a product. Yea but that is all about context isn't it.

@bartlettroscoe
Copy link

Software package releases should post their the package-specific version (i.e. Romantic Version or Calendar Version) as well as a mandatory semantic version. The package version would just be X.Y.Z, etc. while the sematic version identifier would be be prefixed with semver- like semver-<semver-id>. So Package X would have the semver version identifier semver-2.3.1, semver-4.1.0-rc.5, etc. where the sematic version number is 2.3.1, 4.1.0-rc.5, etc. That way, the package could advertise the package-specific version (i.e. Romantic Version or Calendar Version) but package managers and other fastidious actors would query and pull based on the semantic version number.

There would be some conventions needed so that packages would publish both the project-specific version and their sematic version. And if you pulled the semantic version semver-2.3.1 it would be nice to find out its project-specific version 1.5.1 and visa versa.

It is common for software project release to have multiple version identification schemes.

Problem solved. Everyone can have their cake and eat it too. Next cliche ...

@RobSmyth
Copy link

RobSmyth commented Aug 2, 2024

Software package releases should post their the package-specific version (i.e. Romantic Version or Calendar Version) as well as a mandatory semantic version.

Nice thinking. Very interesting. But I do not quite see it :-)

You say "package-specific version" and "mandatory semantic version". Who is the user for each? The work mandatory is interesting, it implies a second player.

If I guess that "package specific version" is a product version like "Visual Studio 2022" then would that not be the name and not the version? For example:

My product_2024.8.1_1.2.3-rc.5.ext

Even that would seem odd to me as it is not clear why I would want to have 2 versions in the one name. If the end user expects "2024.8.1" why add a semver? But that comes back to the "mandatory" word.

@jedwards1211
Copy link

@RobSmyth the user for the semantic version is the package manager. When a person tells the package manager to upgrade a package, the package manager compares semantic versions to determine what it can safely upgrade to.

The romantic number would be purely to satisfy humans who want a subjective number, and provides no objectively useful information.

This has been the problem with getting people on board with semantic versioning - people are used to thinking version numbers are for humans. But version numbers should be for machines - only then are they objectively useful.

@lolmaus
Copy link

lolmaus commented Aug 3, 2024

Being able to signal a substantially significant upgrade is objectively useless?

@Venryx
Copy link

Venryx commented Aug 3, 2024 via email

@jedwards1211
Copy link

jedwards1211 commented Aug 3, 2024

So if a package indicates a breaking change is minor you would want to automatically upgrade to it and just see if it works without reading the changelog to make sure the breaking change doesn’t affect you? Or is it that you would always read the changelog when you do a baking upgrade, but you want to decide if you should even bother before looking up the changelog?

I wouldn’t personally want to risk a “minor breaking upgrades across the board” operation. But I wouldn’t be opposed to letting other people do that I guess. The main thing that’s a problem is people wanting to sweep minor breaking changes under the rug, in which case no automatic upgrade of any kind is wise.

@Venryx
Copy link

Venryx commented Aug 3, 2024

So if a package indicates a breaking change is minor you would want to automatically upgrade to it and just see if it works without reading the changelog to make sure the breaking change doesn’t affect you?

Not as an every day operation, but in some cases yes; for example, if there's a project I haven't worked on in some months, I will sometimes want to get the dependencies more up-to-date, but not have to deal with a 6+ hour refactoring headache (ie. avoid major library revamps). In those cases, triggering automatic upgrades that permit only "small breaking changes" would be useful to me.

The main thing that’s a problem is people wanting to sweep minor breaking changes under the rug, in which case no automatic upgrade of any kind is wise.

Yeah that can be a problem. I think having four digits (MajorBreaking.MinorBreaking.NonBreaking.Patch) would reduce the occurrence of this problem though, because library devs can then increment the version digit accurately (MinorBreaking), while communicating the intent that "yes this is technically breaking but any corrections will be fast or even unnecessary,".

The end developer can then choose whether they want to opt into that "small but breaking change", or avoid any breaking changes whatsoever.

@jedwards1211
Copy link

jedwards1211 commented Aug 3, 2024

Would a change like dropping support for Node version X be major, or super major?

It wouldn’t entail any refactoring, but then again, it might be a complete dealbreaker for projects that are stuck deploying to older versions of node for the time being.

And it would be the kind of thing where if you get that breaking upgrade automatically, and aren’t careful what version of Node you’re using locally, everything could seem fine for awhile, theoretically it could even get released if none of your testcases happen to trigger the codepath that uses new syntax or APIs etc.

If you want to skip reading the changelogs for personal project deps, there’s no problem with taking that risk alone. But if anyone else uses your code, they could get burned if you don’t check changelog entries for breaking changes, no matter how minor.

@jedwards1211
Copy link

jedwards1211 commented Aug 3, 2024

@lolmaus being able to indicate whether there were breaking changes is objectively useful, it enables you to automatically upgrade all of your deps to the newest minor version, and as long as package maintainers are cooperating with the versioning scheme (most major OSS projects do), you can rest assured you didn’t get any breaking changes. It’s “objective” in the sense that code can make a reasonable decision based upon it. Automated decision making from subjective information doesn’t work as well, or at the very least is much more complicated (e.g. AI)

i don’t think that signaling small vs large breaking changes is objectively useful for code making decisions though. I don’t see how an automated process could provide any guarantees off of some number that indicates the “degree” of breaking changes.

@Venryx
Copy link

Venryx commented Aug 4, 2024

Would a change like dropping support for Node version X be major, or super major?

I would consider that MajorBreaking (rather than MinorBreaking), because of scenarios like you mention, where a deploy environment has versioning constraints (or, eg. another library only has native binaries for certain NodeJS versions); but yes, subjectivity exists there.

Like many things in life though, a distinction can be on average beneficial to signal, even if there is not an objective/unambiguous cutoff for it.

And it would be the kind of thing where if you get that breaking upgrade automatically, and aren’t careful what version of Node you’re using locally, everything could seem fine for awhile, theoretically it could even get released if none of your testcases happen to trigger the codepath that uses new syntax or APIs etc.

Yes, that is an example of a case where using automatic upgrade of the "MinorBreaking" could cause unintended problems.

But two points:

  • If the project is critical, and you don't have extensive test cases, you don't have to do upgrades of "MinorBreaking"; you can keep using the existing SemVer behavior and avoid both "major" and "minor" breaking changes. It's opt-in, so devs can weight the pros/cons per project. (ie. I do not advocate for the default being auto-update of minor-breaking changes)
  • Even apart from auto-update commands, I think a fourth digit is useful independently, because it signals information to the end developer (in a compact, globally visible way) whether the library developer is declaring the change as likely to require substantial changes to update to or not. (eg. letting end developers selectively inspect the changelogs of updated libraries)

In short: I think the utility gained from that extra digit outweighs the costs. If you disagree on that weighting, that's fine; but I wanted to explain the reasons I'd find it useful, in practical terms -- and that it is not sought simply because it "looks more appealing" or the like.

@lolmaus
Copy link

lolmaus commented Aug 4, 2024

@lolmaus being able to indicate whether there were breaking changes is objectively useful, it enables you to automatically upgrade all of your deps to the newest minor version, and as long as package maintainers are cooperating with the versioning scheme (most major OSS projects do), you can rest assured you didn’t get any breaking changes. It’s “objective” in the sense that code can make a reasonable decision based upon it. Automated decision making from subjective information doesn’t work as well, or at the very least is much more complicated (e.g. AI)

You are making a lot of mistakes here:

  1. I didn't ask whether the major version number is objectively useful or not. I don't question its usefulness. My question was about the higher-level version number. I like to call it "generation".
  2. I didn't ask whether something is objective or not. I asked if something is useful.
  3. I didn't ask about automated decision making at all. Semver is more or less fine in this regard. This thread is about human convenience and reasonable meaningfulness, not about automation.

TL/DR: stop trying to win this argument on the automation field of battle. We're not following you to that field, you can go fight there alone or with your automation friends. We will stay here on the human convenience field of battle and continue meaningful discussion.

The problem with SemVer and humans is that most projects that claim to follow SemVer fall into two categories:

  1. Their major version number skyrockets, since every wee breaking change is demanding a major bump. AFAIK that's how SemVer is supposed to work. It's efficient but there is no way to signal a package/app generation upgrade (other than changing the package name like apache-2 which is horrible).

  2. Package maintainers want the major version number to reflect package generation. And they fall into two sub-categories:

    a. They hold back breaking changes, including super insignificant ones, and pile them up for an upcoming generation upgrade. This results in weird release schedules that are pulled out of thin air and only exist to worship the SemVer god but not bring convenience to developers or users. When a major release comes, developers have to face with dozens or hundreds of breaking changes at the same time, resulting in unbearable maintenance burden.

    b. They violate semver and release small breaking changes as minor bumps.

    c. Oh, and of course there are projects that do both at the same time.

I believe that the second category are wrong, even though they seem to be much more numerous than the first one. SemVer expects the major number to skyrocket, and all we ask for is to have a separate number to indicate package/app generation. When we have it, all those packages in the second category will be able to stop misusing the major version number.

@christian-weiss
Copy link

christian-weiss commented Aug 4, 2024

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.

@RobSmyth
Copy link

RobSmyth commented Aug 5, 2024

@RobSmyth the user for the semantic version is the package manager.

Now there is the problem/solution. Just sounds wrong as the "user" is not the consumer but a manager. Why does the package manager want to know if there was a feature or bug added?

@RobSmyth
Copy link

RobSmyth commented Aug 5, 2024

IMO a breaking change is a breaking change and must be a major bump. It signals to consumers to read the release notes to understand the magnitude. Trying to flag large/small is left to the release notes. It does not matter if the version major number is large. If it bumps often then it truely lets users know that there are often breaking changes.

It allows you to set up package compatibility spec for automatic upgrades (depends on trust and product criticality) ... so allow for a bug fix and new feature but not a breaking change. e.g: I'll accept an automatic Microsoft package patch upgrade with little thought.

I find it very useful when consuming products that use it.

@RobSmyth
Copy link

RobSmyth commented Aug 5, 2024

As a final note (I hope) ... I find semver very readable (with metadata) and teams I've worked on have found it very helpful. It shows the team and testers:

  • The targeted release
  • An idea of what it contains compared to last release
  • Where is was built (metadata) - build system (controlled), dev box (uncontrolled)
  • If a prerelease and what maturity
  • Traceability - build number and sha (prerelease and informational version metadata)

The critical question is always who is your consumer and what is a release. A dev team's version number is often very different to the product's marketing version number and name.

I find often that questions of readability is due to it being exposed to the wrong customer. Or ... "why/how are you looking at that version number?"

@jedwards1211
Copy link

When a major release comes, developers have to face with dozens or hundreds of breaking changes at the same time, resulting in unbearable maintenance burden

That sounds like a nightmare, but it hasn’t happened to me with libs I work with, so do you have examples?

Most of the major software libraries I use in my work have had breaking changes, and I don’t think there was anything unreasonable or inconvenient about the way they were handled. They bumped the major version number, and there were no surprises. It’s been much better than if I could never upgrade anything without fear that there are minor breaking changes hidden in there that we just happen to use.

@Venryx
Copy link

Venryx commented Aug 5, 2024

It’s been much better than if I could never upgrade anything without fear that there are minor breaking changes hidden in there that we just happen to use.

Yes, knowing whether a breaking change was introduced (simply by looking at the version number) absolutely is useful.

Has anyone been arguing otherwise? If so, in which comment specifically?

My understanding is that the disagreement is merely on whether adding an additional digit at the start is worth it (my opinion: yes), not on whether existing "is this change breaking?" information should be stripped.


EDIT: It has been a long time since I read the earlier parts of this thread. I glanced up quickly and it seems as though some of the earlier comments do seem to be suggesting that "is this change breaking?" information is not critical to preserve in the version number.

In which case, my own position would be a hybrid in the context of this thread: I think it's important to keep is-this-breaking information, but think adding a fourth digit to distinguish between major and minor breaking changes is very much worthwhile. (apologies for not noticing this earlier; I must have been thinking of another thread, as there have been multiple on this subject)

@RobSmyth
Copy link

RobSmyth commented Aug 5, 2024

Being able to signal a substantially significant upgrade is objectively useless?

OK ... I'll comment. If there is no functional difference in the upgrade then yes it is objectively useless.

If the functionality is a substantial marketing benefit then I would expect marketing to do something with the product name / version.

If however the software and product are the same (your delivering a component) ... then I have at times deviated from semver and bumped the major. But, in those cases I do not consider that to be semver :-). If I formed a contract with the users that I used semver and I bumped the major for a highly valued change then I would expect to be punished :-(

So for semver (software) using major to signal a marketing value impact is objectively problematic. I would expect calls of "you say it is a breaking change but I cannot find a breaking change in the release notes". It is a contract with the consumer.

@robnagler
Copy link

@jedwards1211 writes:

That sounds like a nightmare, but it hasn’t happened to me with libs I work with, so do you have examples?

https://numpy.org/devdocs/release/2.0.0-notes.html

Here's a very specific example illustrates the level of breakage. With 2.0, from numpy import * overrides several of Python's builtins, including min and max. This is a major breaking change. You won't find this in the release notes or in the minor release 1.25.0 release notes, where this breaking change was first introduced. This breaking change was reversed in 1.25.2, which of course was now a new breaking change (feature removal).

To be fair, NumPy does not adhere to semantic versioning. However, to a package (or worse, distro) maintainer, NumPy versions look like semantic versions, uses the same vernacular. In this example, NumPy didn't adhere to its own versioning policy.

For those of you who know Python, you'll wag your finger and say import * is not a good idea. That doesn't mean that virtually every scientific script does this. A quick search on GitHub shows about 65.5K files, and those are in public repos. Most scientific scripts are not in public repos.

Python itself is notorious for breaking changes on minor releases even on patch releases. Again, Python doesn't adhere to semantic versioning. Here's what Python's official packaging site has to say about it:

A majority of Python projects use a scheme that resembles semantic versioning. However, most projects, especially larger ones, do not strictly adhere to semantic versioning, since many changes are technically breaking changes but affect only a small fraction of users.

This assumes a lot, like the authors know all the uses of a particular feature in the entire world. Again, this is not semantic versioning, but Python is used by a lot of people. The language they use above can easily confuse people.

Once upon a time, people didn't introduce breaking changes. That couldn't be enforced either, and certainly not proven. Consider Make, where you can still run Makefiles written 40 years ago. I have worked on Fortran files that were written in 1970. Fortran is very good about maintaining backwards compatibility as is C. 40-year-old C programs still compile and run. Linus Torvalds has been quite clear about this with regards to the Linux kernel.

If a versioning scheme can't be enforced or proven in some way, it's not going to be followed strictly, and all attempts to define rigorous standards are going to be a waste of time. Package managers and software developers will simply have to do what they do: slog through the quagmire that Mark Zuckerberg famously popularized.

So maybe this is a cultural question: Zuck or Linus?

@jedwards1211
Copy link

I do think, in the case of npm, which does explicitly recommend semantic version numbers, that they should allow packages to declare themselves as not strictly semver, so that npm can warn us on any upgrade to that package that it's potentially breaking.

@christian-weiss
Copy link

@robnagler, i do not think "numpy" (from Python community) proves a point about "fast bumping major", as:
a) it does not use SemVer
b) collecting all breaking changes in one giant release opposed to release many smaller releases (each with breaking changes), is not related to a version string at all.

2. a. They hold back breaking changes, including super insignificant ones, and pile them up for an upcoming generation upgrade. This results in weird release schedules that are pulled out of thin air and only exist to worship the SemVer god but not bring convenience to developers or users. When a major release comes, developers have to face with dozens or hundreds of breaking changes at the same time, resulting in unbearable maintenance burden

Holding back changes can be done but nowerdays the industry (agile movement, extreme programming) tend to release often and in small chunks, as it helps to get faster feedback and to make it easyer to spot and fix issues faster.

That is not a version schema issue at all. You will experience pain with big releases independent from SemVer.

A SemVer version number is just a technical number/string. You should not attach marketing aspects or emotions to it. Avoiding a version bump due to spiritual or esoteric reasons can be done, but is not subject to a technical contract as SemVer is.

Version bumps are not there to be avoided. A vendor should signal change-events in this code base to its consumers as fast as they occur. In the very next release of your code base.

If a vendor collects many changes (e.g. more then one breaking change or just a couple) then the migration path / implementation burden on the consumer will become exponential, as the consumer will have to handle with more then one issue at the same time (complexity). This will result in "delayed migrations" and even in consumers that stop consuming this vendor package. This is one of the causes for "stuck for years" packages. Unmaintained consumers are one possible outcome. This vendor would harm the ecosystem.

@bartlettroscoe
Copy link

@RobSmyth

Software package releases should post their the package-specific version (i.e. Romantic Version or Calendar Version) as well as a mandatory semantic version.

Nice thinking. Very interesting. But I do not quite see it :-)

You say "package-specific version" and "mandatory semantic version". Who is the user for each?

  • The "user" for the "package-specific version" is for humans. (Likely better to call that the "human version")
  • The "user" for the semantic version is for package managers

The idea that 2.0 is a major upgrade over 1.0 is so ingrained in the public psyche that it is used for almost everything like "this is version 2.0 of Jeff, a major upgrade!". So we can't get away from the usefulness of that for communicating with humans. Yet, from a package management perspective, that is worthless information. A package manager only cares about set of backward-compatible versions.

If I guess that "package specific version" is a product version like "Visual Studio 2022" then would that not be the name and not the version?

That would be the human version. The human version can be any set of ASCII characters in any arrangement that the project would like to chose as a way to communicate with its human customers.

Even that would seem odd to me as it is not clear why I would want to have 2 versions in the one name. If the end user expects "2024.8.1" why add a semver?

Because version "2024.8.1" is not a sematic version and says nothing about the backward compatibly relationship between that version and say "2024.1.1". That type of version naming is completely worthless for a package manager.

But that comes back to the "mandatory" word.

If you package lives on an island with no upstream or downstream dependencies, then a package would not need a semver. (But then a package with no downstream dependencies, by definition has no users, and therefore is not even worth discussing. But such packages do exist, i.e. those that people write for themselves.)

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

All I am suggesting is a way out of this mess. Let people select whatever version naming/numbering they want to human marketing purposes (i.e. the "human version"), but get serious and provide a semver as well so that we give package managers what they need to try to upgrade package ecosystems in a sane and scalable way. The alternative is chaos (which is the de facto state of affairs).

@bartlettroscoe
Copy link

How to end this debate once and for all? => Have packages define two different version names:

  • Human/romantic version
  • Semantic version (semver)

That gives everyone what they want and we end this endless debate. You advertise the human version to humans but you also provide the semver so that package managers can do their job. Win win.

After 10+ years of listening to this debate, this solution seems so obvious that it is shocking that the software community has not already adopted this.

@robnagler
Copy link

@bartlettroscoe writes:

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

If it were possible to implement semver by fiat, then I would agree with semver. From my perspective (one who has maintained numerous software packages over many decades), version numbers are meaningless. I am often caught flat footed by (often capricious) dependent package changes on arbitrary version boundaries. AFAICT, the only thing enforceable by any package manager is that version numbers increase over time.

@christian-weiss writes:

Holding back changes can be done but nowerdays the industry (agile movement, extreme programming) tend to release often and in small chunks, as it helps to get faster feedback and to make it easyer to spot and fix issues faster.

That is not a version schema issue at all. You will experience pain with big releases independent from SemVer.

I believe semver defines this clearly: "MINOR version when you add functionality in a backward compatible manner". MAJOR versions are the only way to release backward incompatible changes. The implication of MAJOR is that they are either infrequent and/or all MAJOR versions are kept around and maintained forever. This is the promise of semver. This is not the reality of package management. With the "move fast and break things" culture, software maintenance includes validating breaking changes, constantly, which can consume a small software team maintaining sophisticated systems.

Which begs the question: If Linux, C, Fortran, Make, and a host of other software packages have maintained backwards compatibility over decades, why is it necessary to release breaking changes at all? If you don't like the way an API works, create a new one with a new name. Or, perhaps, add a new parameter with a logical default so that existing clients continue to work the old way. Add version numbers to configuration, protocols, files, etc. to maintain backwards compatibility.

IPv6 works 100% compatibly with IPv4. Sure, they could have broken the Internet with the major version number change, but they chose not to, and I'm sure you are all grateful for the thoughtfulness for users. Containerization is only possible, because the Linux kernel is 100% backwards compatible. Again, I am sure you are all grateful you can run software applications built with any distro on any Linux system, and even Windows and Mac.

This issue is cultural imho. As an example, there is a striking difference between Perl and Python. Perl strived (no longer use it so dunno) to maintain backwards compatibility. I have Perl programs (and users :), which still run after 35 years. Python strives for "perfection", which results in removing APIs and/or changing them. People could certainly argue Python "won" and "perfection" is the way. I think software maintenance now involves unnecessary friction.

@bartlettroscoe
Copy link

Anyone who argues against semver is never tried to upgrade a bunch of dependent packages over many years.

If it were possible to implement semver by fiat, then I would agree with semver. From my perspective (one who has maintained numerous software packages over many decades), version numbers are meaningless. I am often caught flat footed by (often capricious) dependent package changes on arbitrary version boundaries. AFAICT, the only thing enforceable by any package manager is that version numbers increase over time.

@robnagler, just because a given standard is not universally adopted or fully followed by those that attempt to implement it does not mean it is not worthwhile. You could argue that if a package manager ran the native test suite for each package in the package dependency graph, then it is the package manger that would be validating the semantic version numbers. For example, if a package posts a new release X.(Y+1).0 that it can detect is not backward compatible with X.Y.Z w.r.t. downstream packages, then it should be marked as such in that package manger. When the package manager points out that the release X.(Y+1).0 that is not backward compatible, then the team could decide to put out the patch release X.(Y+2).0 that restores backward compatibility and the package manger can mark X.(Y+1).0 out as non-backward compatible. See What do I do if I accidentally release a backward incompatible change as a minor version?. I know there a lot challenges in running the native package test suites by a package manger but that is where this needs to go.

@lolmaus
Copy link

lolmaus commented Aug 5, 2024

A SemVer version number is just a technical number/string. You should not attach marketing aspects or emotions to it. Avoiding a version bump due to spiritual or esoteric reasons can be done, but is not subject to a technical contract as SemVer is.

@christian-weiss I totally agree, and this is exactly why we need a non-technical contract. A version number to indicate package/app generation to humans, leaving SemVer to machines where it should be.

@robnagler
Copy link

@bartlettroscoe we are talking past each other, just as we have done before. Neither approach can be implemented by fiat. A key difference is this: every package that ensures backwards compatibility improves downstream reliability, because dependents can upgrade without thinking about compatibility. With semver, there is no such guarantee except for minor releases. With backwards compatibility, software becomes more reliable in general, because upgrades to the "latest and greatest" are seamless and there is less friction so there is more time to maintain the dependent software.

The cost of backwards incompatibility is immense, because it is NM where N is the number of packages and M is the number of dependents. The cost of backwards compatibility is linear: N, only the packages themselves need to be maintained. Not to mention that M is much larger than N so the difference between N and NM is more than quadratic.

@mindplay-dk
Copy link

A key difference is this: every package that ensures backwards compatibility improves downstream reliability, because dependents can upgrade without thinking about compatibility.

💯

With semver, there is no such guarantee except for minor releases.

You mean minor and patch releases, right?

and what's your point? I mean, with SemVer, only major releases (are supposed to) contain breaking changes, that's the standard - we need some way to signal a breaking change, right? Otherwise semantic constraints in package manager requirements wouldn't be any use at all.

With backwards compatibility, software becomes more reliable in general, because upgrades to the "latest and greatest" are seamless and there is less friction so there is more time to maintain the dependent software.

of course backwards compatibility is always preferable, whenever it's practical and realistic - in some cases though, things can be simplified and made more reliable by removing code that exists to supports backwards compatibility. Code like this doesn't typically make software more reliable - usually the opposite - so we want to remove it eventually.

I've been following this conversation for a long time, and I'm a bit confused. 😅

is this a discussion about SemVer or about change management in general?

if it's about SemVer, yeah, I agree, there are some problems with SemVer as described - but there aren't any major problems with SemVer as implemented in package managers, is there?

sure, sometimes you get a bad release, because people tagged it with the wrong version number - but I'd say that's maybe 10% of the time, absolute worst case, which means 90% of the time it's saving us a lot of work.

if I notice a package using romantic versioning, usually I just change my constraint to something like 1.2.3 and manually update that package as needed - it's something I've rarely needed though, as most packages (based on my experience with NPM and Composer) are generally versioned according to package manager recommendation.

speaking of, I will mention this again, since no one ever commented:

https://simversion.github.io/

it's a subset of SemVer, which references how version numbers are interpreted by package managers - which is much simpler and easier to describe than actual SemVer, which just seems to create confusion and start endless debates.

my hope with this was that developers would be more interested in solving problems than debating the complex semantics of a specification that (lets face it) most developers don't even bother reading.

it was just a first draft/pitch, but no one ever showed any interest.

wouldn't it be more interesting so solve the problem than to debate the pros and cons of the SemVer spec?

extract "the good parts" and give people something they can actually understand and apply in a way that makes sense with existing package managers? 90% of the value with 10% of the complexity? 🙃

@bartlettroscoe
Copy link

The cost of backwards incompatibility is immense, because it is NM where N is the number of packages and M is the number of dependents. The cost of backwards compatibility is linear: N, only the packages themselves need to be maintained. Not to mention that M is much larger than N so the difference between N and NM is more than quadratic.

@robnagler, the cost calculations are not that straightforward. Maintaining backward compatibility for long periods of time over a lot of development and many releases can become a huge drain on productivity of the package development team, especially for faster moving packages and those that are driven by research and large underlying technology changes. (I come from the area of computational science where changes in GPU and other accelerator technologies are massively disruptive and mandate breaks in backward compatibility in many cases.) One can argue that maintaining backward compatibility for old customers is a large tax on new customers that want to adopt and fund future development of the software.

If you push too hard for never breaking backward compatibility, then you force many teams to abandon packages and start from scratch with a new package for new costumers. Then someone has to maintain the old package and well as the new package. This reminds me of:

(See my summary of this views 20 years later here).

We have to look at the total area under the curve of productivity of package developers, downstream package developers, package ecosystem management, and end customers. For some packages, the minimum area under the curve will come from some key highly used packages needing to never break backward compatibility. But for many other packages with a smaller number of direct downstream customers, the area under the curve will be minimized if the package development team can dump the cost of backward compatibility at regular intervals and have the downstream customers absorb those costs incrementally.

In the organization where I am working on, they are trying to remove an old highly used software package that as not been activity developed for 15 years and trying to get customers to move to the new package that was ready at least 10 years ago. The old package was officially deprecated at least 8 years and is not slated to be removed until the end of next year! That process is hugely expensive for everyone involved.
One can argue that it would have been better and overall cheaper to just incrementally refactor that old package into the new package starting 20 years ago and slowly broke backward compatibility in smaller, easier to absorb, increments.

And that is why we need semver and we need to take it seriously.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment