Semver sucks

6 min

TL;DR

Here be dragons – TLDR comes last

Introduction

When I let AI analyse some of my projects it always flags the lack of semver as a risk to a project. “You cannot trust this, so pin your packages”. AI and people treat semantic versioning as some kind of holy grail.

Why semver?

For some reason people seem to think that semver is the golden ticket to reproducible builds. It assumes, some kind of clear contract, major version, feature added version and bugfix version. The concept, in theory is rock solid. In practise not so much. Whole tribes have defined workflows based on semver, they mostly are trying to accomplish reproducable builds. Build it twice in a row and get the same output.

The problem with semver

Everyone violates the contract. Lockfiles are committed at a blistering scale. People don’t trust upstream, so the x.y.z contract is broken from the z onwards. So what happens? People pin packages to versions, and software becomes stale. There is dependabot, but why would you have a bot tell you when to update?

My problem with semver

Semver’s rules are rather simple: breaking change, major version, add a feature, minor version, bugfix patch version. Works in theory. Now, in the real world users abuse bugfixes and use the (undefined) behaviour as a feature. I’ve seen this happen. Got (semi) angry e-mails from clients or consultants that their setup broke because we fixed a bug. This constitutes a breaking change in semver, you can’t patch bugs only bump majors.

Ok, you might argue this is an edge case, and let’s say I agree with you. But the following scenario baffles me: You add a new feature, nothing breaks, nothing changed, just a new feature. Minor update. Now, let’s assume we also ship a bugfix. Now.. Instead of 1.2.3 and I bump to 1.2.4, I need to bump to 1.3.0. But the way semver notations work, you can block minor updates by using ~1.2.3. Now, the best practise is to ship two versions, so that means, additional ceremony for zero added benefit for the module author. Downstream needs it in this rigid way of working.

The idea to do this stems from so called risk management, but the risk is self inflicted. People want automation and rigid rulesets. Forcing everyone upstream to adhere to their rules. Maybe read about your dependencies before updating them or.. just update them and let your testsuite deal with the fallout of any breaking change.

Deprecating code

I deprecate a function. That is also a y-change. Now time passes, I decide to move remove the deprecation and suddenly I need to bump the x because I announced a year ago function foo had to go. In the meantime I had several releases. There is zero logic to this versioning scheme.

No indication why any bump happened. You can only say. Breaking.. but why?

Read a changelog, upgrade it and be done with it. You need to do it anyway for any “risk reducing” upgrade path.

You know what would be a better way to version: do not break downstream code.

Oh, and before you say, mature projects. Node breaks semver. node-tap broke on me recently, node 24.10.x, it appears node < 24.11.1 broke something, so, that wasn’t an non-breaking change. By defintion it should have gone to node 25. Hmmmmmm, yeah. No. I don’t care that there is a bug, but semver is like the holy grail for fucking it up in 3 places.

The Changes language

I write Perl, now, not so much as before, due to a change in employment status. In Perl land we try not to break your code. This means, we either supply fixes to your package to deal with an upgrade or we keep deprecations notices for three generations. Backward compatibility is the golden standard. And it is a delight to work with.

In every language I code I take the same approach. I do not adhere to semver in any language. I think it’s wrong solution to a imaginary problem. There are other ways to accomplish the same. Most people use containers, use their cache to create your reproducible build. Or only do it for production builds, not for developer builds. Developers should work with the latest and greatest or explicitly pin a version. It ensures projects move forward instead of getting stale. The amount of npm warnings on npm i is mind boggling at times. And in addition, some packages are so finicky with what they work with (react-native says hi), that pulling your nails from your feet is a more pleasant activity.

Development at scale

You might say, well, we have a lot of transitive dependencies via some other package. That might be the case, but that means you have a bigger problem on your hands, mainly, you are depending on a monolith that apparently breaks too much to be comfortable with.

Move fast and break things is the mantra of some ecosystems, precisely those that tell you to pin packages at specific versions not to move fast and break things. The whole mantra is one big conflict of interest. And to top it of, riddled with lockfiles.

Some might argue that semver enables fast iterations, because the ship of any major version means you can experiment with new things. This is what I call a false premise. If you change your whole thing in such a way it is an experiment, you should just start a new project

COmbine it with your own readme’s froms skirbi, et al

Claude said:

That’s the positive counterpart to the rant — here’s what I do instead. The article writes itself: why semver is broken, here’s the philosophy I use instead. The Perl-style “we do not intentionally break working code” is a strong contrast to the semver theater. And “versioning for humans, not machines” is your thesis.

LinkedIn

I don’t trust Azure PowerShell module versioning anymore.

Today my pipeline broke on this:

Az.Storage 𝟵.𝟲.𝟬 → requires Az.Accounts >= 𝟱.𝟯.𝟮

Az.Storage 𝟵.𝟲.𝟭 (PATCH release) → suddenly requires Az.Accounts >= 𝟱.𝟰.𝟬 (which is a minor release)

And that dependency change was not even mentioned in the release notes.

My pipeline blocked it immediately, as it should. I pin exact module versions and only allow automatic patch updates. Minor versions go through validation first.

That is standard enterprise CI/CD practice. This is why semantic versioning matters.

Without strict dependency discipline, semantic versioning becomes meaningless.

Microsoft: A patch release should not introduce new minor-version dependency requirements. Especially not without explicitly documenting it in the release notes.

How are others handling Az module updates?

Strict version pinning? Automatic minor updates? Or full isolation/containerization?