Skip to content

Software & Package Versioning

A comprehensive guide to software and package versioning — from beginner concepts to advanced strategies.


Software changes over time. Versioning is how developers communicate change to other developers (and their future selves). A version number is a label that answers:

  • What state is this code in right now?
  • Has anything broken since the last release?
  • Is it safe for me to upgrade?

Real-world analogy

Think of versioning like book editions. A 2nd Edition textbook has updates, corrections, or new chapters compared to the 1st. The edition number tells you what to expect before you open the book.


The most common format you’ll see is:

MAJOR.MINOR.PATCH

Example: 2.4.1

PartNumberMeaning
MAJOR2Big change — may break existing code
MINOR4New feature — backward compatible
PATCH1Bug fix — no new features

This format is called Semantic Versioning (SemVer) and is the industry standard.


package.json
{
"dependencies": {
"react": "^18.2.0",
"lodash": "~4.17.21",
"axios": "1.6.0"
}
}

SymbolExampleMeaning
^^1.2.3Allow MINOR and PATCH updates (>=1.2.3 <2.0.0)
~~1.2.3Allow PATCH updates only (>=1.2.3 <1.3.0)
>=>=1.2.0Any version equal to or greater
==1.2.3Exactly this version only
**Any version (avoid in production!)
-1.2.0 - 2.0.0Range between two versions (inclusive)

🟡 Intermediate — Semantic Versioning Deep Dive

Section titled “🟡 Intermediate — Semantic Versioning Deep Dive”

Semantic Versioning (semver.org) defines a strict contract:

  1. PATCH — Increment when you make backward-compatible bug fixes.
  2. MINOR — Increment when you add functionality in a backward-compatible manner. Reset PATCH to 0.
  3. MAJOR — Increment when you make incompatible API changes. Reset MINOR and PATCH to 0.

Full version string format:

MAJOR.MINOR.PATCH[-pre-release][+build-metadata]

Examples:

1.0.0-alpha
1.0.0-alpha.1
1.0.0-beta.2
1.0.0-rc.1
1.0.0
1.0.1
1.1.0
2.0.0
2.0.0+build.20240101

IdentifierPurposeExample
alphaEarly internal testing, unstable2.0.0-alpha.1
betaFeature-complete, public testing2.0.0-beta.3
rc (Release Candidate)Final testing before stable release2.0.0-rc.1
+buildBuild metadata (ignored in comparisons)1.0.0+20240501

While your project is in initial development, MAJOR version zero (0.y.z) has special meaning:

  • Anything may change at any time.
  • The public API is not yet stable.
  • 0.1.00.2.0 may include breaking changes.

When to go 1.0.0

Release 1.0.0 when your public API is stable, you have real users depending on it, and you’re committed to backward compatibility.


Lock files record the exact resolved version of every dependency (including transitive ones). They ensure every developer and CI environment installs identical code.

Package ManagerLock File
npmpackage-lock.json
Yarnyarn.lock
pnpmpnpm-lock.yaml
piprequirements.txt (pinned) / poetry.lock
CargoCargo.lock
Bundler (Ruby)Gemfile.lock
Composer (PHP)composer.lock

  1. Audit your current dependencies for known vulnerabilities:

    Terminal window
    npm audit
    pip-audit
    cargo audit
  2. Check outdated packages:

    Terminal window
    npm outdated
    pip list --outdated
    cargo outdated
  3. Update PATCH/MINOR (generally safe):

    Terminal window
    npm update
    pip install --upgrade <package>
    cargo update
  4. Review changelogs before updating MAJOR versions.

  5. Run your test suite after every update.


🔴 Advanced — Versioning Strategies & Tooling

Section titled “🔴 Advanced — Versioning Strategies & Tooling”

A commit message convention that drives automated versioning:

<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
TypeTriggersExample
fix:PATCH bumpfix: handle null pointer in auth
feat:MINOR bumpfeat: add OAuth2 login support
BREAKING CHANGE:MAJOR bumpfeat!: remove deprecated v1 API
docs:No releasedocs: update README
chore:No releasechore: upgrade devDependencies
refactor:No releaserefactor: simplify token parsing
perf:PATCH bumpperf: cache user session lookups

Example commit with breaking change:

feat!: migrate config format to YAML
BREAKING CHANGE: JSON config files are no longer supported.
Migrate your config.json to config.yaml before upgrading.

Automated Versioning with semantic-release

Section titled “Automated Versioning with semantic-release”

semantic-release fully automates version management:

  1. Analyzes commits using Conventional Commits.
  2. Determines the next version (MAJOR/MINOR/PATCH).
  3. Generates a changelog from commit messages.
  4. Publishes to npm/PyPI/GitHub Releases.
  5. Creates a Git tag for the release.

Basic .releaserc.json:

{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
]
]
}

Follow the Keep a Changelog format:

# Changelog
All notable changes to this project will be documented here.
## [Unreleased]
## [2.1.0] - 2024-06-01
### Added
- OAuth2 login support (#142)
### Fixed
- Null pointer exception in token refresh (#138)
## [2.0.0] - 2024-05-01
### Breaking Changes
- Removed deprecated v1 REST endpoints
- Config format changed from JSON to YAML
### Added
- New GraphQL API

Managing multiple packages in a single repository requires specialized tooling.

Changesets (JS/TS)

Recommended for npm monorepos. Developers add “changeset” files describing their changes; releases are batched. bash npx changeset add npx changeset version npx changeset publish

Lerna

Classic JS monorepo tool. Supports independent or fixed versioning across packages. bash lerna version lerna publish

Nx Release

Built into Nx workspaces. Supports conventional commits and programmatic release management. bash nx release version nx release publish

Cargo Workspaces

Rust workspaces share a Cargo.lock. Use cargo-workspaces crate for coordinated releases. bash cargo workspaces version patch cargo workspaces publish


StrategyBest ForProsCons
SemVerLibraries, public APIsUniversal standard, clear intentRequires discipline
CalVerApplications, OS distrosDate conveys release timingDoesn’t communicate impact
GitHashInternal/microservicesAlways unique, traceableNo human-readable meaning
SequentialSimple internal toolsSimple to implementNo semantic information
RomanticConsumer products (e.g. Ubuntu)Memorable codenamesRequires parallel numeric version

CalVer format examples:

Ubuntu: 22.04.3 LTS → YYYY.MM.PATCH
pip: 23.3.1 → YY.MINOR.PATCH
Black: 24.4.2 → YY.MM.PATCH

When your software exposes an API, versioning strategy impacts clients directly.

GET /api/v1/users GET /api/v2/users
✅ Explicit, easy to test

❌ URL bloat, hard to deprecate


Responsible versioning includes a clear deprecation lifecycle:

  1. Announce deprecation in release notes and docs with the target removal version.
  2. Emit warnings at runtime when deprecated features are used.
  3. Maintain deprecated code for at least one MAJOR version cycle.
  4. Remove in the next MAJOR version with a clear migration guide.

Example deprecation notice in code:

/**
* @deprecated since v3.0.0 — use `authenticateWithOAuth()` instead.
* Will be removed in v4.0.0.
*/
export function legacyLogin(username: string, password: string) {
console.warn('[DEPRECATED] legacyLogin() will be removed in v4.0.0.');
// ...
}

A production-grade automated release pipeline:

.github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for semantic-release
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release

Did you change the public API in a breaking way?
├── YES → Bump MAJOR (e.g. 1.4.2 → 2.0.0)
└── NO → Did you add new functionality?
├── YES → Bump MINOR (e.g. 1.4.2 → 1.5.0)
└── NO → Bump PATCH (e.g. 1.4.2 → 1.4.3)
Terminal window
# npm — interactive version bump
npm version patch # 1.0.0 → 1.0.1
npm version minor # 1.0.0 → 1.1.0
npm version major # 1.0.0 → 2.0.0
npm version prerelease --preid=beta # 1.0.0 → 1.0.1-beta.0
# Python (Poetry)
poetry version patch
poetry version minor
poetry version major
# Cargo (Rust) — edit Cargo.toml, then:
cargo build # validates new version