- If the MAJOR changes, you know something intentionally broke, but you don't know what
- You can figure out what broke by seeing your own code break, or perhaps ahead of time by reading the release notes.
- Frequently in practice there are no useful release notes about the breakage, though. If they're following semver strictly, you can maybe find details in documentation, and review changes to documentation when there are no useful release notes. You could also step through all the commits -- but we're trying to avoid that effort, no?
- This leads to interpreting a version number as maybe-care.don't-care.don't-care
- And so the only possible benefit is increased laziness by ignoring the don't-cares, because the intent is that you can upgrade freely until the MAJOR changes
- But this laziness is bad, and in practice doesn't turn out so nicely, because changing the MAJOR only captures intentional breaking changes, and not the very common unintentional breaking changes
- That is, if your goal is just to prevent breaking, intentional or not, you can't even trust a PATCH change, because bugs happen
- Plus, you might actually care about the smaller bug fixes and new features! You should really spend effort to see what changed and consider updating, when there's an update. It might even be critical to your program's security. Sorry, you can't be lazy without consequence.
- Additionally semver makes no requirements on granularity. Change one thing that breaks? Update the MAJOR. Change many things completely (version 2.0 baby!) in a way that requires most everyone to do a rewrite? Update the MAJOR. Which one of those is this update? You have to look to find out.
- Not everyone uses semver, and nor will they ever thankfully, but this means you can't be lazy for everything. Because of scale, it's less effort to be consistent with how you handle updates for all your dependencies, rather than dividing processes and trying to be lazier for semver'd libraries.
- So even with some semver, you need to have automated builds and tests at a minimum, this lets you try every new version regardless of whether the MAJOR changed or not, regardless if the library is semver'd or not, and see if it breaks you in a way you can easily detect
- In practice, when breaking changes happen, the response of the project is one of three not very helpful options, the second being the best but you still experienced breakage
- It was intentional, revert or deal with it
- It was unintentional, revert or wait until we push a new patch to restore behavior
- It was unintentional, but we like it, so we'll bump the MAJOR and re-release, revert or deal with it
Summarizing: I find semver useless (it saves no effort or frustration in practice) and even harmful (it condones making breaking changes without recognizing that people don't like breakages, intentional or not), and so is not worth standardizing on.
The better thing to do as a library author is to not break APIs! When you have this attitude it's very possible to have non-breaking APIs for a long time, much like when you embrace an immutable-by-default language you learn that you don't need as much mutation as you thought.
One way to do it is to maintain multiple versions of your API within the same program, and support all versions indefinitely. It's not as bad as it sounds, and in many cases might just mean exporting a "myFunc2" and updating documentation that users should really use that one. You can even add instrumentation to discover usage, and consider removing if you find no one really is using the disfavored entry point, but it's better to just not break things ever.
This Rich Hickey talk explores some aspects of this more:
What do I use instead? As long as the version number increases, that's good enough. For patches, that is, real patches, that sit between multiple released versions (patching an older version that can't for whatever reasons update to the newest version) the number should of course be in between as is needed. Aesthetically, for open source I like to use "0.x" if no one but me is depending on it and it hasn't been battle-tested outside my own uses, though I'm happy to jump to 1.0 for something released to the wild and serving users in production -- even perhaps if it's just me. Using a floating point number makes it easy to follow the increasing-number requirements. Also aesthetically, bumping the major number can sometimes indicate a "bigger" release, though with modern software practices of continuous deployment, this is less useful than it used to be. Frequently bumping the major hasn't been a problem for browsers. A simple date-string version number is even fine. Really, the only info I care about from a version number is "newer", anything else I might find aesthetic or not but don't want to enforce it in a spec since aesthetics are quite subjective and many projects will have their own.
I like Hickey's advice that API changes in particular should be composed of "provides, requires" sets, and that if you change an API endpoint to require more than it used to, or provide less, you've broken it. (Providing more, or requiring less, however, are possible changes that possibly don't impact anyone! only possibly, though; some languages or API designs make it difficult to provide more/require less without breaking something, e.g. because they rely on statically sized binary footprints for everything.)
Hopefully this short post gets my points across for why I don't like semver. I may update this post if future discussion still produces confusion or other points of contention...
Posted on 2021-08-09 by Jach
Trackback URL: https://www.thejach.com/view/2021/8/reasons_i_dislike_semver