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).
| Version | Meaning |
|---|---|
| 1.0.0 | First stable release |
| 1.0.1 | Bug fix, no new features |
| 1.1.0 | New feature, backward compatible |
| 2.0.0 | Breaking change (API redesign, etc.) |
| 2.1.0-beta.1 | Pre-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
git tag v1.2.3
Creating an annotated tag (recommended for releases)
git tag -a v1.2.3 -m "Release version 1.2.3: add user authentication"
Pushing tags to remote
git push origin v1.2.3
# Push all tags
git push origin --tags
Listing tags
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
- Development happens on feature branches.
- When ready for release, merge to
mainand update the version in your code (e.g., inpackage.json,setup.py, etc.). - Commit the version bump with message "Release v1.2.3".
- Create an annotated tag:
git tag -a v1.2.3 -m "Release v1.2.3" - 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 bumpfeat:commit -> MINOR bumpBREAKING CHANGE:in footer -> MAJOR bump
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
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:
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 -sfor GPG signatures. - Keep a changelog: Tools can generate it automatically from commit messages between tags.
Frequently Asked Questions
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.
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.
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".
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.