Release Labels That Stay Trustworthy with git tag
You will finish with a small, repeatable workflow for creating a release tag, checking what it points to, listing tags by version, and handling a mistaken tag without quietly changing someone else's history. The examples were checked with Git 2.43.0 from Ubuntu package git-man 1:2.43.0-1ubuntu7.3.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need an existing Git repository with at least one commit and permission to read its history. No command in the normal workflow needs sudo. Signing needs a usable GnuPG key, and publishing a tag needs permission to push to the remote.
Checkpoint
If you already have a tag and only need to inspect it, start at step 3. If a tag has already been shared, read the warning in step 6 before using -f.
1. Confirm the repository and the installed command
Run these read-only checks from the repository where the release commit exists:
$ git --version
git version 2.43.0
$ git status --short
$ git log -1 --oneline
4e1b2c3 Prepare the release
Your commit ID and message will differ. An empty status is useful before tagging because it makes it clear that the label will name the committed tree, not uncommitted files. If git status --short prints changes, decide deliberately whether the release should include them. A tag cannot include work that has not been committed.
Git creates tags under refs/tags/. The short name you type must pass Git's reference-name checks, so prefer a predictable form such as v2.4.0. Do not use a name containing spaces or a name that only differs by case when your repository may be used on a case-insensitive filesystem.
2. Create an annotated release tag
For a release, create an annotated tag with a message:
$ git tag --annotate v2.4.0 --message 'Release 2.4.0'
$ git show --no-patch --format=fuller v2.4.0
tag v2.4.0
Tagger: Your Name <[email protected]>
TaggerDate: ...
Release 2.4.0
commit 4e1b2c3...
The tagger identity and object ID are machine-specific, so do not compare those lines literally. The important result is that git show displays an annotation and the commit named by the tag. With -a or --annotate, Git creates a tag object containing the tagger, date and message. The message option avoids an editor, which makes the command safe to repeat in a release script.
If you omit -a, -s, -u, -m and -F, a plain command such as git tag v2.4.0 creates a lightweight tag. That is just a name pointing at an object. It can be useful for a private marker, but annotated tags are the better default for releases because they carry release metadata and are what commands such as git describe normally consider.
By default, the object is HEAD. Make the target explicit when the release commit is not checked out:
$ git tag --annotate v2.4.0 --message 'Release 2.4.0' RELEASE_COMMIT
Replace RELEASE_COMMIT with a real commit ID or other object name. Git refuses to replace an existing tag unless you pass --force, so a duplicate-name error is a useful stop sign rather than a problem to suppress.
3. Inspect tags without changing them
List every tag, or narrow the list with a shell wildcard. Quote the pattern so the shell does not expand it against filenames in the current directory:
$ git tag --list 'v2.*'
v2.3.0
v2.4.0
$ git tag --list --sort='version:refname' 'v*'
v2.3.0
v2.4.0
The default sort is lexicographic unless tag.sort is configured, so lexical order can put v2.10.0 before v2.9.0. version:refname treats matching tag names as versions. If you want newest version first, use --sort='-version:refname'.
Show annotation lines while listing:
$ git tag --list --format='%(refname:short) %(subject)' 'v2.*'
v2.3.0 Release 2.3.0
v2.4.0 Release 2.4.0
$ git tag -n1 v2.4.0
v2.4.0 Release 2.4.0
The --format fields are ref-format fields, not arbitrary shell variables. For a quick target check, compare the tag and commit IDs:
$ git rev-parse v2.4.0^{commit}
4e1b2c3...
$ git show -s --format='%H %s' v2.4.0^{commit}
4e1b2c3... Prepare the release
The ^{commit} suffix peels an annotated tag to the commit it ultimately names. This avoids confusing the tag object's ID with the release commit's ID.
4. Filter tags around a commit
When investigating a release, filter without modifying refs. These forms use HEAD when no commit is supplied:
$ git tag --list --contains HEAD
v2.4.0
$ git tag --list --points-at HEAD
v2.4.0
$ git tag --list --merged HEAD --sort='version:refname'
v2.3.0
v2.4.0
--contains finds tags whose commits contain the selected commit. --points-at is narrower: it finds tags attached directly to the object. --merged finds tags whose commits are reachable from the selected commit. Use --no-contains or --no-merged when you need the inverse. These options imply listing, so adding -l is optional.
5. Verify a signed tag
An annotated tag is not automatically signed. If you need an authenticity check, create a GPG-signed tag with --sign or select a key with --local-user KEY_ID:
$ git tag --sign v2.4.0 --message 'Release 2.4.0'
$ git tag --verify v2.4.0
gpg: Signature made ...
gpg: Good signature from "Release Maintainer ..."
object 4e1b2c3...
type commit
tag v2.4.0
Only use a signed tag when the signing key is configured and recipients know how to validate that key. A local Good signature result says that GnuPG verified the cryptographic signature; it does not by itself prove that the key belongs to the expected maintainer. A signing failure is a reason to stop the release, not to fall back silently to an unsigned tag.
The repository setting user.signingKey can select a default key. The tag.gpgSign setting can require signing, while --no-sign overrides that setting for a command. Treat changes to either setting as security-sensitive configuration and review them separately from a release command.
6. Correct a tag without rewriting trust by accident
Warning
git tag --force replaces an existing local tag. That is a history and release-management change, not a harmless retry.
If the tag has never left your repository, retag the correct commit deliberately:
$ git tag --force --annotate v2.4.0 --message 'Release 2.4.0' CORRECT_COMMIT
$ git rev-parse v2.4.0^{commit}
CORRECT_COMMIT_ID
Replace both placeholders with the intended commit. Before forcing, record the old target so you have an audit trail:
$ git rev-parse v2.4.0^{commit} > /tmp/v2.4.0-old-commit
$ cat /tmp/v2.4.0-old-commit
If the tag was pushed or another person could have fetched it, prefer a new tag name such as v2.4.1. Existing clones should not silently change what v2.4.0 means. If reusing the name is unavoidable, announce the correction, coordinate the change, and tell consumers to delete their old tag and fetch the replacement explicitly. Do not assume a normal pull repairs a changed tag.
Deleting is also a destructive local operation:
$ git tag --delete v2.4.0
Deleted tag 'v2.4.0' (was 4e1b2c3)
There is no special undo command, but the tag can be recreated from the recorded commit and original message. A remote tag needs a separate, coordinated remote deletion or update; local deletion does not alter the remote.
Done means
- The release tag names the intended commit, confirmed with
git rev-parse TAG^{commit}. - You used an annotated tag for release metadata, or consciously chose a lightweight marker.
- Tag listings use a quoted pattern and version-aware sorting where needed.
- A signed tag was verified only when its key identity is trusted, not merely because GnuPG reported a good signature.
- You have not force-replaced a tag that others may already have fetched without an explicit correction plan.