Back to Versioning & Release
Q1Question 1 of 2

Versioning format for deployments (Semantic Versioning) and how the release/deployment process is managed

Key Concepts
  • Semantic Versioning uses MAJOR.MINOR.PATCH where MAJOR is breaking, MINOR is backward-compatible feature, PATCH is backward-compatible fix.
  • The version is assigned once and the artifact is immutable -- the same binary is promoted through dev, staging, and production.
  • Never deploy by the 'latest' tag; always use an explicit version or commit SHA so rollback is trivial.
  • Conventional commit messages (feat:, fix:, BREAKING CHANGE:) let tooling compute the correct version bump automatically.
  • The release pipeline runs tests, static analysis, builds one versioned image, and promotes it through environments with approval gates.
  • Rollback is re-pointing the deployment at the previous version tag, which works because the artifact is immutable.
  • The version is the join key that ties a running pod back to a commit, PR, build log, and changelog.

Definition

MAJOR means 'I broke the contract,' MINOR means 'I added something safe,' PATCH means 'I fixed something quietly' -- and you ship one versioned artifact everywhere, never rebuild per environment.
  • Semantic Versioning (SemVer) -- MAJOR.MINOR.PATCH where MAJOR is backward-incompatible changes, MINOR is backward-compatible new features, PATCH is backward-compatible bug fixes.
  • Immutable artifact -- the versioned binary (JAR, Docker image) is built once and promoted through environments, never rebuilt.
  • Conventional commits -- commit message format (feat:, fix:, BREAKING CHANGE:) that lets tooling compute the version bump.
  • Release pipeline -- CI/CD workflow that builds, tests, versions, publishes, and promotes the artifact through environments.
  • Promotion -- moving the same artifact from dev to staging to production, gated by automated checks and human approval.
  • Rollback -- re-pointing the deployment at the previous version tag.

Think of how National Bank of India numbers its cheque books. A whole new design that old scanners cannot read is a major change. Adding a new UPI reference field on the same design is a minor feature that old cheques still work alongside. Fixing a typo in the fine print is a patch. The bank does not reprint cheques at each branch -- it prints one official batch, stamps it with a serial, and ships that exact batch everywhere. SemVer is the serial on the software, and the release process is the controlled shipping of that one official batch.

Understanding the Concepts

What is Semantic Versioning?

Semantic Versioning gives three numbers meaning so anyone reading a version can predict the risk of upgrading. MAJOR is incremented for backward-incompatible changes. MINOR is incremented for backward-compatible new features. PATCH is incremented for backward-compatible bug fixes.

TEXT
Version: 2.4.1
  MAJOR = 2 (breaking changes from v1)
  MINOR = 4 (four feature additions since v2.0)
  PATCH = 1 (one bug fix since v2.4.0)

Pre-release: 2.5.0-rc.1 (release candidate)
Build metadata: 2.4.1+build.1842
JAVA
package com.bank.payments;

/**
 * Transfer Service API
 *
 * v1.0.0: Initial release -- NEFT transfers only
 * v1.1.0: MINOR -- added RTGS support (backward-compatible)
 * v1.2.0: MINOR -- added UPI support (backward-compatible)
 * v1.2.1: PATCH -- fixed rounding in fee calculation
 * v2.0.0: MAJOR -- changed response format (BREAKING)
 * v2.1.0: MINOR -- added daily transfer limit
 */
public class TransferService {

    public static final String VERSION = "2.1.0";

    public String getVersion() {
        return VERSION;
    }
}
TEXT
# Health endpoint reports version
curl https://transfer.nbi.com/actuator/info
{"version":"2.1.0","commit":"abc1234"}

Why must the artifact be immutable?

You build the artifact once and promote the same binary through environments. Never rebuild per environment. This means what you tested in staging is bit-for-bit what runs in production. Environment differences come from injected configuration, not from the code.

BASH
# Build once
mvn clean package -DskipTests
docker build -t nbi/transfer-service:2.1.0 .
docker push nbi/transfer-service:2.1.0

# Promote the SAME image through environments
kubectl --context=staging set image deployment/transfer-service \
  transfer-service=nbi/transfer-service:2.1.0

kubectl --context=production set image deployment/transfer-service \
  transfer-service=nbi/transfer-service:2.1.0
YAML
# WRONG -- different build for each environment
image: nbi/transfer-service:latest  # ambiguous, unreproducible

# CORRECT -- explicit version
image: nbi/transfer-service:2.1.0  # immutable, traceable

How do conventional commits automate version bumps?

Conventional commit messages follow a format that lets tooling compute the correct version bump. A feat: commit forces a MINOR bump. A fix: commit forces a PATCH bump. A BREAKING CHANGE: footer forces a MAJOR bump. The highest-impact commit since the last release wins.

TEXT
# Commit history since last release (v2.0.0)
fix: correct rounding in UPI fee calculation      -> PATCH
feat: add daily transfer limit for SAV accounts   -> MINOR
feat: add RTGS batch processing                   -> MINOR

# Highest impact: MINOR -> next version: 2.1.0
BASH
# Tool computes version and generates changelog
npx standard-version
TEXT
Bumping version from 2.0.0 to 2.1.0

Changelog:
## [2.1.0] - 2024-01-15
### Features
- add daily transfer limit for SAV accounts
- add RTGS batch processing
### Bug Fixes
- correct rounding in UPI fee calculation

How does the release pipeline work?

When a PR merges to main, the pipeline runs tests, static analysis, and security scans. If everything passes, it builds one versioned artifact, tags the commit, pushes the image, and promotes through environments with approval gates.

YAML
# .github/workflows/release.yml
name: Release Pipeline
on:
  push:
    branches: [main]
jobs:
  build-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
      - run: mvn clean verify
      - run: mvn sonar:sonar -Dsonar.qualitygate.wait=true

  release:
    needs: build-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - id: version
        run: echo "version=$(npx standard-version --dry-run | grep 'to' | awk '{print $NF}')" >> $GITHUB_OUTPUT
      - run: |
          docker build -t nbi/transfer-service:${{ steps.version.outputs.version }} .
          docker push nbi/transfer-service:${{ steps.version.outputs.version }}
      - run: |
          git tag v${{ steps.version.outputs.version }}
          git push origin v${{ steps.version.outputs.version }}

  deploy-staging:
    needs: release
    runs-on: ubuntu-latest
    steps:
      - run: |
          kubectl --context=staging set image deployment/transfer-service \
            transfer-service=nbi/transfer-service:${{ needs.release.outputs.version }}
          kubectl --context=staging rollout status deployment/transfer-service

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # requires manual approval
    steps:
      - run: |
          kubectl --context=production set image deployment/transfer-service \
            transfer-service=nbi/transfer-service:${{ needs.release.outputs.version }}
          kubectl --context=production rollout status deployment/transfer-service

How does rollback work?

Because artifacts are immutable and tagged with explicit versions, rollback is re-pointing the deployment at the previous version tag. No rebuilding, no guessing.

BASH
# Rollback to previous version
kubectl rollout undo deployment/transfer-service

# OR: explicitly set the previous version
kubectl set image deployment/transfer-service \
  transfer-service=nbi/transfer-service:2.0.0

# Verify
kubectl rollout status deployment/transfer-service
curl https://transfer.nbi.com/actuator/info
TEXT
{"version":"2.0.0","commit":"def5678"}

How does the version tie the whole toolchain together?

The version is the join key across your entire toolchain. A running pod reports version 2.1.0 on its health endpoint. That maps to a git tag, which maps to a commit, which maps to a PR, which maps to a changelog entry and build log.

TEXT
Running pod: version 2.1.0
  ↓
Git tag: v2.1.0
  ↓
Commit: abc1234
  ↓
Pull Request: #42 (feat: add daily transfer limit)
  ↓
Changelog: [2.1.0] - 2024-01-15
  ↓
Build log: CI run #187 (all tests passed, sonar passed)
JAVA
package com.bank.payments;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class VersionInfoContributor implements InfoContributor {

    @Value("${app.version:unknown}")
    private String version;

    @Value("${app.commit:unknown}")
    private String commit;

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("version", version)
               .withDetail("commit", commit);
    }
}
TEXT
GET /actuator/info
{"version":"2.1.0","commit":"abc1234"}

What are common mistakes?

The most common mistakes are deploying with the 'latest' tag, rebuilding per environment instead of promoting one artifact, manually deciding version bumps instead of using conventional commits, and not tying the version back to a commit.

YAML
# WRONG -- latest tag
image: nbi/transfer-service:latest
# Which version is this? When was it built? From which commit?
# Rollback is impossible to reason about.

# CORRECT -- explicit version
image: nbi/transfer-service:2.1.0
# Traceable to commit abc1234, PR #42, changelog entry.
# Rollback: just switch to 2.0.0.

What should a senior engineer know?

A senior engineer knows that the version number alone communicates upgrade risk. They enforce that released versions are immutable and never overwritten. They use conventional commits for deterministic version bumps and automated changelogs. They know that the version is the thread that ties incident response together -- when an alert fires, the first question is 'what version is running?' and from there you trace to the exact commit, PR, and build.

Visual Overview

DIAGRAM
  Semantic Versioning
  ───────────────────

  MAJOR . MINOR . PATCH
    2   .   4   .   1
    │       │       │
    │       │       └── Bug fix (backward-compatible)
    │       └────────── New feature (backward-compatible)
    └────────────────── Breaking change (incompatible)

  Release Pipeline:
  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
  │  Merge    │───▶│ Build +   │───▶│ Tag +     │───▶│ Promote   │
  │  to main  │    │ Test +    │    │ Push      │    │ through   │
  │           │    │ Analyze   │    │ Image     │    │ envs      │
  └──────────┘    └──────────┘    └──────────┘    └──────┬───┘
                                                         │
                                        ┌────────────────┼────────────┐
                                        ▼                ▼            ▼
                                   ┌────────┐     ┌──────────┐  ┌──────────┐
                                   │  Dev    │     │ Staging   │  │Production│
                                   │ (auto)  │     │ (auto)    │  │(approval)│
                                   └────────┘     └──────────┘  └──────────┘
                                                                 SAME image

  Version as Join Key:
  Pod: 2.1.0 → Tag: v2.1.0 → Commit: abc1234 → PR: #42 → Changelog → Build Log

Key Takeaway

Semantic Versioning uses MAJOR.MINOR.PATCH where each position communicates upgrade risk. MAJOR is a breaking change that requires migration notes. MINOR is a safe new feature. PATCH is a low-risk bug fix. Conventional commit messages let tooling compute the correct bump automatically, so version decisions are deterministic rather than debated.

The release process builds one immutable artifact -- a Docker image tagged with the version -- and promotes that same binary through dev, staging, and production. Environment differences come from injected configuration, not from rebuilding. Never deploy by the 'latest' tag because it makes rollback impossible to reason about.

Rollback is trivial when artifacts are immutable and explicitly versioned: re-point the deployment at the previous version tag. The version is the join key that ties a running pod back to a commit, a PR, a changelog entry, and a build log, which is what makes incident response fast and auditable.

Remember one sentence: build one versioned artifact, promote it through environments, never rebuild per environment, and the version number ties every running pod back to its exact commit for traceable rollback and auditing.

---

Share
Use ← → arrow keys to navigate