| |

Git 4d 🧩 Tagging Releases

A tag is a permanent label on a specific commit. Unlike branches, tags don’t move — once placed, they stay on that commit forever. That makes them perfect for marking releases: v1.0, v2.3.1, v1.0.0-rc.2. When someone asks “what code was in version 1.0?”, the tag answers immediately.

Key point: A tag is a named pointer to a commit — same mechanism as a branch, but designed to be immutable. Branches move as you commit; tags stay put. That’s why they’re used for releases.


Basic tag creation

Git has two kinds of tags: lightweight and annotated. They look similar but behave differently.

Lightweight tags — just a pointer to a commit, no metadata.

git tag v1.0

That’s it. v1.0 now points to the current commit. No message, no tagger, no date — the tag itself is just a label.

git tag
# [ v1.0 ]
# [ v2.0 ]
# [ v3.0 ]

git show v1.0
# (shows the commit info — no tag-specific metadata)

Annotated tags — stored as full Git objects with a tagger name, email, date, and message.

git tag -a v1.0 -m "Release version 1.0"
git show v1.0
# [ tag v1.0 ]
# [ Tagger: John Doe <john.doe@example.com> ]
# [ Date:   Mon Jan 15 14:30:00 2024 -0500 ]
# [
# [ Release version 1.0
# [
# [ commit abc1234... ]

The annotated tag has its own identity — who created it, when, and why.

FeatureLightweightAnnotated
MetadataNoneTagger, date, message
Git objectNoYes
SignedNoOptional
git describeIgnoredUsed
Recommended forPersonal markersReleases

Why annotated is recommended for releases: An annotated tag records who released what and when. For anything public, that’s the information you want. Lightweight tags are fine for quick personal bookmarks.


Tag management commands

Listing tags:

# All tags
git tag

# Pattern match
git tag -l "v1.*"

# With messages (annotated)
git tag -l -n

# Sorted by version
git tag --sort=version

git tag -l -n shows each tag with the first line of its message — useful for scanning a release history.

Deleting tags:

# Delete a local tag
git tag -d v1.0

# Delete a remote tag
git push origin --delete v1.0

# Delete many local tags matching a pattern
git tag -l "v1.*" | xargs git tag -d

Local and remote tags are separate — deleting one doesn’t affect the other. You need to delete both.

Tagging specific commits:

# By hash
git tag v1.0 abc1234

# Previous commit
git tag v1.0 HEAD~1

# Annotated, specific commit
git tag -a v1.0 -m "Release" abc1234

The tag doesn’t have to point at HEAD. It can point at any commit in the history.

Why delete both: Local tags live in .git/refs/tags. Remote tags live on the server. git tag -d only touches the local one. git push origin --delete removes the remote one. Skipping either leaves the tag visible in one place.


Remote tag operations

Tags aren’t pushed automatically with git push. You have to push them explicitly.

Pushing tags:

# Push all tags
git push origin --tags

# Push a specific tag
git push origin v1.0

# Verbose
git push origin --tags -v

Fetching tags:

# Fetch all tags
git fetch origin --tags

# Fetch a specific tag
git fetch origin v1.0

Most workflows push tags right after tagging a release. Pulling teammates get the tags on their next fetch.

Why tags aren’t auto-pushed: A single git push --tags can push dozens of local tags that were never meant to be shared. Explicit pushing gives you control over what becomes public.


Complete example: release management workflow

A full walkthrough — from empty repo to published release tags.

Initial repository state:

git init
echo "Project started" > README.md
git add README.md
git commit -m "Initial commit"

echo "Main application code" > app.js
git add app.js
git commit -m "Add main application file"

echo "User authentication module" > auth.js
git add auth.js
git commit -m "Implement user authentication"

Three commits. Now tag the milestone.

Create a development tag:

git tag -a v0.1-alpha -m "Alpha release for initial features"

Add more features, then a beta tag:

echo "Dashboard UI" > dashboard.js
git add dashboard.js
git commit -m "Implement dashboard UI"
git tag -a v0.2-beta -m "Beta release with dashboard features"

Stable release:

echo "Bug fixes and performance improvements" > fix.js
git add fix.js
git commit -m "Fix critical bugs and improve performance"
git tag -a v1.0 -m "Stable release version 1.0"

Examine the tags:

git tag -l -n
# [ v0.1-alpha   Alpha release for initial features ]
# [ v0.2-beta    Beta release with dashboard features ]
# [ v1.0         Stable release version 1.0 ]
git show v1.0
# [ tag v1.0 ]
# [ Tagger: Developer Name <developer@example.com> ]
# [ Date:   Mon Jan 15 14:30:00 2024 -0500 ]
# [
# [ Stable release version 1.0
# [
# [ commit abc1234... ]

Push to remote:

git remote add origin https://github.com/user/project.git
git push origin --tags

Verify:

git ls-remote --tags origin
# [ abc1234 refs/tags/v0.1-alpha ]
# [ def5678 refs/tags/v0.2-beta ]
# [ ghi9012 refs/tags/v1.0 ]

All three tags are on the remote.

Why this pattern works: Tags mark milestones. As the project progresses from alpha to beta to stable, tags record where each version lived. The history of releases is readable at a glance.


Advanced tagging techniques

Tag with a specific date:

git tag -a v2.0 -m "Release 2.0" --date="2024-01-15"

The tag records the given date instead of “now.”

Signed tags (requires GPG key):

git tag -s v2.0 -m "Signed release 2.0"

The tag is cryptographically signed. Anyone with the public key can verify it came from you.

Filter tags:

# All version tags
git tag -l "v[0-9]*"

# Tags mentioning "release"
git tag -l -n | grep -i "release"

# Pre-release tags only
git tag -l | grep -E "(alpha|beta|rc)"

Sort by version:

git tag --sort=version

--sort=version understands semver ordering — v1.10 comes after v1.9.

Tags containing a commit:

git tag --contains abc1234

Shows every tag whose history includes abc1234.

Why signed tags matter: For security-sensitive projects, a signed tag proves who created it. git verify-tag v2.0 checks the signature against a known GPG key.


Tag cleanup and maintenance

Remove a local tag:

git tag -d v0.1-alpha

Remove a remote tag:

git push origin :refs/tags/v0.1-alpha

Or with the shorter form:

git push origin --delete v0.1-alpha

Both syntaxes work. The colon form is older; --delete is clearer.

Sort by version:

git tag --sort=version

Only major versions:

git tag -l "v[0-9].*" | sort -V

Filter out pre-release tags:

git tag -l | grep -v -E "(alpha|beta|rc)"

Keeps only stable releases.

Why cleanup matters: Repositories accumulate tags quickly. A project with 200 tags is harder to navigate than one with 20. Delete superseded or erroneous tags — locally and remotely.


Complete Example Session

# ============================================
# PART 1: INIT AND COMMITS
# ============================================

git init
echo "Project started" > README.md
git add README.md
git commit -m "Initial commit"

echo "Main application code" > app.js
git add app.js
git commit -m "Add main application file"

# ============================================
# PART 2: LIGHTWEIGHT TAG
# ============================================

git tag v0.1

git tag
# [ v0.1 ]

git show v0.1
# (commit info only)

# ============================================
# PART 3: ANNOTATED TAG
# ============================================

git tag -a v0.2 -m "Second release"

git show v0.2
# [ tag v0.2 ]
# [ Tagger: Kronos <kronos@example.com> ]
# [ Date: ... ]
# [
# [ Second release
# [
# [ commit abc1234 ]

# ============================================
# PART 4: LIST WITH MESSAGES
# ============================================

git tag -l -n
# [ v0.1 ]
# [ v0.2  Second release ]

# ============================================
# PART 5: TAG A SPECIFIC COMMIT
# ============================================

git log --oneline
# [ abc1234 Add main application file ]
# [ def5678 Initial commit ]

git tag -a v0.0.1 -m "First commit" def5678

git show v0.0.1
# [ tag v0.0.1 ... ]
# [ commit def5678 ]

# ============================================
# PART 6: PUSH TAGS
# ============================================

git remote add origin https://github.com/user/project.git
git push origin --tags
# [ To https://github.com/user/project.git ]
# [  * [new tag]         v0.0.1 -> v0.0.1 ]
# [  * [new tag]         v0.1 -> v0.1 ]
# [  * [new tag]         v0.2 -> v0.2 ]

# ============================================
# PART 7: VERIFY REMOTE TAGS
# ============================================

git ls-remote --tags origin
# [ abc1234 refs/tags/v0.0.1 ]
# [ def5678 refs/tags/v0.1 ]
# [ ghi9012 refs/tags/v0.2 ]

# ============================================
# PART 8: SORT BY VERSION
# ============================================

git tag --sort=version
# [ v0.0.1 ]
# [ v0.1 ]
# [ v0.2 ]

# ============================================
# PART 9: FILTER
# ============================================

git tag -l "v0.1*"
# [ v0.1 ]

# ============================================
# PART 10: DELETE A TAG
# ============================================

git tag -d v0.1
# [ Deleted tag 'v0.1' (was abc1234) ]

git push origin --delete v0.1
# [ To https://github.com/user/project.git ]
# [  - [deleted]         v0.1 ]

Quick Reference

Creating Tags

CommandPurpose
git tag NAMELightweight tag
git tag -a NAME -m "msg"Annotated tag
git tag -s NAME -m "msg"Signed tag
git tag NAME COMMITTag specific commit
git tag -a NAME -m "msg" --date="DATE"With date

Listing Tags

CommandShows
git tagAll tags
git tag -l "PATTERN"Filtered
git tag -l -nWith messages
git tag --sort=versionSorted by semver
git tag --contains COMMITTags including commit

Deleting Tags

CommandEffect
git tag -d NAMEDelete local
git push origin --delete NAMEDelete remote
git push origin :refs/tags/NAMEDelete remote (colon form)
git tag -l "v1.*" | xargs git tag -dDelete matching

Remote Tag Operations

CommandEffect
git push origin --tagsPush all tags
git push origin NAMEPush specific tag
git fetch origin --tagsFetch all tags
git ls-remote --tags originList remote tags

Inspecting Tags

CommandShows
git show NAMETag + commit
git tag -l -nMessage first line
git log NAMEHistory from tag
git describeNearest tag name

Lightweight vs Annotated

AspectLightweightAnnotated
MetadataNoneFull
ObjectNoYes
SignedNoOptional
git describeIgnoredUsed
Use forQuick marksReleases

Naming Conventions

PatternExample
Semanticv1.2.3
Pre-releasev1.0.0-alpha.1
Release candidatev1.0.0-rc.1
Date-based2024.01.15

Best Practices

✅ Do This:

# Use annotated tags for releases
git tag -a v1.0 -m "Release version 1.0"           # ✅

# Use semantic versioning
git tag -a v1.2.3 -m "Release 1.2.3"                # ✅

# Push tags explicitly
git push origin --tags                              # ✅

# Sign release tags
git tag -s v1.0 -m "Signed release"                 # ✅

# Verify remote tags
git ls-remote --tags origin                         # ✅

# Delete both local and remote
git tag -d v0.9
git push origin --delete v0.9                       # ✅

# Tag after merging to main
git checkout main
git merge feature/x
git tag -a v1.1 -m "Release 1.1"                    # ✅

❌ Don’t Do This:

# Don't move an existing tag
git tag -f v1.0                                     # ❌ breaks consumers

# Don't use lightweight tags for public releases
git tag v1.0                                        # ⚠️  no metadata

# Don't push all tags blindly
git push origin --tags                              # ⚠️  if locals exist

# Don't forget to delete remote tags
git tag -d v0.9                                     # ⚠️  remote still has it

# Don't use vague names
git tag release                                      # ❌

# Don't tag on the wrong branch
git tag -a v1.0 -m "..."                            # ⚠️  verify branch first

# Don't re-tag an existing version
git tag -a v1.0 -m "Rebuilt"                        # ❌

Common Pitfalls

PitfallProblemSolution
Forgot to push tagsRemote missing themgit push --tags
Deleted local onlyRemote still has itgit push origin --delete
Moved a published tagBreaks consumersUse a new version
Tagged wrong commitWrong contentDelete, re-tag
Duplicate tag namesConflictUse unique versions
Lightweight for releaseNo metadataUse -a
Unsigned signed-requiredBuild rejectionUse -s
Tag on wrong branchWrong commitVerify before pushing

Real-World Examples

1. Lightweight tag

git tag v0.1

Quick marker.

2. Annotated tag

git tag -a v1.0 -m "Release 1.0"

Standard for releases.

3. Signed tag

git tag -s v1.0 -m "Signed release"

Cryptographically signed.

4. Tag a commit

git tag -a v0.9 -m "Pre-release" abc1234

Points at a specific commit.

5. List with messages

git tag -l -n

Shows each tag’s first message line.

6. Sort by version

git tag --sort=version

Semver ordering.

7. Push all tags

git push origin --tags

Shares every local tag.

8. Push one tag

git push origin v1.0

Shares only that tag.

9. Fetch tags

git fetch origin --tags

Downloads remote tags.

10. Delete local

git tag -d v0.9

Removes it locally.

11. Delete remote

git push origin --delete v0.9

Removes it from the server.

12. Inspect a tag

git show v1.0

Full tag and commit info.

13. Find tags for a commit

git tag --contains abc1234

Tags whose history includes it.

14. Clean pre-release tags

git tag -l | grep -E "(alpha|beta|rc)" | xargs git tag -d

Removes all pre-release tags.

15. Automate in CI

# .github/workflows/release.yml
on:
  push:
    tags:
      - 'v*'
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Releasing ${{ github.ref_name }}"

Tag-based triggers drive release pipelines.


Visual: Branch vs Tag

┌──────────────────────────────────────────────┐
│  Branch                                      │
│                                              │
│  main:  A ──► B ──► C ──► D                  │
│                     ▲                        │
│                     │                        │
│                     points to latest         │
│                     moves with each commit   │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Tag                                         │
│                                              │
│  main:  A ──► B ──► C ──► D                  │
│              ▲                               │
│              │                               │
│              v1.0                            │
│              stays here forever              │
│                                              │
└──────────────────────────────────────────────┘

Visual: Lightweight vs Annotated

┌──────────────────────────────────────────────┐
│  Lightweight tag                             │
│                                              │
│  refs/tags/v1.0 ─────► commit abc1234        │
│                                              │
│  Just a pointer — no metadata                │
│                                              │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Annotated tag                               │
│                                              │
│  refs/tags/v1.0 ─────► tag object            │
│                          │                   │
│                          ▼                   │
│                        commit abc1234        │
│                                              │
│  Tag object contains:                        │
│    Tagger: name, email                       │
│    Date                                      │
│    Message                                   │
│    Optional GPG signature                    │
│                                              │
└──────────────────────────────────────────────┘

Visual: Local vs Remote Tags

┌──────────────────────────────────────────────┐
│  Your machine                                │
│                                              │
│  .git/refs/tags/                             │
│    v0.1                                      │
│    v0.2                                      │
│    v1.0                                      │
│                                              │
└─────────────────┬────────────────────────────┘
                  │
                  │  git push origin --tags
                  ▼
┌──────────────────────────────────────────────┐
│  Remote repository                           │
│                                              │
│  refs/tags/                                  │
│    v0.1                                      │
│    v0.2                                      │
│    v1.0                                      │
│                                              │
│  Deleting locally doesn't remove them here   │
│                                              │
└──────────────────────────────────────────────┘

Visual: Release Workflow

┌──────────────────────────────────────────────┐
│  1. Finish features on main                  │
│                                              │
│  2. Merge and test                           │
│                                              │
│  3. Tag the release                          │
│     git tag -a v1.0 -m "Release 1.0"         │
│                                              │
│  4. Push the tag                             │
│     git push origin v1.0                     │
│                                              │
│  5. CI/CD picks up the tag and deploys       │
│                                              │
│  6. Consumers clone or fetch at v1.0         │
│                                              │
└──────────────────────────────────────────────┘

Visual: Version Sorting

┌──────────────────────────────────────────────┐
│  Without --sort=version                      │
│                                              │
│  v1.10                                       │
│  v1.2                                        │
│  v1.9                                        │
│                                              │
│  (lexicographic — wrong order)               │
│                                              │
├──────────────────────────────────────────────┤
│  With --sort=version                         │
│                                              │
│  v1.2                                        │
│  v1.9                                        │
│  v1.10                                       │
│                                              │
│  (semver-aware — correct order)              │
│                                              │
└──────────────────────────────────────────────┘

Summary

CommandPurpose
git tag NAMELightweight tag
git tag -a NAME -m "msg"Annotated tag
git tag -s NAME -m "msg"Signed tag
git tag -lList tags
git tag -l -nList with messages
git tag --sort=versionSort by semver
git show NAMEInspect tag
git tag -d NAMEDelete local
git push origin NAMEPush one tag
git push origin --tagsPush all tags
git push origin --delete NAMEDelete remote
git fetch origin --tagsFetch tags

Key takeaways:

  • A tag is a permanent label on a commit — it doesn’t move like a branch
  • Lightweight tags are simple pointers; annotated tags carry metadata
  • Use annotated tags for releases — they record who, when, and why
  • Signed tags (-s) prove authenticity for security-sensitive projects
  • Tags are not pushed automatically — use git push origin --tags or push specific ones
  • Local and remote tags are separate — delete both when cleaning up
  • Use semantic versioning — v1.2.3, v1.0.0-rc.1
  • Sort with --sort=version for correct semver ordering
  • Never move a published tag — cut a new version instead
  • Tag releases on main after merging and testing
  • Automate release tags in CI — triggers on push, deploys on tag
  • Keep tags clean — delete superseded or erroneous ones

Remember: Tags are how you mark releases. Use annotated tags for anything public, push them explicitly, and never move one after it ships. Sort them by version, filter out pre-releases when needed, and automate the whole flow in CI. A well-tagged repository tells the story of its releases at a glance.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!