Sign in
← Git: workflows
9 min · easy · Lesson 2 of 3

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

What you'll need for the tasks

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.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson git-workflow-02-tags-releases