Semantic Versioning & Release Schemes
Semantic Versioning & Release Schemes
Version numbers are not cosmetic. At big-tech scale they are a contract between producers and consumers of software: a promise about what changed, how safe it is to upgrade, and whether automation can promote a build without human review. Picking the wrong scheme — or applying it inconsistently — causes real production incidents: a MINOR bump that silently ships a breaking API change, or a CalVer date that causes a stale dependency to be pinned forever because no one knows whether the jump is safe. This lesson covers every versioning scheme used in production, the pre-release ladder, and the tooling that automates bumps so humans never touch a version number by hand.
SemVer: The Three-Part Contract
Semantic Versioning (semver), defined at semver.org, uses a three-integer tuple: MAJOR.MINOR.PATCH. The rules are precise and machine-enforceable.
- PATCH — backward-compatible bug fix. No public API changes. Consumers can auto-upgrade safely.
- MINOR — backward-compatible new functionality added. Existing calls continue to work. Consumers can auto-upgrade safely.
- MAJOR — incompatible API change. Consumers must read release notes and opt in explicitly. Bumping MAJOR signals a migration path is required.
These rules make dependency managers reliable: a consumer declaring ^2.3.0 (caret range in npm) will automatically install any 2.x.y where x >= 3 without human intervention, trusting that MINOR and PATCH bumps are safe. The contract breaks — and production breaks — the moment you violate it. If a MINOR release removes a method, every downstream service using that method breaks at their next npm install or pip install --upgrade, silently, days after the release ships.
Pre-release Identifiers and the Release Ladder
SemVer supports two optional suffixes with strict semantics. Understanding them is essential because a CI/CD promotion pipeline maps directly onto the pre-release ladder.
- Pre-release identifier — appended with a hyphen:
2.0.0-alpha.1,2.0.0-beta.3,2.0.0-rc.1. Pre-releases sort lower than the release itself:2.0.0-rc.1 < 2.0.0. A package manager running in default mode will never auto-upgrade a stable consumer to a pre-release — the consumer must explicitly opt in (e.g.,npm install mylib@nextorpip install --pre mylib). - Build metadata — appended with a plus:
2.0.0+20260611.sha.a3f9d12. Build metadata is completely ignored when comparing versions; two artifacts with different metadata are semver-equal. Use build metadata to embed the git SHA, CI run ID, or build timestamp for human traceability — without affecting version ordering or dependency resolution.
In a mature release pipeline, the pre-release ladder maps to promotion gates. The artifact is built once and re-tagged at each gate — the binary never changes:
CalVer: Versioning by Date
Calendar Versioning (CalVer) encodes the release date into the version string. It is the right choice when "when was this released" is more actionable to consumers than "what semantically changed." Common formats in production systems:
YYYY.MM— Ubuntu:24.04(April 2024 LTS),24.10. The date directly signals the support window end date.YYYY.MM.MICRO— Django used this historically:2024.6.0,2024.6.1. The micro segment is a patch counter within the month.YYYY.MINOR— pip:24.1,24.2. Minor increments within a year, no month granularity.YYYY.0M.DD— some internal Google toolchains use ISO-style full dates for daily release trains.
CalVer is a poor fit for libraries with many downstream consumers and frequent breaking changes: there is no built-in signal that 2024.07 → 2024.08 is safe to auto-upgrade or requires a migration. For libraries with stable APIs but occasional big shifts, a hybrid works well: 24.6.0 where 24.6 is CalVer (year.month) and the last segment is semver-style patch count. Kubernetes uses pure semver (1.30.2) but publishes minor releases on a roughly quarterly cadence — effectively acting like a release train with semver labels.
Automating Version Bumps: semantic-release
semantic-release is the standard tool for zero-human-intervention semver automation. It reads your git commit history — specifically commits formatted as Conventional Commits (feat:, fix:, feat!:) — determines the correct next version, generates a changelog, publishes the artifact, creates a GitHub release, and tags the commit. No engineer ever edits a version number by hand.
The branch configuration above implements a full multi-channel strategy: main produces stable GA releases, the next branch produces x.y.z-next.N pre-releases for cutting-edge consumers, beta produces x.y.z-beta.N builds, and maintenance branches like release/2.x receive backport patches to the 2.x line without disturbing main. This is exactly the model used by large open-source projects like Babel, Jest, and semantic-release itself.
Python and Container Ecosystem Specifics
Not every project uses the Node.js toolchain. The same semver-from-commits discipline applies across ecosystems, but the tooling differs:
- Python:
python-semantic-releasereads Conventional Commits and bumpspyproject.toml. It publishes to PyPI and creates GitHub releases. PEP 440 pre-release syntax is slightly different:2.0.0a1(alpha),2.0.0b2(beta),2.0.0rc1— no hyphen. Tools likepipunderstand both PEP 440 and semver formats. - Go modules: semver is enforced by the Go toolchain. A v2+ module must have a
/v2import path suffix (e.g.,github.com/myorg/mylib/v2). The Go proxy caches immutable module versions — you cannot re-tagv2.0.0after it has been cached. - Container images: there is no standardized version-from-commits tool for OCI images, but the convention is to tag with the semver string alongside the git SHA. A typical CI job produces both
myimage:2.3.1andmyimage:sha-a7f3d91pointing to the same digest. The semver tag is for human consumers; the SHA tag is used in deployment manifests for immutability.
Version Scheme at Scale: What Actually Breaks
The theory is clean; production is messier. These are the failure modes that actually occur at scale when versioning discipline breaks down:
- Zero-based major versions: semver specifies that
0.y.zis for initial development — the API is unstable and anything may change at any time. Many teams keep libraries at0.xfor years, giving consumers a false sense that minor bumps are safe. If your library is used in production, ship1.0.0. - Re-tagging published versions: on npm, PyPI, and Go proxy, versions are generally immutable once published. Re-tagging (deleting and re-publishing the same version) breaks deterministic builds for every consumer who resolved that version. On GitHub Container Registry, re-pushing a mutable tag breaks this guarantee for image consumers. Never re-tag a published release.
- Version drift in monorepos: in a monorepo with 50 packages, it is tempting to version all packages in lockstep (all at
2.3.0regardless of what changed). This violates the semver contract: packages that had no changes should not bump. Tools like Lerna, Nx, and Changesets support independent versioning — each package versions separately based on its own commit history. - Missing pre-release channel: teams that skip the pre-release ladder and ship directly from
alphato GA give consumers no opportunity to validate the upgrade in non-production environments. The pre-release channel is as important as the stable channel — it is the safety valve that catches breaking changes before they reach the full install base.
v* so that only the CI bot (the service account running semantic-release) can create or delete version tags. This prevents an engineer from manually pushing v2.0.0 over the CI-managed tag, which would corrupt the release history and potentially cause consumers on cached versions to get the wrong artifact.
The next lesson covers artifact repositories — where versioned artifacts live between build and deployment, how registries enforce immutability, and how to configure retention policies that balance storage cost against the compliance need to reproduce any release from the last seven years.