Annotated vs lightweight tags Jump to heading
Git has two kinds of tag, and they are created by almost the same command. git tag v2.4.0 creates a lightweight tag: a ref pointing directly at a commit, nothing more. git tag -a v2.4.0 -m "Release 2.4.0" creates an annotated tag: a tag object with its own author, date and message, which the ref points to. For a bookmark in your local repository the difference hardly matters. For release tags it matters a great deal: only annotated tags can be signed, git describe ignores lightweight tags by default, --follow-tags pushes only annotated ones, and the tagβs date and author record who released what and when. This page explains the differences, sets defaults that produce the right kind, and shows how to find and replace lightweight release tags, within release tagging and versioning.
When to use this approach Jump to heading
- You create release tags and are not sure which kind your tooling produces.
git describeignores some of your tags, orgit push --follow-tagsdoes not push them.- You want to sign release tags, as in signing and verifying release tags.
- Release automation creates tags, and you need them to be consistent.
Step 1 β See what each kind stores Jump to heading
A lightweight tag is a ref whose value is a commit ID. An annotated tag is a ref whose value is the ID of a tag object, which in turn points to the commit and carries metadata.
git tag v2.4.0-light
git tag -a v2.4.0 -m "Release 2.4.0"
git cat-file -t v2.4.0-light # commit
git cat-file -t v2.4.0 # tag
git cat-file -p v2.4.0
# object 8b3e4f2β¦
# type commit
# tag v2.4.0
# tagger Priya Raman <[email protected]> 1759395600 +0200
#
# Release 2.4.0 Step 2 β Make annotated (and signed) the default Jump to heading
Configure Git so git tag with a message produces an annotated tag, and signing is automatic.
git config --global tag.gpgSign true # git tag -a β¦ is signed automatically
git config --global push.followTags true # git push also pushes reachable annotated tags There is no setting that turns a bare git tag name into an annotated tag. Teach the -a (or -s) habit and enforce it for release tags in CI or on the server.
Step 3 β Check how describe and push treat them Jump to heading
git describe uses only annotated tags unless told otherwise, so version strings derived from it skip lightweight tags. --follow-tags pushes annotated tags reachable from the pushed commits, but not lightweight ones.
git describe # v2.4.0-3-g1a2b3c4 (annotated only)
git describe --tags # also considers lightweight tags
git push --follow-tags # pushes v2.4.0 but not v2.4.0-light Version derivation is covered in deriving versions from git describe.
Step 4 β Find lightweight release tags in your repository Jump to heading
List tags with their object type, and filter release-pattern tags that are lightweight.
git for-each-ref --format='%(objecttype) %(refname:short)' 'refs/tags/v*' | awk '$1=="commit" {print $2}' Step 5 β Replace a lightweight release tag Jump to heading
If a published release tag is lightweight, replacing it with an annotated tag at the same commit changes the refβs value from a commit ID to a tag-object ID. The commit is the same, but tools that cached the old value see a change, so treat it as moving a published tag.
commit=$(git rev-parse 'v2.3.0^{commit}')
git tag -d v2.3.0
git tag -s v2.3.0 "$commit" -m "Release 2.3.0 (re-tagged as annotated)"
git push --force origin refs/tags/v2.3.0 β οΈ SAFETY WARNING: Force-pushing a tag changes it for everyone who fetches afterwards, while those who fetched before keep the old one until they explicitly re-fetch tags. For published releases, announce the change, and prefer replacing only tags that are not consumed by package managers or deploy systems. The considerations are in moving or deleting a published tag.
Step 6 β Enforce annotated release tags Jump to heading
Reject lightweight release tags at push time with a server hook or a CI check on tag pushes.
# pre-receive / update: release tags must be annotated
case "$ref" in
refs/tags/v*) [ "$(git cat-file -t "$new")" = tag ] || { echo "release tags must be annotated (git tag -a/-s)"; exit 1; } ;;
esac Rulesets on hosted forges cannot check tag type directly, so a CI job on push: tags that fails and alerts is the practical equivalent there.
on:
push:
tags: ['v*']
jobs:
tag-type:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: |
t=$(git cat-file -t "refs/tags/$GITHUB_REF_NAME")
[ "$t" = tag ] || { echo "::error::$GITHUB_REF_NAME is a lightweight tag β re-create it with git tag -s"; exit 1; } The job cannot undo the push, but it fails visibly before any release job consumes the tag, and gives the release manager a clear instruction.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Do forges show the difference? Jump to heading
Release pages work with both, but only annotated tags show a tagger, date and verification badge. A forge βreleaseβ can add notes to a lightweight tag, but those notes live on the forge, not in Git.
Can an annotated tag point to something other than a commit? Jump to heading
Yes β a tree, a blob or another tag. That is rare in practice; release tags should point to commits, and git cat-file -p shows the type of the target.
Which kind does git tag -m create? Jump to heading
Passing -m implies -a, so it creates an annotated tag. Only git tag name with no message options creates a lightweight one.
Related Jump to heading
- Release Tagging & Versioning β the parent topic.
- Signed Tags vs Signed Commits β what a signature on a tag proves.
- Tagging Pre-Releases and Release Candidates β naming tags consistently.
- Iterating Refs with for-each-ref β listing tags by type in scripts.