Back to Blog
Guide7 min read2026-05-01

Semantic Versioning Explained: How to Version Your Software

Semantic versioning gives version numbers a predictable meaning that communicates compatibility — here is how to adopt it and automate it in your pipeline.

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 }}:latest

PandaStack 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.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also