What each number means
| Position | Increment when | Examples |
|---|---|---|
| MAJOR | Incompatible API change | Removing an interface, changing parameter meaning or response shape |
| MINOR | Backwards-compatible addition | New optional parameter, new endpoint |
| PATCH | Backwards-compatible fix | Bug fix, docs, performance work that keeps semantics |
The test is compatibility for consumers, not the size of the diff. Refactoring ten thousand lines with identical external behaviour is a PATCH. Remember who reads a version number: it answers "can I upgrade safely?", not "how big was the change?".
The 0.x phase and pre-releases
- 0.x.y: the public API is not stable, so MINOR may contain breaking changes. Consumers should not assume 0.x upgrades are safe;
- Pre-releases:
1.0.0-alpha.1,-rc.1— they sort below the corresponding release; - Build metadata:
1.0.0+build.7— ignored when comparing precedence.
Ranges in dependency declarations
| Syntax | Meaning (given 1.2.3) | Risk |
|---|---|---|
| 1.2.3 (exact) | Only that version | Safest, but patches need manual bumps |
| ~1.2.3 | Any 1.2.x (patch level) | Low |
| ^1.2.3 | Any 1.x.y (minor level) | Medium — depends on the upstream's discipline |
| * or latest | Whatever exists | High — builds are not reproducible |
Note that ^0.2.3 usually means only 0.2.x, because MINOR can break compatibility during 0.x.
Four common traps
- Breaking changes shipped as MINOR: dependents break during routine upgrades — the most common breach of trust;
- Not committing lockfiles: without package-lock, Cargo.lock or go.sum there is no reproducible build;
- Overly wide ranges: one careless upstream release reaches everyone downstream;
- Using versions as marketing: jumping numbers for a "big release" destroys the meaning.
Making it stick
- Have CI verify the version actually bumps with the change;
- Adopt a commit convention so versions can be derived from history;
- Publish a changelog per release, calling out breaking changes and migration steps;
- Mark APIs deprecated and remove them only after at least one MINOR release.
Real-world cases: three upgrade incidents
- "A routine upgrade broke the service": the upstream shipped a breaking change as a MINOR. Fix: pin versions and route dependency upgrades through CI regression — do not blindly use
^. - "Works locally but CI builds differently": no lockfile committed, so different versions got installed. Fix: commit the lockfile and make CI install deterministically.
- "Bumping the major for hype": semantics broken for marketing, so consumers cannot judge upgrade risk. Fix: increment strictly by compatibility.
Common questions
Do internal services need semver? Yes, especially with multiple consumers. A date-based scheme is fine internally, but be consistent within one system. What level is a security fix? The fix itself is a PATCH; if behaviour changes, it is a MAJOR. What if a dependency ignores semver? Pin the exact version and test manually when upgrading.
Version numbers at a glance
1.4.2: major·minor·patch — a minor bump adds backward-compatible capability, a patch fixes issues compatibly;- 0.x is not stable: while the major is 0 any release may break; pin exact versions or commit a lockfile in production;
- Pre-releases:
1.0.0-beta.1sorts below1.0.0, and+buildmetadata is ignored in comparisons; - Ranges:
^1.2.3allows any 1.x,~1.2.3only 1.2.x,1.2.3pins exactly; - Commit the lockfile: without one, the same declarations can install differently on different machines.
Versioning inside the release flow
- Bump on merge so one release maps to one change;
- Group the changelog into added, fixed and breaking, with breaking items first and a migration example;
- Publish immutable artefacts archived by version, and roll back by switching versions rather than rebuilding.
A cadence for dependency upgrades
Do not let upgrades pile up. Security patches should be applied promptly even within the same major; minor bumps can be batched weekly or per iteration; major upgrades get their own slot, reading the migration guide and filling test gaps first. Regular small batches beat a half-yearly leap.