Git Tag: How to Mark Releases and Version Your Code (With Examples)
A commit hash like `a1b2c3d` is precise but meaningless to a human. When you ship version 1.0.0 of your software, you don’t want to email your team a 40-character SHA and hope they paste it correctly. You want a stable, human-readable label that points to exactly that commit, forever. That is what a git tag is: a named, permanent pointer to a specific commit, used to mark releases and versions like `v1.0.0`.
Tags are how Git turns a flat stream of commits into a versioned history you can reason about. This guide covers the two tag types, how to create them, the one gotcha that catches nearly every developer, and how tags connect to releases and deployment.
Key Takeaways
• A git tag is an immutable, named pointer to one specific commit — ideal for marking releases.
• Annotated tags (`git tag -a`) store a message, tagger, and date; use them for releases. Lightweight tags are just a name.
• Tags do not travel with a normal `git push`. You must push them explicitly: `git push origin v1.0.0`.
• Follow semantic versioning (`vMAJOR.MINOR.PATCH`) so version numbers communicate the scope of change.
• GitHub and GitLab releases are built directly on top of git tags.
What is a git tag and why does it matter?
A branch and a tag are both references to a commit, but they behave in opposite ways. A branch moves — every time you commit, the branch pointer advances to the new commit. A tag is fixed — once you create `v1.0.0`, it points to that commit and never moves on its own. That permanence is the entire point. A version marker that drifts is not a version marker.
This is why tags are the foundation of any sane release process. When a customer reports a bug “in v2.3.1,” you can check out exactly that code, reproduce the issue, and know you are looking at the same bytes that shipped. Versioning your deployments this way is a core part of running a real, controlled environment — see the complete guide to hosting for developers for how this fits into a full self-managed workflow.
What is the difference between lightweight and annotated tags?
Git has two kinds of tags, and the distinction matters more than most tutorials admit.
A lightweight tag is just a name pointing at a commit — nothing more. No metadata, no message, no record of who created it. It is effectively a branch that never moves.
“`bash
git tag v1.0.0-rc1 “`
An annotated tag is a full Git object. It stores the tagger’s name and email, a timestamp, and a tagging message, and it can be GPG-signed. This is the recommended type for any real release, because it records *who* cut the release, *when*, and *why*.
“`bash
git tag -a v1.0.0 -m “Release version 1.0.0 — initial public launch” “`
The rule of thumb: use annotated tags for releases, lightweight tags for private, temporary, or throwaway markers. When in doubt, annotate.
How do you create a git tag?
To create a git tag on the commit you currently have checked out, you only need the tag name.
“`bash
git tag -a v1.0.0 -m “Release version 1.0.0”
git tag v1.0.0 “`
You are not limited to tagging the current commit. To tag a past commit, pass its hash as the final argument. This is invaluable when you forgot to tag a release at the time and need to label it retroactively.
“`bash
git tag -a v0.9.0 9fceb02 -m “Beta release, tagged retroactively” “`
If you try to create a tag that already exists, Git refuses and protects you from accidentally moving a release marker — exactly the immutability behavior you want.
“`bash
git tag -a v1.0.0 -m “oops”
“`
How do you list and view git tags?
Listing tags is straightforward, and the filtering options are genuinely useful on a repository with hundreds of releases.
“`bash
git tag
git tag -l “v1.*”
git tag –sort=-version:refname “`
To inspect a specific tag, `git show` displays the tag metadata (for annotated tags) followed by the commit it points to.
“`bash git show v1.0.0 “`
For an annotated tag, the output includes the tagger, date, and your message — the audit trail a lightweight tag simply does not have:
“`text tag v1.0.0 Tagger: Ravi Subramanian
Release version 1.0.0 — initial public launch
commit a1b2c3d4e5f6… “`
How do you push a git tag to a remote?
Here is the part that trips up nearly everyone exactly once.
The most common git tag surprise is that tags do not travel with a normal `git push`. You push your commits and branches, the tag stays local, and your release marker silently never reaches the remote. So your teammates and CI never see `v1.0.0`. Developers tag a release, run `git push`, and then stare at GitHub wondering where the tag went.
The reason is deliberate. Git treats tags as a separate namespace from branches *precisely because tags are permanent, immutable markers* — a tag should always point to the same commit forever. Branches move and auto-sync; pushing a fixed milestone is something Git makes you do on purpose rather than sweeping it along automatically. The habit that fixes it: after creating a release tag, push it deliberately.
“`bash
git push origin v1.0.0
git push –tags
git push –follow-tags “`
`git push –follow-tags` is the pragmatic middle ground for day-to-day work: it pushes your branch and any annotated tags reachable from it, but ignores lightweight tags — which is usually what you want.
How do you delete a git tag?
Deleting a tag is a two-step affair: local and remote are independent, and removing one does not remove the other.
“`bash
git tag -d v1.0.0
git push origin –delete v1.0.0 “`
Be cautious deleting *published* tags. If a teammate or a CI pipeline has already pulled `v1.0.0`, deleting it remotely will not remove it from their machines, and re-creating it on a different commit creates a confusing split. Treat published release tags as permanent — that is the contract.
How do you check out a git tag?
To inspect the exact code of a release, check out the tag. Because a tag is not a branch, this puts you in a detached HEAD state — you are viewing a fixed point in history, not on any branch.
“`bash git checkout v1.0.0
“`
This is perfect for inspecting or building a known-good release. If you discover you need to make changes *from* that tag — say, a hotfix on an old version — create a branch from it.
“`bash
git checkout -b hotfix-1.0.1 v1.0.0 “`
Git tag command reference
| Command | What it does |
|---|---|
| `git tag v1.0.0` | Create a lightweight tag on the current commit |
| `git tag -a v1.0.0 -m “msg”` | Create an annotated tag with a message |
| `git tag -a v1.0.0 |
Tag a specific past commit |
| `git tag` | List all tags |
| `git tag -l “v1.*”` | List tags matching a pattern |
| `git show v1.0.0` | Show tag metadata and the commit it points to |
| `git push origin v1.0.0` | Push a single tag to the remote |
| `git push –tags` | Push all local tags |
| `git push –follow-tags` | Push commits plus reachable annotated tags |
| `git tag -d v1.0.0` | Delete a tag locally |
| `git push origin –delete v1.0.0` | Delete a tag on the remote |
| `git checkout v1.0.0` | Check out a tag (detached HEAD) |
How does semantic versioning work with git tags?
Most teams name release tags using semantic versioning (semver): `vMAJOR.MINOR.PATCH`. Each segment communicates the scope of change so consumers know what to expect before they upgrade.
- MAJOR (`v2.0.0`) — breaking changes; existing integrations may stop working.
- MINOR (`v1.3.0`) — new, backward-compatible features.
- PATCH (`v1.2.4`) — backward-compatible bug fixes only.
“`bash git tag -a v1.0.0 -m “First stable release” git tag -a v1.1.0 -m “Add export feature (backward compatible)” git tag -a v1.1.1 -m “Fix CSV encoding bug” git tag -a v2.0.0 -m “Rewrite API — breaking changes” “`
The leading `v` is a widely-adopted convention but not part of the semver spec itself; pick one style and stay consistent across the whole project.
How do git tags relate to releases?
A GitHub Release (and GitLab Release, and most package registries) is built directly on top of a git tag. The tag is the immutable anchor; the release adds human-facing artifacts — release notes, compiled binaries, changelogs — attached to that tag.
In practice the flow is: tag the commit, push the tag, then create the release pointing at it. If you use GitHub’s CLI:
“`bash
git tag -a v1.0.0 -m “Release 1.0.0” git push origin v1.0.0 gh release create v1.0.0 –notes “First public release” “`
This is also the cleanest way to drive automated deployments: a CI pipeline watches for a new tag matching `v*`, builds the artifact, and ships it. The tag *is* the trigger and the version of record.
Deploy versioned releases on infrastructure you control. DarazHost VPS and dedicated servers give developers full Git control on the server — tag releases, push tags, and check out a specific tagged version to deploy a known-good release, all over SSH with guaranteed resources and 24/7 support. When your deployment pipeline depends on hitting an exact, immutable version, you need a real environment with predictable performance behind it — not a shared box where another tenant’s load decides your build times. That is the versioned, controlled environment serious deployment needs.
Frequently asked questions
What is the difference between a git tag and a branch? A branch is a moving pointer that advances with every new commit; a tag is a fixed pointer that stays on one commit permanently. Use branches for active development and tags for marking immutable points like releases.
Why is my git tag not showing up on GitHub after I pushed? Because a normal `git push` does not include tags. Push the tag explicitly with `git push origin
Should I use lightweight or annotated git tags? Use annotated tags (`git tag -a`) for anything you publish as a release — they record the tagger, date, and a message. Reserve lightweight tags for temporary or private markers where metadata does not matter.
Can I move a git tag to a different commit? You can force-move it with `git tag -f`, but you should not do this for published release tags. The value of a tag is that it never changes; moving it breaks that contract for anyone who already pulled it.
How do I create a git tag for an old commit? Pass the commit hash as the final argument: `git tag -a v0.9.0