Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXIX · Tags and ReleasesTags and Releases

Lightweight versus annotated tags — what each stores and why the audit trail differs

Intermediate⏱ ~18 mingit

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

Not yet marked complete on this device.

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 is commit; tags can also point at blobs or trees (rarely used in production).
  • tag — the tag’s human name. This is the name that refs/tags/v1.0.0 resolves to in git 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 &lt;ops@example.com&gt; 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 show against 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 object field.
  • What was the target type? The type field.
  • Who tagged it? The tagger field.
  • When was it tagged? The tagger field’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

  1. 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.
  2. 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 tag but behave completely differently in git verify-tag and git describe.
  3. 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.
  4. Forbid lightweight release tags in CI configuration. If the CI pipeline runs git tag, the only tag command in the pipeline should be git tag -a -m ... or git tag -s -m .... A git tag &lt;name&gt; 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

  1. Q1. What does a lightweight tag store on disk, and what does it not store?

  2. 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.

  3. Q3. Name three fields of an annotated tag's payload that answer audit questions a lightweight tag cannot.

  4. 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 &lt;commit&gt;` 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.