Best Practices
Tagging and Versioning
Use Semantic Versioning (SemVer) and annotated tags to establish clear, traceable release conventions.
- Individuals or teams who want more predictable Git habits
- Maintainers setting collaboration expectations
- At least one real collaboration loop
- Basic command familiarity without a stable routine yet
- Treating guidance as absolute law without context
- Memorizing process without understanding team boundaries
The short version
Tags are immutable references pointing to specific commits in Git, making them ideal for marking release points. Combined with Semantic Versioning (SemVer), version numbers themselves communicate the nature and impact of changes.
Update CHANGELOGBump versionCreate tag
Traceable versionsComplete release docsDetermined rollback point
Never save time on tag naming. Consistent SemVer naming is the foundation of team communication.
Semantic Versioning (SemVer)
Version format: MAJOR.MINOR.PATCH
1.2.3
│ │ │
│ │ └── Patch: backward-compatible bug fixes
│ └──── Minor: backward-compatible new features
└────── Major: incompatible API changes
Examples:
v1.0.0 → First stable release
v1.1.0 → New feature, backward compatible
v1.1.1 → Bug fix, backward compatible
v2.0.0 → Breaking change, incompatible with v1.x
Prerelease versions
v1.0.0-alpha.1 → Internal testing
v1.0.0-beta.2 → Public beta
v1.0.0-rc.1 → Release candidate
Tag type selection
Lightweight tag
# Just an alias for a commit, no metadata
git tag v1.0.0
# Suitable for: temporary markers, personal local use
Annotated tag — recommended
# Contains tag name, tag message, date, and tagger
git tag -a v1.0.0 -m "Release version 1.0.0"
# View tag details
git show v1.0.0
# Suitable for: official releases, all team collaboration scenarios
Signed tag
# Sign the tag with GPG to verify the tagger's identity
git tag -s v1.0.0 -m "Release version 1.0.0"
# Verify the signature
git tag -v v1.0.0
Release tagging workflow
Before release
# 1. Confirm current version
git describe --tags --abbrev=0
# v1.1.2
# 2. View changes since the last release
git log v1.1.2..HEAD --oneline
# 3. Determine the version number based on change types
# Bug fixes only → v1.1.3
# New features added → v1.2.0
# Breaking changes → v2.0.0
During release
# 1. Ensure you are on the correct branch
git checkout main
git pull origin main
# 2. Run full tests
npm test
npm run build
# 3. Update version numbers (package.json, etc.)
npm version patch # or minor / major
# This creates a commit and a tag
# 4. Manually create a tag (if not using npm version)
git tag -a v1.2.0 -m "Release v1.2.0
Features:
- Add dark mode support
- Improve search performance
Fixes:
- Fix login redirect issue
- Fix memory leak in dashboard"
# 5. Push code and tags
git push origin main
git push origin v1.2.0
After release
# 1. Verify the tag was pushed
git ls-remote --tags origin | grep v1.2.0
# 2. Create Release Notes on GitHub/GitLab
# Should include: change summary, breaking change notes, migration guide
# 3. If an issue is found, prepare a hotfix branch
git checkout -b hotfix/v1.2.1 v1.2.0
Tag naming conventions
Recommended formats
# Production releases
v1.0.0
v1.2.3
# Prereleases
v1.0.0-alpha.1
v1.0.0-beta.2
v1.0.0-rc.1
# Deployment markers (optional)
deployed-v1.2.3-prod
deployed-v1.2.3-staging
Practices to avoid
# Do not use dates as version numbers
git tag 20240115 ❌
# Do not mix formats
git tag version-1.0 ❌
git tag rel_1_0 ❌
# Do not apply multiple tags with the same meaning to the same commit
Tag management and maintenance
Viewing tags
# List all tags
git tag
# List tags matching a pattern
git tag -l "v1.2.*"
# Sort by version
git tag -l "v*" --sort=-version:refname
# View tag details
git show v1.2.0
Deleting tags
# Delete a local tag
git tag -d v1.2.0
# Delete a remote tag
git push origin --delete v1.2.0
Syncing tags
# Fetch all remote tags
git fetch --tags
# Push all local tags
git push origin --tags
# Push a specific tag
git push origin v1.2.0
CI/CD integration
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- name: Build and test
run: |
npm ci
npm test
npm run build
- name: Create release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
Version change decision tree
What changed since the last release?
├── Bug fixes only, no behavior changes
│ └── PATCH +1 → v1.1.2 → v1.1.3
├── New features, backward compatible
│ └── MINOR +1 → v1.1.2 → v1.2.0
├── API removed or incompatible behavior changes
│ └── MAJOR +1 → v1.1.2 → v2.0.0
└── Nothing (empty release)
└── Do not tag
Best practices summary
- Always use annotated tags: Lightweight tags have no metadata and are unsuitable for official releases
- Strictly follow SemVer: Version numbers are not just numbers; they communicate compatibility promises
- One tag per release: Do not skip tags, and do not apply multiple tags to the same release
- Detailed tag messages: Describe what changed; do not just write "Release v1.0.0"
- Push tags when pushing: Do not forget
git push origin --tags - Protect tags: Set tag protection rules in GitHub/GitLab
- Clean up prerelease tags regularly: Too many alpha/beta tags create clutter
Key takeaways
- Once a tag is pushed, do not modify it. If you realize a tag is wrong, delete and recreate it; do not force push the tag
- Moving a tag (force push tag) causes local tags to diverge from the remote
- Tags are not branches; you cannot commit directly on them. You must create a branch from the commit the tag points to in order to continue development
- Some automation tools depend on tags to trigger; tag names must match tool configurations
- In a monorepo, consider using prefixed tags (e.g.,
@app/v1.0.0) to distinguish different packages