Tagging Releases in Git Is Not Optional in a Real Project
by Eric Hanson, Backend Developer at Clean Systems Consulting
The Incident That Tags Prevent
Production is down. You need to know what version is running. You ask the team and get "the latest main" — but main has had four commits since the last deployment. You check the deployment logs and find a timestamp, but not a commit SHA. You check your CI/CD system and find the build number, but can't easily trace it back to the Git commit.
Twenty minutes into the incident, you're still establishing the baseline of "what code is actually running" — which is the information you need before you can meaningfully debug anything.
If you had tagged your releases, this question would be answered in five seconds: look at what tag is deployed, git show v2.3.1 to see the commit and its context, git log v2.3.2..v2.3.4 to see what changed since the last known-good version.
What a Tag Is
A tag in Git is a named reference to a specific commit. Unlike branches, tags don't move — they point to one commit permanently. They're meant to mark specific moments: releases, deployment points, audit checkpoints.
Two types:
Lightweight tags — just a pointer, like a branch that never moves.
git tag v2.3.1
git tag v2.3.1 a3f9d24 # tag a specific commit
Annotated tags — a full Git object with a tagger, date, message, and optionally a GPG signature. This is what you want for releases.
git tag -a v2.3.1 -m "Release v2.3.1: payment retry and fix for duplicate charges"
Annotated tags show up in git describe output, are stored as objects in the object store, and include metadata that lightweight tags don't have. For any tag that marks a release, use annotated tags.
The Tagging Convention
Semantic Versioning (semver.org) is the standard: MAJOR.MINOR.PATCH.
PATCH— bug fixes, no API changes (v2.3.1 → v2.3.2)MINOR— new features, backward compatible (v2.3.1 → v2.4.0)MAJOR— breaking changes (v2.3.1 → v3.0.0)
The v prefix is conventional for application releases (v2.3.1). Library releases sometimes omit it (2.3.1) because package managers use the version number directly.
For pre-releases: v2.4.0-rc.1, v2.4.0-beta.2, v2.4.0-alpha.1. These sort correctly and signal stability level to anyone looking at the tag list.
Pushing Tags to the Remote
Tags are not pushed automatically with git push. You have to push them explicitly:
# Push a specific tag
git push origin v2.3.1
# Push all local tags
git push origin --tags
# Push all tags in a single command (if configured)
git push --follow-tags # only pushes annotated tags
Configure --follow-tags as the default to avoid forgetting:
git config --global push.followTags true
With this setting, git push automatically includes any annotated tags reachable from the pushed commits.
Tags as Deployment Triggers
In a CI/CD pipeline, a tag push is the cleanest trigger for a production deployment:
# GitHub Actions: deploy to production on version tag
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying ${GITHUB_REF_NAME} to production"
./deploy.sh production ${GITHUB_REF_NAME}
GITHUB_REF_NAME contains the tag name (e.g., v2.3.1). This gives your deployment system the version number automatically — no manual version bumping, no configuration, no ambiguity about what's being deployed.
Generating Changelogs From Tags
With proper tagging, generating a changelog between releases is trivial:
# Commits between two tags
git log v2.3.0..v2.3.1 --oneline
# Full diff between two releases
git diff v2.3.0..v2.3.1
# All tags, with dates
git tag -l --sort=-version:refname \
--format='%(refname:short) %(taggerdate:short)'
With Conventional Commits, tools like git-cliff, conventional-changelog, or release-please can generate formatted changelogs automatically from the commit messages between tags. The convention investment pays off in automated release notes.
Hotfix Tagging
When you cherry pick a hotfix to an older release:
# You're on release/2.3 branch, which tracks the v2.3.x line
git checkout release/2.3
git cherry-pick <hotfix-sha>
git tag -a v2.3.2 -m "Hotfix: fix critical payment processing error"
git push origin release/2.3 --follow-tags
The tag clearly marks what the hotfix release contains and when it was made. Anyone querying "what's the difference between v2.3.1 and v2.3.2?" gets a precise answer.
Finding What's Deployed With git describe
If your build process embeds the Git description in the artifact:
git describe --tags
# v2.3.1-4-ga3f9d24
# v2.3.1: last tag
# 4: commits since that tag
# g: "g" prefix for git
# a3f9d24: SHA of current commit
This string uniquely identifies any built artifact relative to the tag history. A build from exactly v2.3.1 shows v2.3.1. A build from four commits after that shows v2.3.1-4-ga3f9d24. Embed this in your service's /health or /version endpoint and you can always answer "what exactly is running" without consulting deployment logs.
Tags are two minutes of work per release. The cost of not having them is paid in every incident that starts with "what's actually deployed right now?"