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.
| Feature | Lightweight | Annotated |
|---|---|---|
| Metadata | None | Tagger, date, message |
| Git object | No | Yes |
| Signed | No | Optional |
git describe | Ignored | Used |
| Recommended for | Personal markers | Releases |
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 -donly touches the local one.git push origin --deleteremoves 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 --tagscan 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.0checks 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
| Command | Purpose |
|---|---|
git tag NAME | Lightweight tag |
git tag -a NAME -m "msg" | Annotated tag |
git tag -s NAME -m "msg" | Signed tag |
git tag NAME COMMIT | Tag specific commit |
git tag -a NAME -m "msg" --date="DATE" | With date |
Listing Tags
| Command | Shows |
|---|---|
git tag | All tags |
git tag -l "PATTERN" | Filtered |
git tag -l -n | With messages |
git tag --sort=version | Sorted by semver |
git tag --contains COMMIT | Tags including commit |
Deleting Tags
| Command | Effect |
|---|---|
git tag -d NAME | Delete local |
git push origin --delete NAME | Delete remote |
git push origin :refs/tags/NAME | Delete remote (colon form) |
git tag -l "v1.*" | xargs git tag -d | Delete matching |
Remote Tag Operations
| Command | Effect |
|---|---|
git push origin --tags | Push all tags |
git push origin NAME | Push specific tag |
git fetch origin --tags | Fetch all tags |
git ls-remote --tags origin | List remote tags |
Inspecting Tags
| Command | Shows |
|---|---|
git show NAME | Tag + commit |
git tag -l -n | Message first line |
git log NAME | History from tag |
git describe | Nearest tag name |
Lightweight vs Annotated
| Aspect | Lightweight | Annotated |
|---|---|---|
| Metadata | None | Full |
| Object | No | Yes |
| Signed | No | Optional |
git describe | Ignored | Used |
| Use for | Quick marks | Releases |
Naming Conventions
| Pattern | Example |
|---|---|
| Semantic | v1.2.3 |
| Pre-release | v1.0.0-alpha.1 |
| Release candidate | v1.0.0-rc.1 |
| Date-based | 2024.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
| Pitfall | Problem | Solution |
|---|---|---|
| Forgot to push tags | Remote missing them | git push --tags |
| Deleted local only | Remote still has it | git push origin --delete |
| Moved a published tag | Breaks consumers | Use a new version |
| Tagged wrong commit | Wrong content | Delete, re-tag |
| Duplicate tag names | Conflict | Use unique versions |
| Lightweight for release | No metadata | Use -a |
| Unsigned signed-required | Build rejection | Use -s |
| Tag on wrong branch | Wrong commit | Verify 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
| Command | Purpose |
|---|---|
git tag NAME | Lightweight tag |
git tag -a NAME -m "msg" | Annotated tag |
git tag -s NAME -m "msg" | Signed tag |
git tag -l | List tags |
git tag -l -n | List with messages |
git tag --sort=version | Sort by semver |
git show NAME | Inspect tag |
git tag -d NAME | Delete local |
git push origin NAME | Push one tag |
git push origin --tags | Push all tags |
git push origin --delete NAME | Delete remote |
git fetch origin --tags | Fetch 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 --tagsor 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=versionfor 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!