Tags and releases in Git
Lightweight vs annotated tags, how to tag a version, push a tag, and what a release is built from.
Why mark commits
A branch is a moving pointer: main points to one commit today, another tomorrow. That doesn't work
for a release. You need a fixed marker on a specific commit, so that a year from now you know exactly
what version 2.4.1 was built from. That's the job of a tag.
A tag is placed on a commit once and isn't moved after it's published. By convention, a version tag
starts with v: v2.4.1.
Lightweight and annotated
Tags come in two kinds.
Lightweight is just a name for a commit, with no metadata:
git tag v2.4.1
Annotated is a separate object in the repository with the tag's author, date and message:
git tag -a v2.4.1 -m "release 2.4.1"
Releases use annotated tags: you can see who released the version and when, and the tag can be
signed with GPG (git tag -s). Lightweight tags are left for temporary personal markers like
before-refactor.
Without naming a commit, the tag lands on the current HEAD. You can place it on a different commit
by adding the hash or branch name at the end: git tag -a v2.4.1 -m "release 2.4.1" 3f2c1ab.
To see what's inside:
git show v2.4.1
tag v2.4.1
Tagger: Ivan <ivan@example.com>
Date: Thu Jun 18 12:00:00 2026 +0300
release 2.4.1
commit 5198f42fae81c5057efd3b2a0597cd21769de0ec
Author: Ivan <ivan@example.com>
...
The tag / Tagger block only exists for an annotated tag. For a lightweight one, git show prints
the commit right away. Notice that the Tagger and the commit's Author can be different people. One
developer wrote the commit, another released it.
Two more ways to tell tag kinds apart:
git cat-file -t v2.4.1
tag
For a lightweight tag, the same command answers commit: the name points straight at the commit. And
git tag -n prints the first line of the message next to each tag. A lightweight tag has no message,
so you'll see the commit's title there instead.
Which release a commit landed in
A common question from support: "the cache bug is fixed, which version?" --contains gives the
answer:
git tag --contains 3f2c1ab
The command lists the tags from which commit 3f2c1ab is reachable through history, meaning every
release that has it. The first of them by version number is your answer.
Commit dates won't help here. A fix might be written on a branch before the release and merged into
main after it. The commit's author date stays old, but it only makes it into the next release.
Tags don't travel with a regular push
Important point: git push sends commits, but not tags. A tag has to be pushed explicitly:
git push origin v2.4.1
git push origin --tags
The first command pushes one tag, the second pushes all tags at once. Be careful with --tags: it
will also push lightweight personal markers that nobody on the server needs. There's also a
--follow-tags flag: along with the commits, it pushes the annotated tags that point to them, and
leaves lightweight ones at home.
A common mistake: the version was released locally, but it's not on the server because you forgot to push the tag. You can check what's on the server like this:
git ls-remote --tags origin
Lines ending in ^{} belong to annotated tags: they show the commit the tag points to.
The other direction is simpler. A plain git fetch already downloads tags that point to commits from
the branches you fetched. git fetch --tags is for grabbing every tag on the server, including those
that sit on commits outside your branches.
If you tagged the wrong place
As long as the tag isn't pushed, you can safely recreate it: nobody but you has seen it.
git tag -d v2.5.0
Then create the tag again on the right commit. git tag -f does the same thing.
Once published, a tag isn't moved. Your colleagues have already fetched it, an artifact is built from it, and Git on the server will reject a push of a tag with the same name but different content:
! [rejected] v2.5.0 -> v2.5.0 (already exists)
hint: Updates were rejected because the tag already exists in the remote.
If a published release turns out to be wrong, you ship the next version. The git tag documentation
covers this in detail in the "On Re-tagging" section.
A release is built from a tag
On GitLab and GitHub, a release is built on top of a tag: a page with the version's description, a changelog and attached artifacts (built binaries, archives).
CI usually automates this: pushing a v* tag triggers a pipeline that builds the artifact, runs the
tests and publishes the release. That ties the version number, the commit, the built image and the
release page tightly together through one tag.
# fragment of .gitlab-ci.yml
build-release:
rules:
- if: $CI_COMMIT_TAG =~ /^v/
script:
- docker build -t "myapp:$CI_COMMIT_TAG" .
CI_COMMIT_TAG is a built-in variable holding the tag's name; GitLab sets it only when the pipeline
runs for a tag. The rule =~ /^v/ passes tags that start with v. A tag 2.5.0 with no prefix won't
trigger this pipeline.
Often CI also checks, before building, that the version in the code matches the tag: for example, the
VERSION file in the commit must contain 2.5.0 for the tag v2.5.0. If the tag was placed before
the commit that bumps the version, that check won't let it through.
Common mistakes
- Forgot to push the tag.
git pushansweredEverything up-to-date, and there's no release on the server. Tags are pushed separately:git push origin <tag>. - Pushed junk through
--tags. Personal markers rode along with the release to the server. Delete the extra tag from the server:git push origin --delete <tag>. - Lightweight tag instead of annotated. Forgot
-a, and the release has no author, date or message. To check:git cat-file -t <tag>answerscommit, nottag. - Tag on the wrong commit. Tagged it, then committed the version bump into
VERSIONor the changelog afterward. As long as the tag isn't pushed, recreate it. After pushing, ship a new version. - Looking for a release by dates. A commit dated before the release might not have made it in.
Check
git tag --contains <commit>.
What you'll need for the tasks
git tag,git tag -n,git log --oneline --decorate;git show <tag>andgit cat-file -t <tag>: who released a version and which tag is lightweight;git tag --contains <commit>and finding a commit by its message:git log --oneline --all --grep=<word>;git tag -a <tag> -m "<message>" [<commit>],git tag -d <tag>;git push origin <tag>,git ls-remote --tags origin,git push origin --delete <tag>;- the
-C <folder>flag: run a Git command on a repository from a different folder.
In the following tracks, tags come up again in CI/CD and when rolling out images to a registry.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson git-workflow-02-tags-releases
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress