Tags and Releases
A tag is a fixed, human-readable name for one specific commit — the way you mark "this is exactly what we shipped as v1.0." Unlike a branch, a tag is not supposed to move; once v1.0 points at a commit, everyone who fetches it expects that pointer to stay put forever.
The distinction that matters for releases is lightweight versus annotated. A lightweight tag is a bare ref pointing at a commit; an annotated tag is a real object carrying who tagged it, when, a message, and optionally a cryptographic signature. Only annotated or signed tags belong on anything the public depends on.
Lightweight vs Annotated
git tag v1.0 creates a lightweight tag: just a ref under refs/tags/ pointing at the current commit, with no metadata of its own. git tag -a v1.0 -m "Release 1.0" creates an annotated tag, which is a stored object recording the tagger, a timestamp, and the message.
If you are unsure which kind you made, git cat-file -t v1.0 tells you: it reports commit for a lightweight tag (it points straight at the commit) and tag for an annotated one (the tag object sits in between).
Signed Tags
git tag -s v1.0 -m "..." creates an annotated tag and signs it, by default with GPG, or with SSH or X.509 keys when you set gpg.format. The signature binds the release to an identity, so consumers can prove who cut it. git tag -v v1.0 verifies that signature against the signer's public key.
A signature is only worth as much as the verification step: signing without ever running git tag -v in your pipeline leaves the cryptography purely decorative.
Pushing and Listing Tags
Tags do not ride along with git push the way commits on a branch do. You push a specific one with git push origin v1.0, or push all of them with git push --tags. Until you do, the tag exists only in your local repository and never reaches the remote or CI.
To turn the nearest tag into a version string for a build, git describe --tags produces something like v1.0-14-g2a3b4c5: the last tag, how many commits since, and the abbreviated commit it names.
Tags vs Releases
A Git tag is the immutable marker in the repository. A GitHub "Release" is a layer on top of a tag: it wraps that tag with release notes, a title, and downloadable binary assets. The Release is a presentation and distribution convenience, but the tag underneath is the source of truth for what code shipped.
Because the Release points at the tag, deleting or moving the underlying tag breaks the link and leaves the Release referring to something that no longer exists where it claimed.
Lightweight tag — a bare ref with no metadata of its own, fine as a private bookmark or a throwaway marker you alone use.
Annotated tag — a stored object carrying tagger, date, message, and an optional signature, and the only correct choice for a release others depend on. Use -a (or -s) for anything public.
- Cutting a release with a lightweight tag, leaving no record of who tagged it, when, or why.
- Forgetting tags do not push by default, so
v1.0exists locally but never reaches the remote or the release pipeline. - Moving a published tag with
git tag -fafter people have fetched it, breaking everyone who already has the old target. - Signing tags but never running
git tag -vin CI, so the signature proves nothing and is purely decorative. - Relying on a GitHub Release while deleting or moving its underlying tag, breaking the link the Release depends on.
- Cut every release with an annotated tag:
git tag -a v1.2.0 -m "...". - Sign release tags with
git tag -sand verify them in CI withgit tag -v. - Push tags explicitly with
git push origin <tag>rather than assuming they travel with commits. - Derive build version strings from
git describe --tagsinstead of hand-maintaining them. - Treat published tags as immutable — never
-fone; cut a new version instead.
.hgtags plus local tags, gpg extension for signingSubversion tags are mutable directory copies under /tagsPerforce uses labelsFossil tags stored in-repo, including a release patternKnowledge Check
Why is a lightweight tag wrong for a public release?
- It is a bare ref with no tagger, date, message, or signature, so there is no record of who cut it
- It cannot point at a release commit at all, only at a branch tip that keeps moving as new work lands
- It expires and is automatically deleted after about a week, so the release marker vanishes from the repository once its short lifetime runs out
- It can never be pushed to a remote for others to fetch, so the release marker stays trapped on the machine that created it
What does an annotated tag carry that a lightweight one does not?
- A stored tag object with tagger, date, message, and an optional signature
- A full copy of the entire working tree captured at tag time and stored inside the tag object itself
- A forward pointer to the next release, linking each tag to its successor so you can walk the chain
- The tagged commit's full diff against its parent commit, stored inline
Why doesn't a tag reach the remote when you push a branch?
- Tags are not pushed with commits by default; you push them with
git push origin <tag>or--tags - Tags exist server-side only and are created on the remote, never locally, so a branch push has nothing to send
- The remote strips out any tags that arrive alongside an ordinary branch push
- Only annotated tags ride along with a branch push; lightweight ones never travel
What is the consequence of force-moving a published tag?
- Anyone who already fetched the old target keeps it, so their tag disagrees with the remote and builds diverge
- Nothing happens, because Git syncs the new tag target to every clone automatically on the next fetch, overwriting the old one
- Moving a published tag deletes it from the repository outright, taking the old release marker with it
- Every commit downstream of the tag is rewritten so the history matches its new target
What is the relationship between a Git tag and a GitHub Release?
- The tag is the immutable source of truth; the Release wraps it with notes and assets and breaks if the tag moves
- The Release is the immutable source of truth and the tag is generated from it, with the repo's ref derived from the commit the Release records
- They are unrelated; a Release stands alone on GitHub and needs no underlying tag in the repository
- A Release fully replaces the underlying tag once it is published, and the tag itself is then removed
You got correct