Skip to main content
RunBook Academy

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

Creating and listing tags — git tag, -a, -m, -d, -l with patterns, and git tag -n

Intermediate⏱ ~18 mingit

What you'll learn

  • Create lightweight and annotated tags with git tag, -a, -m, and a target commit
  • Delete local tags with git tag -d and recognise the difference from a remote delete
  • List tags matching a glob pattern with git tag -l and read the output
  • Show the annotation message of one or more tags with git tag -n and -n<num>
  • Choose the correct combination of flags for a release tag in a CI script

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.

The git tag porcelain command is the single entry point for every tag operation: create, delete, list, verify. The flag set is small — -a, -m, -d, -l, -n, -f, -s, -u — but each flag changes the operation’s semantics. The mistakes that show up in production are not exotic; they are forgetting -m (no message), forgetting -a (the tag is lightweight), or running git tag -d against a remote expecting it to remove the remote tag (it does not). This lesson covers the flags you need for day-to-day release work; signing (-s, -u, --verify) is covered in XIX-04.

Creating tags

The git tag command takes a tag name, optionally a target commit, and optionally a set of flags that determine what kind of tag is created:

# Lightweight tag at HEAD
git tag v1.0.0
# Annotated tag at HEAD with a message
git tag -a v1.0.0 -m "Release v1.0.0"
# Annotated tag at a specific commit with a message
git tag -a v1.0.0 "$COMMIT_OID" -m "Release v1.0.0"

The three forms differ in two ways: whether a tag object is created (lightweight versus annotated, covered in XIX-01) and which commit is tagged (HEAD by default, or the commit named as the second positional argument).

# What -a actually does: the tag is an object, not a bare ref
git tag -a v1.0.0 -m "Release v1.0.0"
git cat-file -t v1.0.0
# tag
git cat-file -p v1.0.0
# object 8a3f9d2...
# type commit
# tag v1.0.0
# tagger Ops <ops@example.com> 1730000000 +0000
# 
# Release v1.0.0

# Without -a, the same name produces a lightweight tag
git tag v0.9.0
git cat-file -t v0.9.0
# commit

The -m flag supplies the message inline. Without -m, Git opens the editor and prompts for a message, which is the correct interactive behaviour but is brittle in CI: a script that hits git tag -a v1.0.0 without -m will hang waiting for the editor. Always pass -m in scripts; the message can be a variable built from the changelog or the release notes:

RELEASE_MESSAGE=$(cat release-notes.md)
git tag -a v1.0.0 -m "$RELEASE_MESSAGE" "$COMMIT_OID"

Deleting tags

git tag -d &lt;name&gt; removes a tag from the local ref store. It does not remove the underlying commit and does not propagate to any remote:

# Delete a local tag
git tag -d v1.0.0
# Deleted tag 'v1.0.0' (was 8a3f9d2)

# The remote tag still exists
git ls-remote --tags origin v1.0.0
# 8a3f9d2... refs/tags/v1.0.0

Deleting the remote tag requires an explicit push:

# Delete a remote tag
git push origin --delete v1.0.0
# To git@github.com:example/repo.git
#  - [deleted]         v1.0.0

This is the most common foot-gun in tag work: an engineer deletes a local tag, believes the release is undone, and discovers the next deploy still resolves the remote tag. The two operations are independent; both are required to retire a release marker.

flowchart LR
    L["git tag -d v1.0.0\nremoves local ref"] --> R["remote tag\nstill present"]
    R --> P["git push origin --delete v1.0.0\nremoves remote ref"]

Listing tags

The default git tag invocation lists every tag in the repository, in alphabetical order, one per line. The -l flag takes a glob pattern and filters the list:

# All tags
git tag
# v0.9.0
# v1.0.0
# v1.1.0
# v2.0.0-rc1

# Tags matching v1.*
git tag -l "v1.*"
# v1.0.0
# v1.1.0

# Tags matching v*-rc*
git tag -l "v*-rc*"
# v2.0.0-rc1

The -l flag looks redundant when the argument is * (matches everything), but it is the right form when a script wants to filter: git tag -l always interprets its argument as a pattern, so git tag -l "v2.*" is the correct form and git tag "v2.*" is a syntax error.

git tag -l is also the right command to capture a list of tags into a variable for scripting:

LATEST_V2=$(git tag -l "v2.*" | sort -V | tail -n 1)
echo "Latest v2 release: $LATEST_V2"

The sort -V (version sort) ensures v2.10.0 sorts after v2.9.0; plain sort would put v2.10.0 before v2.9.0 because 1 sorts before 9 lexicographically.

Showing tag annotations with -n

git tag -n prints the annotation message alongside each tag name. The optional &lt;num&gt; argument limits the message to that many lines:

# One-line annotation summary
git tag -n
# v0.9.0   Lightweight placeholder; superseded
# v1.0.0   Release v1.0.0
# v1.1.0   Release v1.1.0 — TLS rotation

# First three lines of each annotation
git tag -n3
# v1.0.0   Release v1.0.0
#          - Add new ingress controller
#          - Bump base image to 1.2.3
# v1.1.0   Release v1.1.0 — TLS rotation
#          - Rotate CA bundle
#          - Pin cert-manager to 1.13

-n is the right command for a quick scan of the recent release history without invoking git show on each tag. The output is column-aligned with the tag name in the first column and the annotation in the second; if a tag has no annotation (lightweight), the second column is empty.

flowchart LR
    A["git tag -n"] --> B["annotated tag\nshows message"]
    A --> C["lightweight tag\nempty column"]

Production discipline

  1. Always pass -a and -m in release scripts. A release tag without -a is lightweight; a release tag without -m hangs on the editor in CI. Both failure modes are avoidable by passing both flags.
  2. Pass the target commit explicitly. git tag -a v1.0.0 -m "..." "$COMMIT_OID" is more robust than git tag -a v1.0.0 -m "...", which tags whatever HEAD resolves to at the moment of the command. The two are equivalent today and may diverge tomorrow if a hook advances HEAD.
  3. Quote patterns in git tag -l "v1.*". Unquoted patterns are shell-expanded against the working directory, producing a pattern that Git never sees.
  4. Treat git tag -d as a local-only operation. The remote tag is not removed. Use git push origin --delete &lt;tag&gt; (or, on older Git, git push origin :refs/tags/ &lt;tag&gt;) to remove the remote tag.

Cross-course references

  • Git, CI/CD & GitOps — Part XIX-01 (Lightweight versus annotated tags) — the on-disk difference the -a flag encodes.
  • Git, CI/CD & GitOps — Part XIX-03 (Tag pushing and fetching)git push --tags and git push --follow-tags as the propagation step after git tag.
  • Git, CI/CD & GitOps — Part XIX-04 (Signed tags)-s and -u as the signing variants of -a.
  • Ansible for Production Sysadmins — Part XXXVII (RepoArch)git tag invocations inside release playbooks, where quoting and -m discipline matters.

Quiz

Knowledge check · 4 questions

  1. Q1. Which `git tag` invocation creates an annotated tag at a specific commit with a message inline?

  2. Q2. `git tag -d v1.0.0` removes the tag from the remote as well as from the local repository.

  3. Q3. What does `git tag -n3` print, and how does its output differ between an annotated tag and a lightweight tag?

  4. Q4. Diagnose a release pipeline that creates lightweight tags because the CI step forgot `-a`, and propose the corrected invocation.

    A CI pipeline runs `git tag v$RUNTIME_VERSION` at the end of a release job, then `git push origin $RUNTIME_VERSION`. The pipeline succeeds. The audit team later asks for the release notes of v3.2.1 and runs `git tag -n v3.2.1` — the output is empty for v3.2.1 but populated for v3.1.7. The team realises the recent releases are lightweight tags because the pipeline never passed `-a` or `-m`.

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