Skip to main content
RunBook Academy

Git, CI/CD & GitOpsIII · Git ObjectsGit Objects

Tag objects — annotated tags, lightweight tags, and signed tags

Intermediate⏱ ~19 mingit

What you'll learn

  • Distinguish an annotated tag object from a lightweight tag ref
  • Read a tag object with git cat-file -p and identify object, type, tag name, tagger
  • Create an annotated tag with git tag -a -m and verify it with git verify-tag
  • Recognise how signed tags embed a GPG signature in the tag payload
  • Explain why lightweight tags cannot be signed and cannot carry a message

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.

Tags are how Git names a release. A lightweight tag is a plain ref pointing at a commit OID; it has no metadata and no signature. An annotated tag is a full object in the content-addressed store, with its own OID, pointing at the target object, naming the target type, carrying a tag name, a tagger, a message, and an optional GPG signature. The distinction matters enormously for production: lightweight tags are convenient names, annotated tags are release identities.

Lightweight tags versus annotated tags

A lightweight tag is just a ref under .git/refs/tags/ that contains the OID of a commit. It has no object of its own, no message, no tagger, and no signature. It is exactly equivalent to a branch that never moves.

An annotated tag is a tag object. The tag object lives in the object store under its own OID, and the ref under .git/refs/tags/ points at that tag OID, which in turn points at the tagged commit (or blob, tree, or another tag). The tag OID is computed from the tag payload, so the tag is content-addressed: a tag with the same payload bytes in any repository has the same OID.

flowchart LR
    L["lightweight tag ref\nrefs/tags/v1.0\ncontents: commit-oid"] --> C1["commit"]
    A["annotated tag ref\nrefs/tags/v1.0\ncontents: tag-oid"] --> T["tag object\nobject: commit-oid\ntype: commit\ntag: v1.0\ntagger: ...\ngpgsig: ..."]
    T --> C2["commit"]

Anatomy of a tag object

A tag object, printed by git cat-file -p <oid>, has this shape:

object <target-oid>
type <target-type>
tag <tag-name>
tagger <name> <<email>> <unix-timestamp> <timezone>

<tag message, free-form, on its own lines>
git cat-file -p "$TAG_OID"
# object 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
#
# Release v1.0.0 — production-ready.
# Signed by the on-call release engineer.

The object line names the OID the tag points at. The type line names the type of that target (commit, tree, blob, or tag). The tag line is the human-readable tag name; it is metadata about the tag, not a ref path. The tagger line has the same shape as in a commit object: name, email, Unix timestamp, timezone.

A signed tag adds a multi-line gpgsig header just like a signed commit:

git cat-file -p "$SIGNED_TAG_OID"
# object 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
# gpgsig -----BEGIN PGP SIGNATURE-----
#          iQEzBAABCgAdFiEE...
#          -----END PGP SIGNATURE-----
#
# Release v1.0.0

The signature covers the tag object minus the gpgsig header itself. Verification reconstructs the signed bytes and runs them through GPG.

Creating annotated and signed tags

The plumbing is the same as the porcelain:

# Annotated tag with a message
git tag -a -m "Release v1.0.0" v1.0.0 "$COMMIT_OID"

# Signed annotated tag (uses the user.signingkey config)
git tag -s -m "Release v1.0.0" v1.0.0 "$COMMIT_OID"

# Lightweight tag (no -a, no -s, no -m)
git tag v1.0.0 "$COMMIT_OID"

The -a flag creates an annotated tag object with a message. The -s flag creates an annotated tag object and signs it with the configured GPG key. Without -a or -s, the tag is lightweight.

After creating an annotated tag, you can inspect it with the same tools used for any other object:

# Show the tag object
git cat-file -p v1.0.0^{}

# Show what the tag points at (dereferencing through the tag)
git cat-file -p v1.0.0^{commit}

The ^{} suffix asks Git to dereference recursively until a non-tag object is reached; the ^{commit} suffix asks Git to dereference until a commit is reached, refusing to proceed if the chain does not end at a commit. Both are essential when reading tags from automation.

Verifying tag signatures

git verify-tag &lt;name&gt; verifies the GPG signature on an annotated tag:

git verify-tag v1.0.0
# gpg: Good signature from "Ops <ops@example.com>" [unknown]
# gpg: WARNING: This key is not certified with a trusted signature!

Exit code 0 means the cryptographic verification passed. The WARNING about trust is a separate question — the signature is valid, but the key itself is not in the local trustdb. In a production CI pipeline, the trust check is performed by configuring a trustdb that includes only the keys the organisation has vetted, and rejecting any tag whose signing key is not in that trustdb.

# Verify multiple tags in one command
git verify-tag v1.0.0 v1.1.0 v2.0.0

Building a tag object by hand

For forensic and fixture workflows, git mktag reads a tag payload from standard input and writes the tag object, printing the tag OID:

printf 'object 8a3f9d2a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7\ntype commit\ntag v1.0.0\ntagger Ops <ops@example.com> 1730000000 +0000\n\nRelease v1.0.0\n' \
    | git mktag
# <tag-oid>

git mktag validates the payload minimally before writing. For most workflows git tag -a -m is sufficient; git mktag is useful when a tag object is being reconstructed from a known byte sequence (for example, when verifying a third party’s claim about a tag’s contents).

Production discipline

  1. Use signed annotated tags for releases. A lightweight tag is a name; a signed annotated tag is a cryptographic commitment. Releases, deployments, and supply-chain attestations must use signed annotated tags.
  2. Verify tags in CI before trusting them. A tag ref can be moved; a tag signature cannot. git verify-tag &lt;name&gt; is the cryptographic check; the exit code is the answer.
  3. Treat the tag OID as the unit of identity. When pinning a release in a pipeline, pin by tag OID (the content-addressed object), not by tag name (the mutable ref). The OID commits to the target, the tagger, the message, and the signature.

Cross-course references

  • Docker for Production Sysadmins - Part XIV (SupplyChain) draws the parallel between signed Git tags and signed OCI image manifests: both are cryptographic commitments to a release identity, both can be verified, and both can be pinned by digest.
  • Terraform for Production Sysadmins - Part IX (State) shows how Terraform state versions are pinned by content hash, the state analogue of an annotated tag OID.
  • Ansible for Production Sysadmins - Part XXXVIII (Review) describes the convention of using Git signed tags to mark Ansible release branches; the tag is the boundary between reviewed and unreleased code.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the key difference between a lightweight tag and an annotated tag?

  2. Q2. An annotated tag carries a GPG signature in a gpgsig header, and git verify-tag validates that signature against the configured GPG trustdb.

  3. Q3. Which git tag command creates a signed annotated tag, and which git command verifies the signature?

  4. Q4. Diagnose whether a release tag is trustworthy as a cryptographic commitment, and what the production pipeline should do if it is not.

    A CI pipeline is about to deploy the artifact built from the commit referenced by the `v1.0.0` tag in the infrastructure repository. The release engineer claims the tag is signed. The pipeline must decide whether to deploy.

Passing score: 75%. Answers are checked in this browser.