Tags & Releases
A tag in Git is a permanent name you pin to one specific commit so you can find it again later. While branches keep moving as you add commits, a tag stays frozen on the exact commit it was created on. That makes tags perfect for marking releases — the precise snapshot of your code that you shipped to users. In DevOps this is huge, because your CI/CD pipeline (the automated system that builds and deploys your code) can watch for new tags and ship that exact version automatically.
Lightweight vs annotated tags
Git has two kinds of tags. Knowing the difference matters because one is barely more than a sticky note and the other is a real, traceable record.
A lightweight tag is just a name pointing at a commit. It stores nothing else — no author, no date, no message.
An annotated tag is a full object stored in the Git database. It records who made the tag, when, and a message, and it can be signed with a GPG key (a cryptographic signature that proves you really made it).
| Feature | Lightweight tag | Annotated tag |
|---|---|---|
| Stores author and date | No | Yes |
| Has a message | No | Yes |
| Can be GPG-signed | No | Yes |
| Good for releases | No | Yes |
| Good for quick private bookmarks | Yes | Not needed |
When to use which: Always use annotated tags for anything that leaves your machine — releases, deploys, anything CI will react to. Use a lightweight tag only as a throwaway bookmark for yourself.
Gotcha: Lightweight tags are easy to create by accident (just
git tag namewith no flags). They have no metadata, so months later you cannot tell who made the release or why. Stick to-afor releases.
Semantic versioning
Before you tag, decide what to call the version. The industry standard is Semantic Versioning (SemVer). The version is three numbers: MAJOR.MINOR.PATCH, for example 1.4.2.
- MAJOR — increase when you make a breaking change that forces users to change their code.
- MINOR — increase when you add a new feature in a backward-compatible way.
- PATCH — increase when you fix a bug without changing behavior.
Release tags almost always start with a lowercase v, like v1.0.0. The v is a long-standing convention that makes tags easy to spot and filter.
| Change you made | Old version | New version |
|---|---|---|
| Fixed a bug | v1.4.2 | v1.4.3 |
| Added a feature | v1.4.2 | v1.5.0 |
| Broke the API | v1.4.2 | v2.0.0 |
Creating and pushing a tag
Let’s tag the current commit as your first release, v1.0.0. The -a flag makes it annotated and -m supplies the message.
git tag -a v1.0.0 -m "First stable release"
This command prints nothing on success. To confirm the tag exists, list your tags:
git tag
Output:
v1.0.0
To see the full details of an annotated tag — the message, author, date, and the commit it points to — use git show:
git show v1.0.0
Output:
tag v1.0.0
Tagger: Alex Doe <[email protected]>
Date: Mon Jun 15 10:22:31 2026 +0000
First stable release
commit 4f9a2c1e8b7d3a6f0c5e9b1d2a3f4c6e8d7b9a0c
Author: Alex Doe <[email protected]>
Date: Mon Jun 15 10:20:04 2026 +0000
Finalize release notes
Pushing tags to the remote
Here is the part that trips up almost everyone: git push does NOT push tags. A normal push only sends branch commits. You have to push the tag explicitly.
To push one specific tag:
git push origin v1.0.0
Output:
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 178 bytes | 178.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To github.com:alex/myapp.git
* [new tag] v1.0.0 -> v1.0.0
To push every local tag at once:
git push origin --tags
Tagging an older commit
You don’t have to tag the latest commit. If you forgot to tag a release, find the commit hash with git log --oneline and pass it at the end:
git tag -a v0.9.0 4f9a2c1 -m "Beta release"
git push origin v0.9.0
Deleting a tag
If you tagged the wrong commit, delete it locally and on the remote:
git tag -d v1.0.0
git push origin --delete v1.0.0
Warning: Never re-use a tag name after others have pulled it. If someone already downloaded
v1.0.0, moving the tag to a different commit means two people now have different code under the same version. Always bump to a new version instead.
How CI triggers on tags
This is where tags pay off. Most CI/CD platforms (the servers that build and deploy your code, like GitHub Actions or GitLab CI) can run a pipeline only when a tag is pushed. So your release process becomes: push a tag, walk away, and the build, test, and deploy happen automatically.
Here is a GitHub Actions workflow that runs only when you push a tag starting with v. Save it as .github/workflows/release.yml:
name: Release
on:
push:
tags:
- "v*"
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Build the project
run: npm ci && npm run build
- name: Create a GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
The tags: - "v*" part is the key. The pipeline ignores normal commits and pushes; it only wakes up for tags like v1.0.0 or v2.3.1. The softprops/action-gh-release step then turns that tag into a published GitHub Release with auto-generated notes.
On your Ubuntu server or CI runner you can also tag directly from a script. For example, a simple release helper:
#!/usr/bin/env bash
set -e
VERSION="$1"
git tag -a "v${VERSION}" -m "Release v${VERSION}"
git push origin "v${VERSION}"
echo "Tagged and pushed v${VERSION} — CI will take it from here."
Run it like ./release.sh 1.2.0, and your tag-triggered pipeline does the rest.
Best practices
- Always use annotated tags (
-a) for releases so authorship and a message are recorded. - Follow SemVer (
MAJOR.MINOR.PATCH) and prefix release tags withv. - Remember tags are not pushed by
git push— push them explicitly withgit push origin <tag>or--tags. - Never move or re-use a published tag; bump to a new version instead.
- Let CI trigger on tags so releases are automatic, repeatable, and hands-off.
- Sign release tags with GPG (
git tag -s) when you need to prove a release is genuinely yours. - Keep a
CHANGELOGor use auto-generated release notes so each tag explains what changed.