What Is Semantic Versioning?
Semantic versioning (SemVer) is a versioning convention described at semver.org. Every version number takes the form MAJOR.MINOR.PATCH — for example, 2.4.1. Each segment has a precise meaning:
- MAJOR — incompatible API changes. Consumers must update their code.
- MINOR — new backward-compatible functionality. Consumers can upgrade safely.
- PATCH — backward-compatible bug fixes only.
# Examples
1.0.0 → Initial stable release
1.1.0 → New feature added (backward compatible)
1.1.1 → Bug fix (backward compatible)
2.0.0 → Breaking change (consumers must adapt)Version numbers are not arbitrary labels — they are a contract with anyone who depends on your software.
Pre-Release and Build Metadata
SemVer also supports pre-release identifiers and build metadata:
1.0.0-alpha.1 # Alpha pre-release (unstable)
1.0.0-beta.3 # Beta pre-release (feature-complete but untested)
1.0.0-rc.1 # Release candidate (final validation)
1.0.0 # Stable release
1.0.0+20260501 # Build metadata (ignored in precedence)Pre-release versions have lower precedence than the associated normal version: 1.0.0-rc.1 < 1.0.0.
Conventional Commits: The Foundation for Automated Versioning
Conventional Commits is a commit message format that encodes version intent directly in your Git history:
# PATCH bump — bug fix
git commit -m "fix: resolve null pointer in user profile loader"
# MINOR bump — new feature
git commit -m "feat: add webhook event filtering by event type"
# MAJOR bump — breaking change (note the footer)
git commit -m "feat!: rename /api/v1/users to /api/v2/members
BREAKING CHANGE: The /api/v1/users endpoint has been removed.
Clients must migrate to /api/v2/members."The format: type(optional-scope): description. Valid types include fix, feat, chore, docs, refactor, test, perf, and ci.
Automating Versioning in CI/CD
With conventional commits in place, tools like semantic-release can automatically determine the next version, generate a changelog, and create a Git tag — all from your CI pipeline.
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog generation
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run tests
run: npm test
- name: Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release# .releaserc.yml — semantic-release configuration
branches:
- main
- name: beta
prerelease: true
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]'Versioning Docker Images
For containerised applications on PandaStack, tag Docker images with the SemVer version alongside latest:
# Build and tag with SemVer
VERSION=$(node -p "require('./package.json').version")
docker build -t myapp:$VERSION -t myapp:latest .
docker push myapp:$VERSION
docker push myapp:latest# .github/workflows/publish-image.yml
- name: Build and push image
run: |
VERSION=$(node -p "require('./package.json').version")
docker build -t ghcr.io/${{ github.repository }}:$VERSION -t ghcr.io/${{ github.repository }}:latest .
docker push ghcr.io/${{ github.repository }}:$VERSION
docker push ghcr.io/${{ github.repository }}:latestPandaStack container deployments can be pinned to a specific image tag, so promoting v2.1.0 to production is a deliberate act rather than an implicit latest tag pull.
Enforcing Conventional Commits
Use commitlint with a commit-msg hook to reject commits that do not follow the convention:
# Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
# Configure
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# Add the git hook
npx husky install
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'With commitlint enforced locally and semantic-release running in CI, versioning becomes entirely automatic. Every commit contributes to a meaningful changelog, every release is tagged, and anyone consuming your software knows exactly what each version number means.