Post co-author + `cargo-semver-checks` maintainer here, AMA.
What we did:
1. Scan Rust's most popular 1000 crates with `cargo-semver-checks`
2. Triage & verify 3000+ semver violations
3. Build better tooling instead of blaming human error
Around 1 in 31 releases had at least one semver violation.
More than 1 in 6 crates violated semver in at least one release.
These numbers aren't just "sum up everything `cargo-semver-checks` reported." We did a ton of validation through a combination of automated and manual means, and a big chunk of the blog post is dedicated to talking about that.
Here's just one of those validation steps. For each breaking change, we constructed a "witness," a program that gets broken by it. We then verified that it:
- fails to compile on the release with the semver-violating change
- compiles fine on the previous version
Along the way, we discovered multiple `rustc` and `cargo-semver-checks` bugs, and found out a lot of interesting edge cases about semver. Also, now you know another reason why it was so important to us to add those huge performance optimizations from a few months ago: https://predr.ag/blog/speeding-up-rust-semver-checking-by-ov...
In the Scala ecosystem, MiMa [1] has been in widespread use for years. It automatically checks compatibility for the binary API of a library. Every library with any amount of success uses it. One could say it's the foundation of a stable ecosystem. We also have sbt-version-policy [2] to set it up with minimal configuration (and directly relate it to SemVer).
More recently, we got tasty-mima [3], which checks compatibility at the type system level, rather than the binary level.
Semver will never "solve" the library dependency problem, because it is not a problem that can be "solved". We want 2 things out of dependency management.
1. If a new version of a dependency contains a desirable/necessary improvement (however that is defined), we want to automatically pick up the new version.
2. If upgrading to the new version will make things worse (however that is defined), we do not want to pick up the new version.
But there is no automated way of determining which of those is the case sort of actually trying the new version and fully testing it, even in the presence of "proper" semantic versioning (or any other kind of metadata you might provide).
- you may already have a workaround for the library bug in your code, so a non- interface-breaking fix in the library may break your usage until you remove the workaround. Semver can't help you with this because the library maintainer doesn't know about your workaround
- an update may have both properties (fixed one thing, broke something else). Semver can't make the judgement call about whether the tradeoff is a net improvement or not
- the update may change a behavior that is not formally guaranteed, but which your use has an (unknown to you) dependency on. So the semver can "correctly" label it as a non-breaking change, but it still breaks your use case.
- etc.
This doesn't mean semver is worthless. Knowing that something is definitely a breaking change because an API was removed or modified in an incompatible way will still save you time.
But you still need to actually test to know if updating a dependency is actually safe.
Honestly, I don't know why people overthink it... a major bump means "your code probably has to change"; anything else means "your code probably doesn't have to change". That's more than good enough for me and beats the hell out of "no indication whatsoever whether your code has to change".
Yes but currently semver declaration happens in a best effort base by a human.
The authors advocate that they can have the semver declaration test by a computer which unquestionably will increase the reliability of the declaration in the long run.
It is like testing but for backward compatibility, which of course can be part of the testing infrastructure some day.
I like their approach very much. What a great bachelor project... And the witness approach is great. Bonus points for the systematic approach.
Of course! `cargo-semver-checks` is not a replacement for testing. It's exactly the kind of tool you describe:
> Knowing that something is _definitely_ a breaking change because an API was removed or modified in an incompatible way will still save you time.
We want to help maintainers spot accidental breaking changes early — before they are released — instead of both maintainers and downstream projects having to scramble for fixes after an accidental breaking change is released.
This is more a coordination problem, semver doesn't really matter here, except as a good enough standard or guardrail for most. It's going to get broken from time to time, and that is fine.
The CUE Unity project is an interesting take on how to "solve" the bigger issue of breaking changes or regressions.
Basically, the idea is that users can register their project, tests, or benchmarks in Unity. The project maintainers can then test new code against these projects before releasing, or even before pushing a commit, because a regression is found for example. This works by injecting the version (or local code) into the dependency management system. For CUE, this is a `go mod edit`, or setting up the environment with the cli at version / local. How this is set up and managed isn't as important as getting the process in place and making it an easy workflow for both sides. Hence why I call it a coordination problem.
We have a similar project Harmony, inspired by Unity, powered by Dagger.
it's probably impossible to automate an entire ecosystem, and there is value to enabling a tighter integration within a project ecosystem (a subset of the language ecosystem).
In the meantime, semver is good because it gives us a small number of additional semantics to impose on version numbers if we wish. Semver is not a solution to versioning, it's just a foundation to build better automatic versioning tools atop of, such as the tool linked here.
Seriously. Machine-readable semver is a pipe dream. You're relying on everyone else getting it right every single time. At best it's an indication of a maintainer's intention. But it's not a substitute for QA; if your dependencies change, you need to test your software no matter what. So what does semver actually do? Using it as a guideline for setting your own version numbers to communicate to humans is one thing. Treating it as a machine-readable indicator with absolute certainty is taking it too far.
v44 to v45: restructured the whole thing, steps for users to migrate are expected to be in the man-months for most projects
Semver is a human convention that can also frequently help machines. It'd be nice if it were a machine system that also helped humans, but very very few compatibility systems achieve that.
There are no other options currently to achieve this, and it works pretty well in practice. Use semver, don't make everyone else pay for your willful ignorance.
And now you need to update from version 42 to 67 after not paying attention for 4 months because there's an emergency bugfix or crucial feature that you can't backport trivially.
They do have a backport for version 56, but it's in a branch named v62-backport-v56-githubusername-2023-asdf-try-2 and you have no way to discover that.
Instead of choosing v56.1, because that would be semver.
I agree. Semver isn’t perfect, but it gives you a general idea of how hard an upgrade might be. That’s even more true with the most popular of libraries.
For example, Rails and React are the two most important dependencies in our system. They are extremely good about semver with major versions including highly detailed patch notes.
But isn’t the point of this post to suggest that even if SemVer suggests a release isn’t breaking, they still are in practice? Such that, coding defensively, you have to assess whether a new version of a tool breaks regardless of SemVer or not. In which case, why bother with SemVer in the first place?
As a co-author of the post, I wouldn't agree with that summary.
If semver suggests a release isn't breaking, in Rust it overwhelmingly isn't breaking. Around 3% of the time, assuming you use every obscure bit of public API in the library, you might get broken by a release when you shouldn't be. In practice, that percentage is way lower than that: nobody uses the entire public API of anything, or comes anywhere close. That's why the post says these semver violations were unknown, even though most of them were multiple years old. If something breaks, and you weren't using it anyway, you were not broken.
The post is emphatically in favor of semver as used by Rust, because it has clear benefits and works great the vast majority of the time. The part that we want to make better is to lower the odds even further that an accidental breaking change breaks a bunch of people's code and ruins their day.
No single technique can do that: not testing, not fuzzing, not cargo-semver-checks. But a sound combination of techniques can drive the odds to damn near zero for all practical purposes.
I'd be curious about these metrics for a more widely used language. Admittedly I'm not discussing SemVer _in Rust_, as much as just SemVer, which is in my direct experience just kind of a mess. Adherence is as good or bad as any library's tooling or developers want it to be.
I'm curious too! And I also agree that semver in _most_ languages is a mess.
I'm sure someone could build a semver-checker for Python or JS or Java or whatever, and I'd even be willing to help them and walk them through how cargo-semver-checks works. But in Rust, a big driver of adoption is that Rust and cargo are opinionated about things, so people adopt cargo-semver-checks not necessarily out of love for semver but out of a desire to not wake up to 100 people being upset in their GitHub issues about broken builds. The adoption situation might be harder for other languages with less opinionated versioning stories.
How does not hinting your tools the rest of the time improve things?
Rust compatibility (thus versioning) is fairly fragile due to its relatively powerful type system, and yet:
>Around 1 in 31 releases had at least one semver violation
Abandoning semver means making it worse 30 out of 31 times for a fragile language, or likely more (because you do not always use the thing that technically broke, or in a way that notices the break). Other languages generally have even stronger arguments in favor of semver.
It's not a "both sides" thing, semver wins by an absolute knockout in every conceivable way, and especially in practical day-to-day ways. Anyone opposed to it does not know what they're talking about, or they're simply trolling, and it shows. There could definitely be better systems, but "just don't use semver lol" is not one of them.
> How does not hinting your tools the rest of the time improve things?
If the hints are sometimes incorrect, should we trust them? Isn't it the same release-verification legwork either way? If so, what's the point of the hint?
> Rust compatibility (thus versioning) is fairly fragile due to its relatively powerful type system, and yet:
Yes, I'm speaking about SemVer, which was neither invented by nor solely implemented in Rust. I understand that this blog post is about SemVer in Rust.
> Abandoning semver means making it worse 30 out of 31 times for a fragile language, or likely more (because you do not always use the thing that technically broke). Other languages generally have even stronger arguments in favor of semver.
I read this as: tooling is required to verify a release does or does not break a contract defined by a library, regardless of versioning strategy. It's not like we're talking about 1 in a million here.
> Anyone opposed to it does not know what they're talking about, or they're simply trolling, and it shows.
Ahh yes. "I'm right and if you disagree you're wrong". The ultimate debate tactic. I'll skip the rest of this thread. Enjoy the day!
> Isn't it the same release-verification legwork either way?
No, as it turns out it very much is not even close to the same amount of legwork.
> I read this as
I don't think your attempt at a re-statement is accurate to the source. Also, "regardless of versioning strategy" is simply not an option in Rust, since the entire ecosystem is opinionated about how versions should work and overall everything works very well.
This post is about taking something that is already very rare (though painful when it does happen!) and making it even more rare. It shows a concrete way to do so, and also shows that so far we've been "getting lucky" by experiencing breakage less often than we might have been, given the same code changes.
In other words: `cargo update` today has a tiny chance of breaking your Rust project. But if everyone used `cargo-semver-checks` every time, as demonstrated by what the blog post's analysis caught, we'd reduce the number of accidentally-breaking releases by *3% of all releases*. That is an astoundingly high reduction, so coupled with good testing practices and other static checks (type system, borrow checker, etc.) it means that `cargo update` will be even more astonishingly unlikely to break your project.
It works the vast majority of the time. Many, many studies across many languages repeatedly show it. There are flaws, but there are extremely clear benefits - most updates just work, automatically. It works so well most people don't even realize how much they're being protected from.
So what are you gaining by abandoning it? You're losing a lot, if you're not gaining a lot in return then there's no reason to choose it.
---
>Ahh yes. "I'm right and if you disagree you're wrong". The ultimate debate tactic.
I agree in principle, but it is also simply true in some cases. It's like arguing against vaccines or range checks on arrays - there's a massive load of trolling or willful ignorance in semver debates. There can definitely be better systems, but none are used widely enough to be noteworthy to most, and most presented alternatives are laughably bad to anyone who has done anything but throw nonsense at the internet's wall and see what sticks. Some things are just not that complex, and armchair bullshitters can be spotted miles away.
One thing that's often not mentioned is - how to deal with experimental APIs and such? You don't want to mark them as experimental indefinitely, and you don't want to force an upgrade if the experimental API didn't change when stabilizing.
The best solution I've found is: each compiled library doesn't just have a single API, but rather has multiple.
libfoo-1.2.3 might implement the following APIs:
* libfoo-1.0
* libfoo-1.1
* libfoo-1.2
* libfoo-1.2.1 (because people don't actually use the "no API additions in patch releases" rule)
* libfoo-1.2.2
* libfoo-1.2.3
* libfoo-experimental-1_2_3 (no semver in the version here)
and if you enable experimental exports for the library, you gain a package-manager-level dependency on libfoo-experimental-1_2_3. Then later you can declare "libfoo-1.3 includes libfoo-experimental-1_2_3", so old programs can use newer libraries without change.
That said, mutable metadata is still an important part of the ecosystem. Blacklisting is essential (since the symbol-list isn't actually the whole API and it is impossible to automatically detect violations), but easily abused for "that's insecure" and "I don't support that" purposes so there should be some friction.
Note also that ABI (and sometimes API) is fundamentally platform-specific. And there's enough complexity in the world that the tooling shouldn't think in terms of semver, but rather arbitrary compatibility DAGs (but with sparseness when possible). Consider the GCC 4.7.0/4.7.1 std::list breakage that was reverted in 4.7.2, breaking software that was compiled in the mean time (the only reason this wasn't catastrophic is because nobody actually uses std::list) - that intermediate software needs to know it can't use new binaries.
In Rust, this is often done by using optional features, and usually features named like `unstable-<something>`. They are a form of conditional compilation: people that don't opt in don't see that code, and people that do opt in know the code is unstable.
`cargo-semver-checks` by default doesn't check for any semver obligations for code behind such features. (I'm its maintainer )
The problem with the current approach is that it does not specify whether `libfoo-1.2.3[unstable-bar]` is compatible with `libfoo-1.3.4[unstable-bar]`, nor whether the latter is compatible with `libfoo-1.4.5[default]`.
(also I'm not sure under what circumstances recompilation is required)
I'm curious: When updating packages, say NPM packages, what are your practices?
For us, we audit every update of every package. We read the changelog or release notes for every version from the current version to the new version (some packages make this very cumbersome, looking at you Vercel). Then, we use our judgement about the order in which to update packages; usually we apply all patch-level updates, run our tests; then minor updates, re-run our tests; and then we apply major updates one-by-one and re-run tests after each of them.
We've found plenty of semver violations in this process, to the point where we don't really trust the semver of NPM packages at all.
Is this a common approach, or do you just blindly update everything at once and fix things if they break?
Speaking for myself in Rust, I blindly update everything and fix what's broken. My projects tend to have substantial test suites, so if the compiler thinks it's fine and the tests think it's fine, it's probably fine.
Sometimes it isn't fine, of course. Those cases fortunately are few enough and not damaging enough that this approach hasn't been that problematic thus far.
(disclaimer - I'm the founder of infield.ai; we're software for upgrading dependencies safely, even when there are breaking changes). I spent most of last year upgrading large Rails/React apps as a consultant so I have lots of thoughts on this. In general companies are in one of two states - deep in a hole of technical debt, or in a good place and trying to stay relatively up-to-date.
Most established JS/Ruby codebases I've seen have dependency technical debt and they _know_ they're going to run into breaking changes trying to upgrade. These companies need to do the project management work to break their debt down into as many small incremental changes as they can. Any upgrade that has a breaking change gets done standalone (you need to read the changelog to figure out if there's a breaking change, you don't trust semver). Upgrades that are necessary but safe can be batched together. When you have to make code changes for breaking changes these should also get done incrementally ahead of time. Usually these changes are backwards compatible (e.g., if a deprecated method is going away in v2.0, you can switch to the new method in v1.X so you're not upgrading the package and changing your code in the same PR). Even if they're not trivially backwards compatible you can often make them so, and you should. I've done individual upgrades consisting of dozens of small pull requests. When you do it this way you don't get stuck with a long-running branch that gets abandoned, you roll back fewer deploys, and changes are easy to review.
Once you're on the latest major version of all your packages then you want to get into an ongoing cadence of dependency maintenance. Good small-medium teams tend to do a maintenance rotation where you spend ~1 day/sprint of one developer's time on this upgrade work. Here project management is also important - ideally someone is looking at CVEs, stale packages, and other indicators of package risk, reading changelogs to identify effort, and breaking out maintenance tickets. Some of these tickets will be individual package upgrades, some will be batches of safe upgrades, and some (when a new major version of a framework comes out) will get treated like the previous paragraph. Larger teams might consolidate this work in a platform engineering team, but that has its own organizational challenges.
All those rules are derived from a much smaller set of rules that are easier to understand. In Rust, those rules are more or less "breaking changes require a major version, unless the breaking change could have been avoided by the caller by adding disambiguation ahead of time." More details here: https://predr.ag/blog/some-rust-breaking-changes-do-not-requ...
Rust generally has more exceptions than most languages about which breaking changes are technically considered non-major, so writing such guidelines for Python or Java shouldn't be too difficult.
The same tech and ideas that power `cargo-semver-checks` could also be repurposed for those languages as well. If a company is interested in sponsoring such work, I'd be happy to help build something like that!
For Java it should be easy to create such a list, if it doesn't exist (even easier than for Rust, I assume). For Python, I think the dynamicity would make it hard to come up with anything other than a subjective set of basic, non-comprehensive guidelines (which still sounds useful, but not from the perspective of a tool like this).
Semver is based on a lie. It assumes that you can define what is the proper behavior of a library. That's
not necessarily true, and can be subtlety false, and different depending on the use case. I can think on some situations where I trusted semantic versioning and it bited me when an update was made. I am not talking about new bug introductions. I assumed behavoir that the library owner assumed as not important.
Yes, no matter how hard you try semver will sometimes get it wrong, but as long as it usually doesn't it's still useful to let my tools do their best at automatically selecting a version of the library that will be compatible with every part of my application that cares, and that has had as many bugs fixed as possible. That's what semver allows cargo to do, and 99.9% of the time it just works. 0.1% of the time you have to manually intervene, and that's ok. It's better than having to manually intervene 100% of the time, or the old-school C solution of "just pick whatever is lying around on the OS and hope it works/is compatible".
In the end it's why Google insists on "vendoring" (checking in) third party dependencies and only having one version of a dependency for the entire (massive) mono repo and designated maintainers. You want a new version? Put a ring on it. Test it, and maintain it.
At first I thought it was insanity. Now, buried in a maze of transitive third party and first party dependencies scattered across multiple git repositories and crates, I really really miss it.
Of course like many other things there it depends on having lots of well-paid staff, without deadline guns to their head, committed to code quality and infrastructure.
Because it’s not perfect doesn’t mean it’s useless. If semver correctly describes 90% of behaviour changes (random number), that’s a significant load off my plate as a lib user
tldr: Semver dictates three version fields with specific definitions. At least one of those fields as defined is always useless to everyone except in a very rare circumstance. Another of those fields as defined is always risky to trust except in a very rare circumstance. Semver itself is silly. Better tooling is great but won't fix that.
...
This article starts with the wrong assumption that semver is itself meaningfully correct and useful. Semver is neither of those things except in a very specific circumstance that almost nobody has.
Semver violations are common because:
1. Semver as defined by semver.org is founded on the fundamentally false/flawed premise that you can predict whether a change will break compatibility. You can't. You should still try, but you should also recognize that sometimes you will guess wrong. And sometimes when you guess right you will still be wrong. The notion that just because you didn't add/remove an input or output parameter means you didn't change your API is extremely naive.
But this isn't the important reason. The important reason is...
2. Even if that weren't the case, it still would only make sense for approximately 0% of all versioned software projects, because almost no projects in the wild continually preserve separate feature branches with backported bug fixes, and continually preserved feature branches with backported bug fixes is the only scenario where semver's separation of features that aren't expected to break compatibility and fixes that aren't expected to break compatibility makes any kind of sense, because there is no such thing as a clean separation between feature and fix.
I hope that the truth of #1 is obvious to everyone, but if it isn't please refer to https://xkcd.com/1172/
Less pithily, internal changes alter outward behavior literally all the time in unintentional ways, and no software project on the planet defines and constrains the expectations for its interface with sufficient clarity to avoid that. Not one. It's probably not even possible.
As for #2, consider the following scenario...
You have released version 1.0.0 of something. Then you add a feature and fix a bug unrelated to that feature. Are you at version 1.1.0 or 1.1.1? Well, it depends on the order you added your changes, doesn't it? If you fixed the bug first you'll go from 1.0.0 to 1.0.1 to 1.1.0, and if you add the feature first you'll go from 1.0.0 to 1.1.0 to 1.1.1. And if that difference doesn't matter, then the last digit doesn't matter.
The problem here is that Semver treats feature and fix as fundamentally distinct from each other. But non-breaking is non-breaking. The user on the other side of the fence wants the best version of whatever you have that doesn't break compatibility with what they're doing. If they trust you to make that distinction, then they only care about your major versions. If they don't trust you to make that distinction, then they only care about strictly matching the whole string. If you trust yourself (lol), you can cater to both groups with two-part major.minor. If you don't trust yourself, you can just use a single value that increases over time. But semver has three fields, and one of those fields is basically always completely useless except to satisfy a contractual obligation.
There is only one scenario where three version fields matter, and that's when a government defense contract forces you to fork development at the start of each project and then maintain separate code branches for separate projects, where they only get the features they ask for and you only fix the bugs they ask you to fix (this is exactly how defense contracts work), and you laboriously backport bug fixes to all of them, and the major and minor versions indicate the point of the fork, and the patch version is all the changes applied to that fork.
Great, you agree then that this tool is useful because it will help you try to predict whether a change will break compatibility.
I can barely follow the rest of your comment. Semver is useful in, at least, a decentralized set of actors trying to minimally cooperate with one another by using a short number to communicate coarse but important information with respect to compatibility. Guess what that describes? A FOSS software ecosystem. So...
> Semver is neither of those things except in a very specific circumstance that almost nobody has.
> Great, you agree then that this tool is useful because it will help you try to predict whether a change will break compatibility.
No. I guess you didn't actually read what I said, and decided instead to construct a strawman by carving very few words out of it completely ignoring the rest of the words around them.
> I can barely follow the rest of your comment.
Ah ha.
> Semver is useful in, at least, a decentralized set of actors trying to minimally cooperate with one another by using a short number to communicate coarse but important information with respect to compatibility.
It's not any more useful than a system with only either date/incremental (this is newer than that) or major.minor (I promise that this change breaks, and that other change might also break but I hope not) versioning except for the one case I mentioned, which FOSS decentralization has absolutely nothing to do with.
cargo-semver-checks checks almost entirely only major.minor versioning issues. Rust's flavor of semver doesn't impose or demand any particular rules about what is a minor and what is a patch version.
So I'm a bit confused about the position you seem to be arguing against, when the tool and the entire ecosystem are doing exactly what you (AFAICT) consider a good idea.
Semantic Versioning 2.0.0
Summary
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes
MINOR version when you add functionality in a backward compatible manner
PATCH version when you make backward compatible bug fixes
Three fields. If you're saying that cargo-semver-checks doesn't need three fields, then you're saying that it doesn't benefit from semver as defined by semver.org.
I don't really follow this comments. It's a bit too rambly and circuitous for me to follow, and I gave it a few tries, but I do admit I'm not the best at reading walls of text.
From what I can tell, you're trying to argue that semver is useless while also saying that not following semver means that you don't get any benefits, but that just doesn't seem like a proper argument, so I'm surely misinterpreting.
I think SemVer is a social construct, but that most of the time we can predict what changes will be breaking. We may get it wrong of course, but that doesn't mean we should throw our hands up and not try.
cargo-semver-checks checks for breaking changes. minor and patch versions are defined to be backwards compatible and therefore not breaking. So why would it distinguish.
For tooling it's very hard to detect between a feature and a bug fix so it's almost impossible to automate that series of checks
> Because as a user of semver, I am not checking for breaking changes. I'm checking if I need to update because of a bug.
Semver doesn't help you do that. If you're on version 1.0.1 of my software, and I update to version 1.1.0 and then 1.1.1, have I fixed a bug that exists in 1.0.1? There's no way for you to know based on the version number.
Knowing whether you need to update because of a bug is something you can only find out from change logs, not from version numbers.
> This notion that just because you didn't add/remove an input or output parameter means you didn't change your API is extremely naive.
If I didn't change the behavior with any set of input parameters valid for the prior version, I haven't broken compatibility.
> because almost no projects in the wild continually preserve separate feature branches with backported bug fixes
Maybe not “continually”, but nore than 0% of real software projects using SemVer do release bug fix releases of m.f.b after feature releases of m.(f+1).0
> The user on the other side of the fence wants the best version of whatever you have that doesn't break compatibility with what they're doing.
They may not, because feature releases may be assumed to have greater risk of expanding the risk surface, whether or not they break backward compatibility.
> If I didn't change the behavior with any set of input parameters valid for the prior version, I haven't broken compatibility.
You don't anticipate that the behavior will change in a way that you care about, but that's not the same as not changing behavior. Different sides of an API nearly always have different definitions of "valid" and "behavior" here, and API documentation is never as concrete as would be needed to prevent that. So are you exhaustively fuzzing the input space? Because if you are, then great. But I bet you aren't. And even if you are, nobody else is.
> more than 0% of real software projects using SemVer...
I carefully said approximately. It gives us some leeway when talking about this without falling into the trap of believing that a small degree of adherence is meaningful.
> feature releases may be assumed to have greater risk of expanding the risk surface
I know it's common for people to feel that way, but there isn't a meaningful interface safety dividing boundary that any of us can point to between development of a feature vs a fix. Code is code is code is code. The developers can either reliably identify when changes will break users or not. If they can, then breaking.nonbreaking is plenty. If they can't, then only a single newer_than_before value is fine.
Like, if you want to argue for a new Dragonwriter's TrustVer where we talk about degrees of belief about whether a change will break something, we can do that. I'm all for it. But more than two values will still not be more useful to the user than at most two (except in the one contractual obligation case I presented), and it won't be Semver.
Compatibility is not the only thing version numbers are used for. If we assume bugfixes are a lot more common than new features, then being able to distinguish versions that contain new features from versions that don't is useful, if only for documentation. And if it's not useful to you then by all means treat increments to minor / patch the same when consuming other libraries, and always bump at least the minor version when you make a new release of your own libraries.
I suspect they meant the other way around - 1.2.18 is always newer than 1.1.10. But if you're running 1.1.18 and see 1.2.10, it would be nice if it were easy to tell wmhether your bug from 1.1.18 is potentially fixed in 1.2.10 or not.
Actually I didn't. Perhaps the 1.1.x branch is for legacy clients and only gets the occasional crucial backport, where the 1.2.x branch is the active you-should-use-this-one-for-new-stuff version, and sees frequent feature releases. It is the very treatment of semver as being equivalent to a monotonically increasing decimal that I object to.
What we did:
1. Scan Rust's most popular 1000 crates with `cargo-semver-checks`
2. Triage & verify 3000+ semver violations
3. Build better tooling instead of blaming human error
Around 1 in 31 releases had at least one semver violation.
More than 1 in 6 crates violated semver in at least one release.
These numbers aren't just "sum up everything `cargo-semver-checks` reported." We did a ton of validation through a combination of automated and manual means, and a big chunk of the blog post is dedicated to talking about that.
Here's just one of those validation steps. For each breaking change, we constructed a "witness," a program that gets broken by it. We then verified that it:
- fails to compile on the release with the semver-violating change
- compiles fine on the previous version
Along the way, we discovered multiple `rustc` and `cargo-semver-checks` bugs, and found out a lot of interesting edge cases about semver. Also, now you know another reason why it was so important to us to add those huge performance optimizations from a few months ago: https://predr.ag/blog/speeding-up-rust-semver-checking-by-ov...