Versioning Strategies: Semantic Versioning with Git Tags

Why Versioning Matters

Versioning is the practice of assigning unique identifiers to software releases. It helps developers, users, and automated tools understand what changes are included, whether an update is safe, and how different versions relate to each other. Git tags are the natural way to mark these versions in your repository.

Key idea: A version number is a promise. It tells users what to expect: bug fixes, new features, or breaking changes. Semantic Versioning (SemVer) makes this promise explicit.

Semantic Versioning (SemVer) Basics

SemVer uses a three-part number: MAJOR.MINOR.PATCH. For example, 2.5.1.

  • MAJOR – increment when you make incompatible API changes (breaking changes).
  • MINOR – increment when you add functionality in a backward-compatible manner (new features).
  • PATCH – increment when you make backward-compatible bug fixes.

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format (e.g., 1.0.0-alpha, 2.1.0+build.123).

VersionMeaning
1.0.0First stable release
1.0.1Bug fix, no new features
1.1.0New feature, backward compatible
2.0.0Breaking change (API redesign, etc.)
2.1.0-beta.1Pre-release of a new feature (not stable yet)

Git Tags: The Mechanism for Versioning

A Git tag is a reference that points to a specific commit. Unlike branches, tags do not move. They are perfect for marking release points.

Creating a lightweight tag

# Create a tag for version 1.2.3
git tag v1.2.3

Creating an annotated tag (recommended for releases)

# Annotated tags store a message, author, and date
git tag -a v1.2.3 -m "Release version 1.2.3: add user authentication"

Pushing tags to remote

# Push a specific tag
git push origin v1.2.3

# Push all tags
git push origin --tags

Listing tags

git tag
git tag -l "v1.*" # List tags matching pattern

Integrating SemVer with Git Tags

The common convention is to prefix the version number with a "v" (e.g., v1.2.3). Tools and automation often rely on this pattern.

Example release workflow

  1. Development happens on feature branches.
  2. When ready for release, merge to main and update the version in your code (e.g., in package.json, setup.py, etc.).
  3. Commit the version bump with message "Release v1.2.3".
  4. Create an annotated tag: git tag -a v1.2.3 -m "Release v1.2.3"
  5. Push the commit and the tag: git push && git push --tags

Pro tip: Use CI/CD to automate version bumping and tagging. For example, semantic-release (JavaScript) analyzes commits and automatically determines the next version number, updates the changelog, and creates a Git tag.

Automated version bumping with commit messages

Tools like semantic-release or commit-and-tag-version (formerly standard-version) follow the Conventional Commits specification to determine the next SemVer version:

  • fix: commit -> PATCH bump
  • feat: commit -> MINOR bump
  • BREAKING CHANGE: in footer -> MAJOR bump
# Example conventional commit
feat(api): add login endpoint

BREAKING CHANGE: The login endpoint now requires a token.

Tagging strategies for complex projects

1. Mono-repo versioning

In a monorepo with multiple packages, you can either use a single global version (all packages released together) or independent versioning with tags per package: pkg-a@1.2.3, pkg-b@2.0.1.

2. Pre-releases and release candidates

Use SemVer extensions: v2.0.0-rc.1 (release candidate), v1.3.0-beta.2. These are considered "pre-releases" and can be sorted correctly by SemVer-aware tools.

3. Backport tags (for hotfixes on older versions)

If you need to fix a bug in an older release, create a branch from the old tag (e.g., v1.2.x), apply the fix, and tag it as v1.2.1.

Viewing and comparing tags

# Show details of a tag
git show v1.2.3

# Compare changes between two tags
git diff v1.2.0 v1.2.1

# See which commits are in v1.3.0 but not in v1.2.0
git log v1.2.0..v1.3.0 --oneline

Integrating with CI/CD for release automation

When you push a tag, your CI server can automatically build and deploy that exact version. For example, in GitHub Actions:

# .github/workflows/release.yml
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and publish
        run: ...

Common pitfalls and best practices

  • Don't move tags: Tags should be immutable. If you need to fix a release, create a new patch version.
  • Always use annotated tags for releases: They store metadata that is important for auditing.
  • Version your code and your tags consistently: The version in your code manifest (e.g., package.json) should match the Git tag.
  • Sign your tags if security is critical: git tag -s for GPG signatures.
  • Keep a changelog: Tools can generate it automatically from commit messages between tags.
Which part of a semantic version (MAJOR.MINOR.PATCH) should you increment if you add a new backward‑compatible feature?
  • MINOR
  • MAJOR
  • PATCH
  • BUILD

Frequently Asked Questions

What is the difference between a lightweight and an annotated tag?

A lightweight tag is just a pointer to a commit (like a branch that never moves). An annotated tag is stored as a full object in Git: it contains a tagger name, email, date, and a tagging message. Annotated tags are recommended for public releases because they include metadata and can be GPG-signed.

Should I include the "v" prefix (e.g., v1.2.3) or just use 1.2.3?

Both are common, but the "v" prefix is widely adopted (Git itself uses it, as do many projects). It clearly distinguishes tags that are versions from other tags. If you use automation, ensure it can handle either; most tools are flexible.

How do I handle versioning when I need to release a hotfix for an older major version?

Create a new branch from the tag of the previous release (e.g., git checkout -b v1.2.x v1.2.0), apply the fix, and tag it as v1.2.1. This is often called a "release branch" or "maintenance branch".

Can I have multiple tags on the same commit?

Yes, you can. This is sometimes used when a release is known by different names (e.g., v1.2.3 and latest-stable). However, for versioning it's cleaner to have exactly one SemVer tag per release commit.

Previous: GitOps Principles Next: Release Automation