How-to

Git tags: how to use git tag and version your releases

Updated: · Skillaria.Top

git tag puts a permanent label on a specific commit, usually to mark a release such as v1.2.0. You create an annotated tag with git tag -a v1.2.0 -m "message", and tags are pushed to the remote separately: git push origin v1.2.0 or git push --tags.

git tag is a named label on a specific commit. Unlike a branch, a tag doesn't move when new commits arrive: a year from now, v1.2.0 will still point to the same snapshot of the code. That's why tags are used for releases, and CI/CD systems often trigger builds and publishing precisely when a new tag appears.

Lightweight vs. annotated tags

Git has two kinds of tags, and the difference matters.

LightweightAnnotated
How to creategit tag v1.0.0git tag -a v1.0.0 -m "..."
What it isjust a pointer to a commita separate object in the Git database
Author, date, messagenoyes
GPG/SSH signaturenopossible (-s)
Used by git describe by defaultnoyes
When to useprivate, temporary markersreleases and anything you publish

Use annotated tags for releases: they record who shipped the version and when, and they can be signed.

How to create a tag

On the current commit

git tag -a v1.2.0 -m "Release 1.2.0: report export, fixed API timeout"

Without -m, Git opens an editor for the message — handy if you want to write out a list of changes.

On an older commit

Forgot to tag the release? No problem — you can tag any commit:

git log --oneline -5
# 7be41d0 feat: report export
# 1c02e8f fix: request timeout
git tag -a v1.1.0 1c02e8f -m "Release 1.1.0"

Signed tags

git tag -s v1.2.0 -m "Release 1.2.0"
git tag -v v1.2.0     # verify the signature

You need a configured GPG key (or an SSH key with gpg.format=ssh). GitHub and GitLab show such tags with a "Verified" badge.

How to list tags

git tag                       # all tags, alphabetically
git tag -l "v1.*"             # by pattern
git tag -n                    # with the first line of the message
git tag --sort=-v:refname     # by version, newest first
git show v1.2.0               # tag details and the commit
git describe --tags           # nearest tag: v1.2.0-3-g7be41d0

Watch out for sorting: by default, v1.10.0 comes before v1.9.0 because strings are compared character by character. The --sort=v:refname option sorts them as versions.

Read the git describe output like this: three commits after tag v1.2.0, current commit 7be41d0 (the g stands for git). It's a convenient source of version numbers for builds between releases.

How to push a tag to the remote

A plain git push doesn't push tags — that's the usual answer to "why isn't my tag on GitHub?"

git push origin v1.2.0        # a single tag
git push --tags               # all local tags
git push --follow-tags        # commits + annotated tags pointing at them

--follow-tags is safer than --tags: it won't push stray lightweight tags you created for yourself. You can make it the default: git config --global push.followTags true.

To fetch tags from the remote: git fetch --tags. A regular git fetch also brings in tags that point to the commits being downloaded.

How to delete or move a tag

Deleting

git tag -d v1.2.0                     # locally
git push origin --delete v1.2.0       # on the remote
# older equivalent: git push origin :refs/tags/v1.2.0

Moving a tag to another commit

git tag -a -f v1.2.0 <new-hash> -m "Release 1.2.0"
git push origin -f v1.2.0

It's technically possible, but in public projects you shouldn't move a published tag. Anyone who already fetched it won't get the update automatically: Git doesn't overwrite existing local tags on fetch without --force. You end up with "v1.2.0" meaning different code for different people. The right move is to release v1.2.1.

How to see which commits went into a release

Tags work well as range boundaries in every history command:

git log v1.1.0..v1.2.0 --oneline        # commits between releases
git diff v1.1.0 v1.2.0 --stat           # which files changed
git tag --contains 1c02e8f              # which releases include this commit
git merge-base --is-ancestor 1c02e8f v1.2.0 && echo "in the release"

The last two commands answer a common support question: "Has the fix reached users yet?" If the fix only landed in main and the release was built earlier, git tag --contains returns an empty list, and it's clear you need a new tag.

Another trick is to grab the source of a specific version without history: git archive --format=zip -o app-1.2.0.zip v1.2.0. That's how archives are prepared for delivery to a client or for builds in an isolated environment.

How to check out a version by tag

git switch --detach v1.1.0
# HEAD is now at 1c02e8f fix: request timeout

You'll be in a detached HEAD state: you can browse and build, but new commits won't belong to any branch. If you need to fix an old version, create a branch from the tag:

git switch -c hotfix/1.1.x v1.1.0
# changes, commit
git tag -a v1.1.1 -m "Security fix"
git push origin hotfix/1.1.x --follow-tags

Versioning releases: SemVer

The most common tag naming scheme is Semantic Versioning: MAJOR.MINOR.PATCH.

  • MAJOR goes up with incompatible API changes: 1.4.2 → 2.0.0.
  • MINOR — new backward-compatible functionality: 1.4.2 → 1.5.0.
  • PATCH — bug fixes: 1.4.2 → 1.4.3.
  • Pre-releases get a suffix: 2.0.0-beta.1, 2.0.0-rc.1.

The v prefix (v1.4.2) is a common convention for tag names; SemVer itself doesn't require it. What matters is picking one format and sticking to it, or scripts that look for the latest release will break.

Tags in the release process

A typical team process looks like this:

  1. Changes are merged into main and the tests are green.
  2. The CHANGELOG is updated, along with the version number in project files if it lives there.
  3. An annotated tag is created: git tag -a v1.5.0 -m "Release 1.5.0".
  4. git push origin main --follow-tags.
  5. CI sees the new tag, builds the artifacts and publishes the release; on GitHub and GitLab a Release page with notes and files is created from the tag.

This way any version in production maps unambiguously to code: the tag is in the build name, and git show tells you what went into it. And git log v1.4.0..v1.5.0 --oneline gives you the list of changes between releases for your release notes.

Release notes from tags

If your commit messages are meaningful, tags turn history into a ready-made changelog. You can find the latest and previous releases without searching by hand:

LAST=$(git describe --tags --abbrev=0)
PREV=$(git describe --tags --abbrev=0 "$LAST^")
git log "$PREV..$LAST" --no-merges --pretty=format:"- %s (%h)"
# - feat: report export (7be41d0)
# - fix: request timeout (1c02e8f)

If the team follows Conventional Commits (feat:, fix:, feat!:), the next version number can be calculated automatically: fix bumps PATCH, feat bumps MINOR, and an exclamation mark or BREAKING CHANGE bumps MAJOR. Popular automated release tools are built on this: they create the tag and write the CHANGELOG themselves.

Tags and CI/CD

Pipelines usually distinguish two events: a push to a branch and a new tag. A branch push runs tests and builds a staging environment; a tag triggers publishing. In GitHub Actions it looks like this:

on:
  push:
    tags:
      - "v*.*.*"

In GitLab CI, the CI_COMMIT_TAG variable and a rules: - if: $CI_COMMIT_TAG rule play the same role. Inside the pipeline, the tag name becomes the version of the Docker image or package, so the tag format has to be strict and predictable.

That's one more reason not to move tags: if the pipeline already published package 1.2.0 to a registry, republishing with the same number is usually forbidden, and users will keep the old image tagged 1.2.0.

Tag naming: what's allowed

Tag names follow the same rules as branch names: no spaces, none of the characters ~ ^ : ? * [, and no two consecutive dots. Slashes are allowed, so in monorepos you'll see tags like api/v1.2.0 and web/v3.0.1 — one version series per package. You can validate a name in advance with git check-ref-format "refs/tags/v1.2.0": it returns a non-zero exit code if the name is invalid.

Protecting tags

In team repositories, release tags are worth protecting: GitHub and GitLab let you block creating, overwriting and deleting tags matching a pattern (for example, v*) for everyone except maintainers. That way a stray git push --tags from someone's machine won't publish an unplanned release, and a mistaken git push origin --delete won't wipe out version history.

Common mistakes

  • The tag isn't on the remote. You forgot git push origin <tag> or --follow-tags.
  • A tag with the same name as a branch. Git warns refname is ambiguous; use a v prefix for tags and keep names distinct.
  • Tagging uncommitted work. A tag points to a commit, not to files: uncommitted changes won't make it into the release.
  • A lightweight tag for a release. No author, date or message, and git describe ignores it by default.

Summary

git tag marks an immutable point in history: create annotated tags for releases with git tag -a, name them according to SemVer, and push them explicitly — git push origin <tag> or --follow-tags. Don't move published tags; ship a new version instead. Tags, along with rebase, stash and cherry-pick, are covered hands-on in the Git course.

FAQ

What's the difference between a tag and a branch?

A branch automatically moves forward with every new commit, while a tag always points to the same commit.

Can a single commit have two tags?

Yes, there's no limit on the number of tags per commit, for example v2.0.0 and latest-stable.

Are tags deleted when I delete a branch?

No, tags are independent of branches, and a tagged commit stays reachable even after the branch is deleted.

How do I find out which release a commit is in?

Run git tag --contains &lt;hash&gt; — it lists every tag whose history includes that commit.

How do I get the latest tag in a script?

Use git describe --tags --abbrev=0 — it prints the name of the nearest tag without the hash suffix.

Learn this topic on video

This topic is covered in depth with practice in the course “Git from Scratch to Confident Developer”.

Go to course