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

Sponsored

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

  1. Commit your work — tags only point to commits, so make sure you're where you want to be.
  2. Create the tag — choose git tag <tagname> for lightweight, or git tag -a <tagname> -m "message" for annotated.
  3. Verify — run git tag to list, or git show to inspect.
  4. Pushgit push origin <tagname> (or --tags to push all at once).
  5. 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 push doesn't send tags by default; you need git push origin <tagname> or git push --tags.
  • Using git tag -f on 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 tag on a specific commit hash.

Variations

  1. Use semantic versioning tags like v1.2.3 for public releases, and lightweight tags like weekly-snapshot for internal milestones.
  2. Automate tag creation in your CI/CD pipeline — after a successful build, run git tag -a v${VERSION} -m "Automated release" and push automatically.
  3. 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.0 so the ops team can roll back to an exact commit if the deploy fails.
  • Create a lightweight tag ci-build-1234 before 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 --tags or 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.