Skip to main content

Below 1.0, the minor number is the breaking one

Cargo and npm treat ^0.3.4 as any 0.3.y. Ship a breaking change as 0.3.5 and it lands in everyone's next build unasked. So we wrote the rule down.

Updated 4 min readMarkdown
Part of seriesFoxstash
Below 1.0, the minor number is the breaking one

Foxstash 0.9 removed a public storage mode. Anyone whose code named it gets a compile error on upgrade. That's a breaking change, so the release is 0.9.0, not 0.8.1.

That sounds obvious. It wasn't written down anywhere until this week, and the reason it matters below 1.0 is not the reason most people think.

What the caret actually does

When you add a dependency in Rust, cargo add writes something like foxstash-core = "0.8.0", which Cargo reads as ^0.8.0. npm writes "^0.8.0" outright. Both mean "this version, plus anything compatible with it". And both decide compatibility the same way:

You wrote Accepts Refuses
^1.3.4 1.3.5, 1.4.0, 1.99.0 2.0.0
^0.3.4 0.3.5, 0.3.99 0.4.0

Below 1.0, the caret treats the second number as the one that breaks things. So a dependent pinned to ^0.3.4 picks up every 0.3.y the next time anyone runs an update or builds without a lockfile, and refuses 0.4.0 until a person chooses it.

That makes the version number a promise. Ship a breaking change as 0.3.5 and it lands in every dependent's next build, unasked, and breaks it. Ship it as 0.4.0 and it waits until they opt in. The number is the only signal they get.

The rule

Semantic versioning, shifted one place while the major version is zero:

Change Before 1.0.0 (0.x.y) From 1.0.0 (x.y.z)
Breaking (! or BREAKING CHANGE:) x: 0.3.4 to 0.4.0 x: 1.3.4 to 2.0.0
New feature (feat) y: 0.3.4 to 0.3.5 y: 1.3.4 to 1.4.0
Fix or other released change (fix, perf) y: 0.3.4 to 0.3.5 z: 1.3.4 to 1.3.5

Below 1.0 there's no separate slot for features, so features and fixes share the last number. That loses a little information. It's the price of keeping the one signal that matters, "this will break you", in the position the tools actually read.

Two more lines go with the table.

Going to 1.0 is a decision, not a counter. It means the public API is stable. It shouldn't happen because a run of features rolled the number over.

Tell your release tool. By default, release-please treats a breaking change below 1.0 as a jump straight to 1.0.0, and bumps the minor on every feature, which under this rule marks every feature as breaking. Set bump-minor-pre-major and bump-patch-for-minor-pre-major to true and it follows the table.

Where it lives now

My company's software lifecycle procedure now has this table in its release stage, and a release isn't done until the version bump matches the type of change. The yearly internal audit checks it: for each sampled release, compare the commits since the last tag, and if any were breaking, the minor number had better have moved. The same table sits in the knowledge base my build machines read, word for word, because those machines can't see the company drive where the procedure lives.

Foxstash 0.9.0 was the first release after the rule was written down. It's breaking, it bumped the minor, and anyone on foxstash-core = "0.8" stays on 0.8 until they decide to move.

What I'd tell you if you were buying this work

If you depend on a library that's still below 1.0, read its changelog before each minor upgrade, and check whether the maintainers treat the minor number as the breaking one. If you're paying someone to ship a library to you, ask how they number releases and what stops a breaking change from going out as a patch. "We're careful" isn't an answer. A rule someone audits is.

About the author

  • Founder of Narcoleptic Fox. Marine veteran. Senior engineer learning AI/ML and writing about it. Building Mousing.ai, Reqforay,.ai, and foxstash.

No comments yet. Be the first to comment.

Foxstash

Part 3 of 3

Foxstash is my open-source Rust library for local vector, keyword and hybrid search, embedded in your application with no server. Releases, benchmarks, and the mistakes caught along the way.

Start from the beginningThe delete that didn't deleteThree Foxstash releases in four days: encryption at rest, a free lunch that wasn't, a delete that left the data on disk, and a position that wasn't an identity