Git Tagging & Releases: Complete Versioning Guide

Git Tagging & Releases: Complete Versioning Guide
A tag in git is a named pointer to a specific commit. Unlike branches, tags do not move — they permanently mark a point in history. Combined with Semantic Versioning and GitHub Releases, tags become the foundation of a professional software delivery workflow.
This guide covers lightweight vs annotated tags, Semantic Versioning rules, how to automate versioning with Conventional Commits, creating GitHub Releases with binary assets, and a complete GitHub Actions release pipeline.
Lightweight vs Annotated Tags
Lightweight Tags
A lightweight tag is just a name for a commit — a file in .git/refs/tags/ that contains a SHA.
git tag v1.0-beta
git tag v1.0-beta abc1234 # Tag a specific commit
git push origin v1.0-beta # Push to GitHubLightweight tags have no author, no date, no message. Use them for temporary local markers or quick personal bookmarks.
Annotated Tags (Use for Releases)
An annotated tag is a full git object stored in the object database, containing:
- Tagger name and email
- Tagging date and time
- Tag message
- Optional GPG signature
# Create an annotated tag
git tag -a v1.2.0 -m "Release v1.2.0: Add user authentication and dashboard"
# Tag a specific commit (for retroactive tagging)
git tag -a v1.1.0 abc1234 -m "Release v1.1.0"
# Sign the tag with GPG (for high-security releases)
git tag -s v1.2.0 -m "Release v1.2.0"Inspect an annotated tag:
git show v1.2.0tag v1.2.0
Tagger: Alice Smith <alice@example.com>
Date: Mon Apr 18 10:30:00 2026 +0000
Release v1.2.0: Add user authentication and dashboard
commit abc1234def5678...
Author: Alice Smith <alice@example.com>
Date: Mon Apr 18 10:00:00 2026 +0000
feat: complete dashboard implementationAlways use annotated tags for releases — they provide a permanent record of who released what and when.
Semantic Versioning (SemVer)
Semantic Versioning (semver.org) defines a standard format: MAJOR.MINOR.PATCH.
The Three Numbers
MAJOR — Breaking changes. Existing code that uses the previous version may need to be updated.
v1.5.3 → v2.0.0Examples of breaking changes:
- Renamed or removed a public function
- Changed a function's return type or parameter types
- Removed a REST endpoint
- Changed a database schema in a way that requires migration
MINOR — New functionality, backwards compatible. Users can upgrade without changing their code.
v1.5.3 → v1.6.0Examples of minor additions:
- Added a new API endpoint
- Added an optional parameter to an existing function
- Added a new UI page that doesn't change existing flows
PATCH — Bug fixes, backwards compatible. No new features.
v1.5.3 → v1.5.4Examples of patches:
- Fixed a crash on null input
- Corrected a wrong calculation
- Fixed a typo in error messages
Pre-release and Build Metadata
v1.0.0-alpha # Alpha: unstable, API may change
v1.0.0-alpha.1 # Alpha iteration 1
v1.0.0-beta # Beta: feature-complete, testing
v1.0.0-beta.2 # Beta iteration 2
v1.0.0-rc.1 # Release candidate: production-ready candidate
v1.0.0 # Stable release
v1.0.0+build.123 # Build metadata (ignored for version precedence)Version Precedence
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0Pre-release versions have lower precedence than the associated normal version.
Git Tag Commands Reference
# List all tags
git tag
# List tags matching a pattern
git tag -l "v1.2.*"
# Create annotated tag at HEAD
git tag -a v1.2.0 -m "Release v1.2.0"
# Create annotated tag at a specific commit
git tag -a v1.1.0 abc1234 -m "Retroactive tag"
# Push a single tag
git push origin v1.2.0
# Push all tags
git push origin --tags
# Delete a local tag
git tag -d v1.2.0-beta
# Delete a remote tag (two-step)
git tag -d v1.2.0-beta
git push origin --delete v1.2.0-beta
# Verify a signed tag
git tag -v v1.2.0
# List tags with their commit messages
git tag -n
# Find which tag contains a commit
git describe --tags abc1234
# Check out the state of the repo at a tag (detached HEAD)
git checkout v1.1.0Conventional Commits: Automated Versioning
Conventional Commits is a standard for writing structured commit messages that automated tools can parse to determine the next version number.
Format
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Types that affect versioning:
| Type | SemVer impact | Example |
|---|---|---|
feat | MINOR bump | feat: add user authentication |
fix | PATCH bump | fix: correct tax calculation for EU |
feat! or BREAKING CHANGE: | MAJOR bump | feat!: redesign API response format |
docs, style, refactor, test, chore | No release | docs: update README |
Example Commit History
feat: add stripe payment integration
fix: handle null user in profile endpoint
docs: add API documentation
fix: correct order total rounding
feat: add dark mode to dashboard
feat!: rename /api/v1/user to /api/v2/usersAn automated tool reads these commits since the last tag and calculates:
- 1 breaking change → MAJOR bump
- 2 features → (already covered by MAJOR)
- 2 fixes → (already covered by MAJOR)
- Result:
v1.0.0→v2.0.0
Automated Release Pipeline with GitHub Actions
Using semantic-release
# .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 needed for semantic-release
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- run: npm test
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} # If publishing to npm
run: npx semantic-release.releaserc.json configuration:
{
"branches": ["main"],
"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]"
}]
]
}This workflow:
- Analyses all commits since the last tag
- Determines the next version number
- Generates CHANGELOG.md
- Creates the git tag
- Creates a GitHub Release with auto-generated notes
- Publishes to npm (if configured)
Manual Release Workflow
For teams that prefer manual control:
# .github/workflows/manual-release.yml
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g. v1.2.0)'
required: true
prerelease:
description: 'Is this a pre-release?'
type: boolean
default: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate version format
run: |
if ! echo "${{ inputs.version }}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then
echo "ERROR: Version must be in format v1.2.3"
exit 1
fi
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
# Build release artifacts
- name: Package release
run: |
tar -czf app-${{ inputs.version }}-linux-amd64.tar.gz -C dist .
zip -r app-${{ inputs.version }}-windows-amd64.zip dist/
# Create annotated tag
- name: Create tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a ${{ inputs.version }} -m "Release ${{ inputs.version }}"
git push origin ${{ inputs.version }}
# Create GitHub Release with artifacts
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ inputs.version }}
name: Release ${{ inputs.version }}
prerelease: ${{ inputs.prerelease }}
generate_release_notes: true # Auto-generate from PRs merged since last tag
files: |
app-${{ inputs.version }}-linux-amd64.tar.gz
app-${{ inputs.version }}-windows-amd64.zipGitHub Releases
A GitHub Release is a deployable version of your software built on top of a tag. It adds:
- A human-readable title and description
- Auto-generated release notes (list of PRs merged since last release)
- Binary assets (executables, archives, checksums)
- Pre-release flag for beta/RC versions
Creating a Release Manually
- Go to your repository → Releases → Draft a new release
- Choose a tag (existing or create a new one)
- Set a title (e.g., "v1.2.0 — User Authentication")
- Click Generate release notes — GitHub automatically lists all merged PRs
- Edit the notes if needed
- Upload binary assets
- Check "Pre-release" for alpha/beta/RC versions
- Click Publish release
Auto-Generated Release Notes
GitHub generates release notes from PR titles and labels. Improve them by:
- Adding a
.github/release.ymlconfiguration:
# .github/release.yml
changelog:
exclude:
labels:
- ignore-for-release
categories:
- title: "🚀 New Features"
labels:
- enhancement
- feature
- title: "🛠Bug Fixes"
labels:
- bug
- fix
- title: "🔠Security"
labels:
- security
- title: "📠Documentation"
labels:
- documentation
- title: "â¬†ï¸ Dependencies"
labels:
- dependencies- Use consistent PR labels that match the configuration
- Write clear, user-facing PR titles (not internal jargon)
Checksums and Verification
For publicly distributed releases, always provide checksums so users can verify downloads:
# Generate SHA256 checksums
sha256sum app-v1.2.0-linux-amd64.tar.gz > checksums.txt
sha256sum app-v1.2.0-windows-amd64.zip >> checksums.txt
# Users verify with:
sha256sum -c checksums.txtIn GitHub Actions:
- name: Generate checksums
run: |
sha256sum *.tar.gz *.zip > checksums.txt
cat checksums.txt
- uses: softprops/action-gh-release@v2
with:
files: |
*.tar.gz
*.zip
checksums.txtFrequently Asked Questions
Q: Can I move a tag after creating it?
Technically yes, but it is strongly discouraged. Tags are meant to be immutable records. If you find a bug in v1.0.0 after releasing, the right action is to fix it and release v1.0.1 — not to move the v1.0.0 tag to the fixed commit. Moving tags breaks everyone who has already downloaded or referenced the old version.
Q: What is the difference between git push --tags and git push origin v1.2.0?
git push --tags pushes all local tags that don't exist on the remote — including old, temporary, or test tags you never intended to publish. git push origin v1.2.0 pushes only the specified tag. Prefer explicit tag pushes in automated workflows to avoid accidentally publishing test tags.
Q: How do I find the most recent tag on the current branch?
git describe --tags --abbrev=0 # Most recent tag reachable from HEAD
git describe --tags # Most recent tag + commits since it (e.g., v1.2.0-3-gabc1234)git describe is commonly used in build scripts to embed the version into the compiled binary.
Q: Should library versions and application versions follow the same rules?
Libraries must follow SemVer strictly because consumers depend on the version number to understand compatibility. Applications are more flexible — many teams use CalVer (calendar versioning like 2026.04.1) or simple incrementing build numbers for internal applications where API compatibility is less of a concern.
Key Takeaway
Git tags are the bridge between a codebase and a software release. Annotated tags provide a permanent, signed record of who released what version and when. Semantic Versioning gives your users a contract: patch versions are safe to upgrade, minor versions add features, major versions may break things. Automating the release process with Conventional Commits and semantic-release eliminates manual version bumping mistakes and keeps changelogs accurate. The result is a professional delivery pipeline where every deployed version is fully traceable to the exact code that produced it.
Read next: Git Submodules & External Libraries →
Part of the GitHub Mastery Course — masters of the release.
