Git, CI/CD & GitOpsXIX · Tags and ReleasesTags and Releases
Lightweight versus annotated tags — what each stores and why the audit trail differs
What you'll learn
- State exactly what a lightweight tag stores on disk and what it does not
- State exactly what an annotated tag stores and what each field means
- Explain why an annotated tag is content-addressed and a lightweight tag is not
- Articulate the audit trail difference between the two kinds of tags
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A tag names a moment. The question that decides whether you
need a lightweight tag or an annotated tag is: who needs to
answer who, when, and why later? If the answer is “no-one,
this is a personal note”, a lightweight tag is sufficient. If
the answer is “the security team, the audit team, the
compliance team, the supply-chain consumer six months from
now”, an annotated tag is the only correct answer. The two
kinds of tags look similar in git tag, behave similarly in
git push, and differ catastrophically in what they record.
What a lightweight tag stores
A lightweight tag is a file under .git/refs/tags/ whose
contents are exactly the OID of a commit. There is no other
field, no metadata, no signature:
# Create a lightweight tag
git tag v1.0.0-light "$COMMIT_OID"
cat "$GIT_DIR/refs/tags/v1.0.0-light"
# 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
The tag is a name; the OID is its sole content. There is no
tag object in the object store for this tag — git cat-file -t v1.0.0-light returns commit (the tag dereferences
straight to the commit), and git cat-file -p v1.0.0-light
prints the commit object. The tag itself is not an object.
The information a lightweight tag records is “this commit”.
flowchart LR
A["refs/tags/v1.0.0-light\ncontents: 8a3f9d2..."] --> B["commit 8a3f9d2"]
What an annotated tag stores
An annotated tag is a ref under .git/refs/tags/ whose
contents are the OID of a tag object. The tag object lives
in the object store, and its contents are a structured
payload:
# Create an annotated tag
git tag -a -m "Release v1.0.0" v1.0.0 "$COMMIT_OID"
cat "$GIT_DIR/refs/tags/v1.0.0"
# aa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
git cat-file -p v1.0.0
# object 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
#
# Release v1.0.0
Each field answers a specific question:
object— the OID of the target. This is the answer to “what commit did the tag name?”.type— the type of the target. For tags pointing at commits, this iscommit; tags can also point at blobs or trees (rarely used in production).tag— the tag’s human name. This is the name thatrefs/tags/v1.0.0resolves to ingit show.tagger— the name, email, UNIX timestamp, and timezone of the person who created the tag. This is the answer to “who tagged it and when”.- The remainder of the payload — the message body. This is the answer to “why was this commit tagged”.
If the tag was signed (covered in XIX-04), the payload also
contains a gpgsig block with the GPG signature over the
tag bytes.
flowchart LR
A["refs/tags/v1.0.0\ncontents: aa1b2c3d..."] --> B["tag object aa1b2c3d\nobject: 8a3f9d2\ntype: commit\ntag: v1.0.0\ntagger: Ops <ops@example.com> 1730000000\nmessage: Release v1.0.0\ngpgsig: ..."]
B --> C["commit 8a3f9d2"]
Why the audit trail differs
The two structures encode different amounts of information, and the difference is what an audit can reconstruct six months later.
A lightweight tag answers three questions:
- What commit was tagged? The commit OID.
- What was on that commit?
git showagainst the commit. - When was the tag created? Not recorded anywhere on the tag itself. The closest answer is the mtime of the ref file in the local clone, which is per-clone and per-host and has no relation to the actual tagging moment.
An annotated tag answers five:
- What commit was tagged? The
objectfield. - What was the target type? The
typefield. - Who tagged it? The
taggerfield. - When was it tagged? The
taggerfield’s timestamp. - Why was it tagged? The message body.
Why an annotated tag is content-addressed
The tag object’s OID is computed from the tag payload bytes — the object line, the type line, the tag line, the tagger line, and the message. Any change to the payload produces a new OID. This means two repositories that tag the same commit with the same tagger, date, message, and signature arrive at the same tag OID. The tag is a cryptographic commitment to its own contents.
A lightweight tag is not content-addressed. The OID stored
under the ref is the commit’s OID; the tag itself has no
OID. Two repositories can create a lightweight tag called
v1.0.0 pointing at different commits and the difference is
invisible in the tag’s identity.
# Two repositories, two lightweight tags, no way to tell apart
# Repo A
git tag v1.0.0 8a3f9d2
# Repo B
git tag v1.0.0 4d2c8e0a
# Both have refs/tags/v1.0.0; the contents differ; the tag
# has no OID of its own to compare.
This is why signing and verification only work on annotated tags (XIX-04): there is nothing to sign in a lightweight tag. The signature has to be over a payload, and the payload has to be an object.
Production discipline
- Annotated tags for releases, lightweight tags for personal notes. A release tag without a tagger, a date, and a message is a release without an audit trail.
- Never substitute a branch for an annotated tag. A
branch is a moving ref; an annotated tag is a fixed
release marker. They look similar in
git tagbut behave completely differently ingit verify-tagandgit describe. - Treat the tag message as part of the release notes. The tag message is the durable record of what the release was for. Write it for the engineer who reads it in three years — that engineer might be you, in an incident.
- Forbid lightweight release tags in CI configuration.
If the CI pipeline runs
git tag, the only tag command in the pipeline should begit tag -a -m ...orgit tag -s -m .... Agit tag <name>in a release job is a smell that points at a missing tag message.
Cross-course references
- Git, CI/CD & GitOps — Part III (Git Objects) — the tag object’s on-disk layout and the role of the content-addressed store.
- Git, CI/CD & GitOps — Part V (Branches, Refs and
HEAD) — the tag-versus-branch distinction and
git describe’s annotated-only default. - Docker for Production Sysadmins — Part VII (Tagging) — OCI image tags and the parallel argument for pinning by digest rather than by tag name.
- Terraform for Production Sysadmins — Part IX (State) — Terraform state versions tagged in Git as the boundary between committed and uncommitted state.
Quiz
Knowledge check · 4 questions
Q1. What does a lightweight tag store on disk, and what does it not store?
Q2. Two repositories that create an annotated tag with the same tagger, date, message, and signature on the same commit will arrive at the same tag OID.
Q3. Name three fields of an annotated tag's payload that answer audit questions a lightweight tag cannot.
Q4. Diagnose why a release audit fails to identify who tagged the production release, and recommend the corrected discipline.
Six months after a release tagged `v2.4.0`, the security team needs to know who tagged the release and when. The repository used `git tag v2.4.0 <commit>` to create the tag — no `-a`, no `-m`. The reflog of the release clone has expired (>90 days). The audit team has the tag name and the commit OID but no record of the tagging event.
Passing score: 75%. Answers are checked in this browser.