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 Tutorial: Find the Bad Commit Fast
Use git bisect to binary-search through history and pinpoint the commit that introduced a regression, with manual and automated examples.
- Git Git Cherry-pick and Revert Tutorial
Learn how to copy specific commits across branches with cherry-pick and how to safely undo merged changes with revert, including conflict handling and recovery.
- Git Git Hooks and pre-commit Tutorial
Automate checks before commits, pushes, and merges with native git hooks and the pre-commit framework. Keep your repo clean without slowing down.
- Git Git Large File Storage (LFS) Tutorial
Set up Git LFS to version large binaries like images, models, and datasets without bloating your repository, including tracking, migration, and CI tips.