Version numbers are a promise to the people and systems that depend on your software. Semantic Versioning (SemVer) makes that promise explicit: from the number alone, users can tell whether an upgrade is safe.

The format

MAJOR.MINOR.PATCH     e.g. 2.4.1
  • MAJOR — incompatible changes (breaking an API, removing a feature, changing behaviour users rely on).
  • MINOR — new, backward-compatible functionality.
  • PATCH — backward-compatible bug fixes.

Examples:

Change 2.4.1 →
Fix a crash 2.4.2
Add an optional API field 2.5.0
Remove an endpoint, rename a config key 3.0.0

0.x versions signal “still unstable”: anything may change between minor versions. Releasing 1.0.0 means you’re committing to the public API.

Pre-releases and build metadata

3.0.0-alpha.1 < 3.0.0-beta.2 < 3.0.0-rc.1 < 3.0.0
1.4.0+build.517          # metadata, ignored for ordering

What counts as “breaking”?

Define your public API — HTTP endpoints, CLI flags, configuration files, library functions, database migrations users run. Breaking any documented behaviour is a major change. Internal refactors that don’t affect the public surface are patches (or minors if they add features).

For web applications deployed only by you, strict SemVer matters less, but a consistent scheme still helps: it labels deployments, links errors to versions, and makes rollbacks clear.

Tag releases in Git

git tag -a v2.5.0 -m "Release 2.5.0"
git push origin v2.5.0

Annotated tags store author, date and message. CI can build and publish artifacts automatically on tag pushes:

# GitHub Actions
on:
  push:
    tags: ['v*.*.*']

Keep a changelog

A human-readable CHANGELOG.md, grouped by release:

## [2.5.0] - 2026-11-04
### Added
- Export orders as CSV.
### Fixed
- Timezone bug in reports.
### Deprecated
- `/api/v1/legacy-export` (removal in 3.0).

Users read changelogs to decide when to upgrade; you read them when debugging “what changed?”.

Automating versions with Conventional Commits

Conventional Commits encode the change type in commit messages:

feat: add CSV export            → minor
fix: correct report timezone    → patch
feat!: remove legacy export     → major (or "BREAKING CHANGE:" in the body)

Tools like release-please, semantic-release or changesets read these commits, calculate the next version, update the changelog, tag the release and publish it — releases become a pull-request merge.

Docker image tags

Publish several tags per release so users choose their update policy:

myapp:2.5.0   # exact, never changes
myapp:2.5     # latest patch of 2.5
myapp:2       # latest minor of 2
myapp:latest  # whatever is newest (avoid in production)

Plus an immutable tag per commit (myapp:sha-a1b2c3d) for deployments. Consumers pinning myapp:2 get fixes and features without surprise breaking changes. See Docker image tags best practices and how this interacts with Watchtower.

Dependencies: version ranges

Package managers use SemVer to decide what to install:

  • ^2.4.1 (npm) — >=2.4.1 <3.0.0
  • ~2.4.1 — >=2.4.1 <2.5.0
  • "2.4.1" — exactly that version

Commit lockfiles (package-lock.json, composer.lock, poetry.lock) so builds are reproducible regardless of ranges, and let tools like Renovate or Dependabot propose updates.

Deprecation policy

Before removing something in a major version:

  1. Mark it deprecated in a minor release (docs, changelog, warnings in logs or response headers).
  2. Give users a migration path and time.
  3. Remove it in the next major release.

Alternatives

  • CalVer (2026.11.0) — date-based versions, common for applications and distributions (Ubuntu 24.04) where “compatibility” is less meaningful than “how recent”.
  • Build numbers / commit SHAs — fine for internal services deployed continuously.

Related: Git tags and releases and CI/CD pipelines explained.

  • Versioning
  • Releases
  • DevOps