Git Tags and Releases Tutorial
Learn lightweight vs annotated tags, signed tags, semantic versioning, and how to ship clean releases on GitHub. Practical workflow from tagging to changelog to publishing.
What you'll learn
- ✓The difference between lightweight and annotated tags
- ✓How to sign tags with GPG or SSH for trust
- ✓Semantic versioning in practice
- ✓Linking tags to GitHub Releases and changelogs
- ✓How to move or delete a tag without breaking consumers
Prerequisites
- •Basic Git usage and a remote like GitHub
What and Why
A tag in Git is a named pointer to a specific commit. Branches move as you commit; tags stay put. That makes tags the natural way to mark “this is the exact code that shipped as v1.4.0”. On top of tags, hosts like GitHub layer Releases: a tag plus a title, notes, and downloadable assets.
You tag releases for three reasons: customers and CI systems can fetch a stable version, you can compare what changed between versions, and you can roll back without guesswork.
Mental Model
There are two kinds of tags:
- Lightweight: a file under
.git/refs/tags/containing a commit SHA. No metadata, no signature, no message. Like a sticky note. - Annotated: a full Git object stored in the object database with a tagger name, date, message, and optionally a GPG/SSH signature. It points to a commit.
Annotated tags are what you almost always want for releases: they are traceable, signable, and survive git describe cleanly.
Semantic versioning (MAJOR.MINOR.PATCH) gives the tag a meaning:
- MAJOR: breaking changes
- MINOR: backward-compatible features
- PATCH: backward-compatible fixes
Pre-releases use suffixes like -rc.1 or -beta.2. Build metadata uses +sha.abcdef0.
Hands-on Example
Create and push an annotated tag for a release:
git checkout main
git pull
git tag -a v1.4.0 -m "v1.4.0: add CSV export and fix login redirect"
git push origin v1.4.0
List and inspect tags:
git tag --list 'v1.*'
git show v1.4.0 # tag metadata + commit + diff
Sign a tag so consumers can verify it:
git config --global user.signingkey <KEYID>
git tag -s v1.4.1 -m "v1.4.1: patch null pointer in export"
git tag -v v1.4.1 # verify signature
Generate release notes from commits since the previous tag:
git log v1.3.0..v1.4.0 --pretty=format:'- %s (%h)' --no-merges
Create a GitHub Release from the tag using the GitHub CLI:
gh release create v1.4.0 \
--title "v1.4.0" \
--notes-file CHANGELOG-v1.4.0.md \
dist/app-v1.4.0.tar.gz
main: C1 -- C2 -- C3 -- C4 -- C5
| |
v1.3.0 v1.4.0 (annotated, signed)
git push origin v1.4.0
|
v
GitHub stores tag object
|
v
gh release create v1.4.0
|
v
Release page:
title, notes, assets (tar.gz, checksums, sigs)
download link pinned to commit C5 Common Pitfalls
- Moving tags after publish: once consumers fetch
v1.4.0, do not retag the same name on a different commit. It silently breaks reproducible builds. Cutv1.4.1instead. - Forgetting to push tags:
git pushdoes not push tags by default. Usegit push origin <tag>orgit push --tagsfor all of them. - Lightweight tags for releases: they lose history when re-cloned shallowly and cannot be signed. Use annotated tags.
- Tagging the wrong commit: always check
git log -1on the commit you intend to tag, especially after merges. - Inconsistent prefix: mixing
v1.0.0and1.0.0confuses tooling. Pick one and stick with it.
Practical Tips
- Automate tagging in CI when a
release/*branch merges, so humans do not forget to push tags. - Pair every tag with a CHANGELOG entry; “Keep a Changelog” is a solid format.
- Use
git describe --tagsto derive build versions likev1.4.0-3-gabc1234. - Delete a local tag with
git tag -d v1.4.0and a remote tag withgit push origin :refs/tags/v1.4.0only if it has never been consumed. - For monorepos, prefix tags by component:
api/v1.4.0,web/v2.0.1.
Wrap-up
Tags are cheap, immutable, and incredibly useful. Annotate them, sign them when trust matters, follow semver, and pair them with a Release page that links to notes and artifacts. Future-you, debugging an outage at 2am, will thank past-you for tagging clearly and never moving a published tag.
Related articles
- Git Git Bisect: Find the Commit That Introduced a Bug
Learn how to use git bisect to perform binary search through your commit history and pinpoint the exact commit that introduced a bug in your codebase.
- Git Git Hooks for CI/CD and Automation Pipelines
Build custom Git hooks that integrate with CI/CD pipelines: server-side hooks, push-based triggers, branch protection enforcement, and automated deployment gates.
- Git Git Monorepo Management at Scale
Manage large Git monorepos with sparse checkout, partial clone, subtrees, CODEOWNERS, and scaling strategies that keep performance acceptable as the repo grows.
- Git Git Rebasing Strategies for Clean Team Histories
Advanced git rebase strategies for teams: interactive rebase workflows, rebase --onto, autosquash, and policies that keep shared branches clean without rewriting public history.