Git Tag Releases
Learn how to tag releases with lightweight and annotated tags in Git — hands-on steps, troubleshooting, and what to study next.
Focus: tag releases with lightweight and annotated tags
Every team has that moment: you deploy a hotfix, and suddenly nobody knows which commit is running in production. You scroll through git log, squint at timestamps, and pray. The fix is Git tags — named, permanent bookmarks on specific commits. In this lesson, you'll master the two kinds of tags — lightweight and annotated — and learn exactly when to use each to tag releases cleanly and confidently.
The problem this lesson solves
Without tags, your release history is a blur. You can't reliably say "version 1.4.2 is this commit" because commits don't carry version numbers. You end up with sticky notes like "the good one" or "deploy this", which break down the moment your teammate pushes to production. Tags solve this by giving a specific commit a permanent, human-readable name — "v1.4.2" — that travels with your repository and survives pushes, pulls, and time.
More importantly, using the wrong kind of tag creates a second problem. Lightweight tags are just pointers — they carry zero metadata. Annotated tags are full Git objects — they include the author, date, and a message, and they're cryptographically signed if you choose. Picking the wrong one means losing critical release information that your team (or future you) will desperately need during an audit or a rollback.
Core concept / mental model
Think of a lightweight tag as a sticky note you slap on a commit — it says "v1.4.2" and nothing else. It's cheap, fast, and perfect for a personal bookmark. An annotated tag is more like a signed, timestamped certificate — it contains the tagger's name, email, date, and a release message, and it can be verified with a GPG signature. It's the professional choice for anything your team will reference.
Both tags are immutable — they always point to the same commit forever (unless you force-delete and recreate them, which you should avoid on shared branches). They're stored in .git/refs/tags/ and are pushed with git push --tags. The commit itself is never changed; the tag just adds a label.
[!TIP] Mental shortcut: Lightweight = a bookmark. Annotated = a signed chapter entry in a release journal.
How it works step by step
Understanding the anatomy of a tag
- Lightweight tag: A simple reference (
refs/tags/v1.4.2) pointing directly to a commit. No extra data — only the commit hash. - Annotated tag: A tag object that contains:
- The commit hash it points to (called the tagged object)
- Tagger name and email
- Date and time of tagging
- A tag message (like "Release v1.4.2 — new dashboard")
- Optionally, a GPG signature
The tagging lifecycle
- Commit your work — tags only point to commits, so make sure you're where you want to be.
- Create the tag — choose
git tag <tagname>for lightweight, orgit tag -a <tagname> -m "message"for annotated. - Verify — run
git tagto list, orgit showto inspect. - Push —
git push origin <tagname>(or--tagsto push all at once). - Use — checkout, deploy, or tag an old commit for a hotfix.
Lightweight vs. annotated — under the hood
# Lightweight tag — creates a ref, no object
$ git tag v1.4.2
$ git cat-file -t refs/tags/v1.4.2
commit # it points directly to a commit
# Annotated tag — creates a tag object
$ git tag -a v1.4.3 -m "Release v1.4.3"
$ git cat-file -t refs/tags/v1.4.3
tag # it's a tag object with metadata
The distinction matters when someone runs git show — for annotated tags you'll see the tagger info and message; for lightweight, only the commit diff appears.
Hands-on walkthrough
Let's put this into practice with a real scenario. You'll create a repository, make a few commits, tag a release both ways, and push.
Step 1: Set up a throwaway repo
mkdir tag-demo
cd tag-demo
git init
git config user.name "Dev Learner"
git config user.email "dev@example.com"
# Create a simple file and commit
echo "v1 features" > app.py
git add app.py
git commit -m "Initial commit"
echo "v2 features" >> app.py
git commit -am "Add v2 features"
Step 2: Create tags at different points
# Lightweight tag on the latest commit
git tag v1.0.0
# Annotated tag on the previous commit (use its hash from git log)
PREV_HASH=$(git rev-parse HEAD~1)
git tag -a v1.0.0-beta -m "Beta release" $PREV_HASH
Step 3: Inspect the difference
$ git tag
v1.0.0
v1.0.0-beta
$ git show v1.0.0 # short — just commit diff
$ git show v1.0.0-beta # includes tagger name, email, date, message
Step 4: Push tags to remote
# Push a single tag
git push origin v1.0.0
# Or push all tags at once
git push origin --tags
Expected output from git show v1.0.0-beta will look like:
tag v1.0.0-beta
Tagger: Dev Learner <dev@example.com>
Date: Thu Dec 05 10:00:00 2024 -0500
Beta release
Bonus: Tag a commit that's far in the past
git log --oneline
# find the hash you want
git tag -a v0.9 -m "Final pre-release" 3f2a1b4
This is perfect for hotfixes or post-hoc release documentation.
Compare options / when to choose what
| Tag type | Pros | Cons | Best for |
|---|---|---|---|
| Lightweight | Fast, no overhead, no prompts | No metadata, no signer, no message | Personal bookmarks, temporary snapshots, internal CI tags |
| Annotated | Full metadata, signable, self-documenting | Slightly more steps, requires a message | Official releases, team-facing milestones, audit trails |
Rule of thumb: If you're tagging a release your team will reference or deploy, always use annotated. If you're just marking a spot for yourself, lightweight is fine.
Troubleshooting & edge cases
"Fatal: tag already exists"
You tried to create a tag that already exists. Check with git tag -l. If you need to move it (on a branch you own), force-update:
git tag -f v1.0.0 new-commit-hash
But never force-push a tag on shared repositories — it breaks everyone's references.
"Tag not found when pushing"
You may have created the tag but forgot to push it. Run git push origin <tagname> — tags don't push automatically with git push. Use --tags to push all.
"I want to delete a tag"
Locally:
git tag -d v1.0.0-beta
On remote (if you pushed it):
git push origin :refs/tags/v1.0.0-beta
"Annotated tag shows no message"
You created it with git tag -a but forgot -m — Git opens your editor. If you skip -a, you get a lightweight tag. Always use -m "release note" to avoid editor surprises.
"Why does git show look different on CI?"
CI systems may only fetch commits, not tags, unless you fetch tags explicitly. Add --tags to your fetch: git fetch --tags.
What you learned & what's next
You now understand the core difference between lightweight and annotated tags. You can:
- Explain what a tag is and why it's critical for release management
- Create both tag types from the command line
- Push, list, and delete tags safely
- Choose the right tag type for each scenario
Your next step is to master release branches and versioning workflows — how to combine tags with git checkout and deployment scripts. That will make your release pipeline bulletproof.
[!TIP] Pro tip: Always push annotated tags after merging a release branch — it gives you a permanent, documented history of every production deploy.
Now go tag your next release with confidence. Your future self (and your team) will thank you.
Practice recap
Open any Git repo (or create a new one) and tag your last three commits: one lightweight, one annotated, and one annotated on an older commit. Then push them to a remote and run git show on each to observe the difference. Finally, delete one tag locally and remotely to practice cleanup.
Common mistakes
- Using a lightweight tag for a release that your team will reference — no metadata means no author, date, or message, so you lose the audit trail.
- Forgetting to push tags —
git pushdoesn't send tags by default; you needgit push origin <tagname>orgit push --tags. - Using
git tag -fon a shared repository to move a tag — this overwrites history and breaks clones for everyone else. Only force-update on private branches. - Creating an annotated tag without
-m— Git opens an editor and you may end up with an empty message or abort the process unintentionally. - Trying to tag a working tree state instead of a commit — tags only point to commits; you must commit first, or use
git tagon a specific commit hash.
Variations
- Use semantic versioning tags like
v1.2.3for public releases, and lightweight tags likeweekly-snapshotfor internal milestones. - Automate tag creation in your CI/CD pipeline — after a successful build, run
git tag -a v${VERSION} -m "Automated release"and push automatically. - Enable GPG signing on annotated tags (
git config tag.gpgSign true) to cryptographically verify release authenticity in security-sensitive projects.
Real-world use cases
- Tagging a production release as
v2.3.0so the ops team can roll back to an exact commit if the deploy fails. - Create a lightweight tag
ci-build-1234before each CI job to trace which commit triggered a specific build artifact. - Use annotated tags with release notes for every major version in an open-source library, so users see the tagger and date in
git describe.
Key takeaways
- Tags are immutable pointers to commits — they make releases reproducible and shareable.
- Lightweight tags are simple refs with no metadata; annotated tags store tagger, date, message, and can be signed.
- Always push tags explicitly with
git push --tagsor the tag name, or they stay local only. - Use annotated tags for anything your team will reference; save lightweight tags for personal or temporary marks.
- Deleting or force-updating tags on shared repos is risky and disrupts collaboration.
- Tag both new commits and historical ones — you can tag any commit by hash anytime.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.