Last modified: September 13, 2025

This article is written in: 🇺🇸

Git Tags

Tags mark exact commits. They’re perfect for releases, rollbacks, changelogs, and CI/CD triggers. Unlike branches, tags don’t move—ever—so you can always point to the exact build you shipped.

Think of a branch as a bookmark you keep moving, and a tag as a sticky note glued to a page.

A -- B -- C -- D -- E  (main)
          ^ 
          |
        v1.2.0  ← frozen pointer to commit C

Mental model

Creating Tags

Tags are markers you can place on commits — often used for releases, milestones, or just to remember an important point in your project’s history. Git supports three main flavors: lightweight, annotated, and signed.

1. Lightweight Tag (quick and simple)

A lightweight tag is basically a named pointer to a commit. It doesn’t store extra info (no tagger, no message).

Tag the latest commit (HEAD):

git tag v2.0.0

Tag a specific commit (using its short hash):

git tag v2.0.0 b4d373a

Check what the tag points to:

git show --no-patch v2.0.0

Example output:

tag v2.0.0
Tagger: (none - lightweight tag)
commit b4d373k8990g2...
Author: Jane Dev <jane@example.com>
Date:   Mon Sep 1 12:34:56 2025 +0000

    Merge feature: new billing flows

Notice how there’s no tagger or message section — Git just jumps straight to the commit info. Lightweight tags are fine for personal bookmarks, but not recommended for formal releases.

2. Annotated Tag (the usual choice for releases)

An annotated tag creates a real tag object that stores metadata: who created it, when, and an optional message.

At HEAD:

git tag -a v2.0.0 -m "v2.0.0: usage-based billing, new invoices"

At a specific commit:

git tag -a v2.0.0 -m "v2.0.0: ..." b4d373k

Inspect it:

git show v2.0.0

Sample output:

tag v2.0.0
Tagger: Jane Dev <jane@example.com>
Date:   Mon Sep 1 12:45:00 2025 +0000

v2.0.0: usage-based billing, new invoices

commit b4d373k8990g2...
Author: Jane Dev <jane@example.com>
Date:   Mon Sep 1 12:34:56 2025 +0000

    Merge feature: new billing flows

Here you see two parts:

This extra metadata is super handy for tracking releases and automation.

3. Signed Tag (for authenticity and trust)

If you have GPG or SSH signing set up, you can cryptographically sign tags. This proves the tag really came from you.

git tag -s v2.0.0 -m "Release v2.0.0"
git tag -v v2.0.0   # verify the signature

Signed tags are especially useful in open-source or enterprise projects where you need strong guarantees about who made a release.

👉 One last note: tags don’t get pushed automatically. To share them, run:

git push origin v2.0.0      # push a single tag
git push origin --tags      # push all tags

Seeing and Finding Tags

Tags pile up quickly in a project, so Git gives you ways to list, filter, and sort them.

List all tags

git tag

This just prints every tag name in your repo:

v1.0.0
v1.1.0
v2.0.0
v2.1.0-rc.1

Filter tags by pattern

git tag -l "v2.*"

Only tags starting with v2. show up:

v2.0.0
v2.1.0-rc.1

You can also match other patterns, like release candidates:

git tag -l "*-rc.*"

Show messages next to tags

Annotated tags can have messages, and -n lets you preview them:

git tag -n

Output:

v1.0.0   First stable release
v2.0.0   Usage-based billing, new invoices

You can also limit or filter:

git tag -n9 -l "v2.*"

This shows the first 9 lines of each tag message, but only for tags starting with v2.. Great when you want quick release notes.

Sort tags semantically

Normal sorting treats v1.10.0 as before v1.9.9 (because text sorting sees 1 vs 9). Version-aware sort fixes that:

git tag --sort=-v:refname

Now the latest version appears first:

v2.1.0-rc.1
v2.0.0
v1.10.0
v1.9.9

Find the nearest tag to the current commit

git describe --tags

Example output:

v2.0.0-5-gdeadbeef

That means:

This is super useful in CI/CD pipelines for naming builds like 2.0.0+5.

Using Tags in Real Workflows

Tags are the backbone of releases, hotfixes, and rollbacks. Here are the most common ways you’ll use them in practice.

Cutting a Release + Triggering CI

The usual flow:

  1. Merge your release PR into main.
  2. Create a tag at that commit.
  3. Push the tag — CI/CD kicks off.

Example:

A -- B -- C -- D -- E -- F  (main)
                ^           ^
               v1.1.0      v1.2.0   ← each tag starts a release build

Commands:

git tag -a v1.2.0 -m "v1.2.0: new billing"
git push origin v1.2.0

What happens next: most CI platforms (GitHub Actions, GitLab CI, CircleCI, etc.) can listen for “tag push” events. A tag like v1.2.0 often triggers a pipeline that builds artifacts, signs them, and publishes a release.

Hotfix from a Release Tag

Say a bug slipped into production. You want to patch exactly what was shipped.

Instead of checking out the tag directly (which gives you a detached HEAD), make a branch starting from it:

git switch -c hotfix/invoice-rounding v1.2.0

Now you can fix, commit, and PR back into main or your release branch.

Interpretation: you’re working from the exact code you shipped last time — no surprise commits from main sneaking in.

Rolling Back to a Known-Good Version

Sometimes the latest release misbehaves. Tags let you roll back safely.

See what commit a tag points to:

git show --no-patch v1.2.0

Deploy tooling (Heroku, Kubernetes, GitHub Actions, etc.) often accepts a tag name directly:

deploy --ref v1.2.0

That redeploys the exact same bytes as last time. No guessing.

Generating a Changelog Between Releases

Need release notes or just want to see what changed? Compare two tags:

git log --oneline v1.1.0..v1.2.0

Shows commit subjects:

abc123  Fix invoice rounding error
def456  Add usage-based billing

Or see file-level stats:

git diff --stat v1.1.0..v1.2.0

Output:

billing.js    | 20 +++++++++++---------
invoice.test  | 12 ++++++++--
2 files changed, 22 insertions(+), 10 deletions(-)

Great for writing changelogs.

Checking Out a Tag (and Avoiding Detached HEAD Confusion)

To browse code at a tag:

git switch --detach v1.2.0
# or: git checkout v1.2.0

Now you’re in a detached HEAD state — safe for read-only browsing, not great for new commits.

ASCII reminder:

(main) ──●──●──●──●
              ↑
            v1.2.0  (HEAD is detached here)

If you do want to commit, make a branch first:

git switch -c fix-from-v1.2.0 v1.2.0

That way your work doesn’t get “lost in limbo.”

Moving, renaming, and deleting tags

Move a tag to a different commit (local)

git tag -f v2.0.0 NEWCOMMIT

If it’s already on the remote, you must replace it there too:

git push origin -f v2.0.0   # force updates tag ref on remote

⚠️ Caution:

Rename a tag (no direct rename; recreate)

git tag new-name old-name
git tag -d old-name
git push origin new-name
git push origin :refs/tags/old-name   # or: git push origin --delete old-name

Delete tags

Local:

git tag -d v2.0.0

Remote:

git push origin --delete v2.0.0
# or:
git push origin :refs/tags/v2.0.0

Interpretation:

Power viewing

See where tags sit in history:

git log --graph --decorate --oneline --all

You’ll see tags in-line:

* 3fa1c2d (tag: v1.2.0, origin/main, main) Merge ...
* 0b7a3ff Add invoices API
* 9e0a1a1 (tag: v1.1.0) Release v1.1.0

Find tags with notes:

git tag -n | grep "billing"

Show only tags on the current branch:

git tag --merged

Practical Naming for Tags

Pick a naming style that both humans and automation tools will love.

Why bother with all this? Because automation thrives on predictable names — and so do humans reading the history months later.

Common “Why Isn’t This Working?” Pitfalls

“I created the tag but CI didn’t run.”

You probably forgot to push the tag. Remember:

git push origin v2.0.0

Also double-check your CI config — many pipelines only listen for certain patterns (v*, release-*, etc.).

“I pushed but my teammate still can’t see it.”

They need to fetch tags explicitly:

git fetch --tags

By default, git fetch doesn’t grab new tags unless they’re tied to commits being fetched.

git show doesn’t display my tag message.”

That means you made a lightweight tag. Those don’t have messages. Use an annotated tag next time:

git tag -a v2.0.0 -m "Release v2.0.0"

“I checked out a tag and my commits disappeared.”

That’s the classic detached HEAD trap. Tags don’t move, so commits made in this state get “orphaned.” The fix: create a branch at the tag before committing:

git switch -c fix-from-v2.0.0 v2.0.0

“We moved a release tag and broke everything.”

Never rewrite or move public tags. Once something like v2.0.0 is out in the world, it’s immutable. If you need to patch, bump the version (v2.0.1) instead.

Real-world mini recipes (copy/paste)

Create an annotated release tag at HEAD and push:

git tag -a v2.3.0 -m "v2.3.0: SSO + revamped audit logs"
git push origin v2.3.0

Push only release tags automatically when pushing commits:

git push --follow-tags

Generate a changelog between releases:

git log v2.2.0..v2.3.0 --pretty=format:"- %s (%h)"

Diff code between releases:

git diff --stat v2.2.0..v2.3.0

Find latest version tag (nearest reachable):

git describe --tags --abbrev=0

Clean up a mistaken remote tag:

git tag -d v2.3.0
git push origin --delete v2.3.0

Retag correctly (same name, new commit), and push forcefully:

git tag -a -f v2.3.0 -m "v2.3.0: hotfix included" NEWCOMMIT
git push -f origin v2.3.0

(Again: avoid for public releases; prefer v2.3.1.)

Why teams use tags in practice?

Table of Contents

    Git Tags
    1. Mental model
    2. Creating Tags
      1. 1. Lightweight Tag (quick and simple)
      2. 2. Annotated Tag (the usual choice for releases)
      3. 3. Signed Tag (for authenticity and trust)
    3. Seeing and Finding Tags
      1. List all tags
      2. Filter tags by pattern
      3. Show messages next to tags
      4. Sort tags semantically
      5. Find the nearest tag to the current commit
    4. Using Tags in Real Workflows
      1. Cutting a Release + Triggering CI
      2. Hotfix from a Release Tag
      3. Rolling Back to a Known-Good Version
      4. Generating a Changelog Between Releases
      5. Checking Out a Tag (and Avoiding Detached HEAD Confusion)
    5. Moving, renaming, and deleting tags
      1. Move a tag to a different commit (local)
      2. Rename a tag (no direct rename; recreate)
      3. Delete tags
    6. Power viewing
    7. Practical Naming for Tags
    8. Common “Why Isn’t This Working?” Pitfalls
    9. Real-world mini recipes (copy/paste)
    10. Why teams use tags in practice?