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.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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.

Start free on PandaStack

More in Guide

Browse all Guide articles →

See also