Tagging Releases in Git Is Not Optional in a Real Project

by Arif Ikhsanudin, Backend Developer

The Incident That Tags Prevent

Production is down. You need to know what version is running. You ask the team and get "the latest main" — but main has had four commits since the last deployment. You check the deployment logs and find a timestamp, but not a commit SHA. You check your CI/CD system and find the build number, but can't easily trace it back to the Git commit.

Twenty minutes into the incident, you're still establishing the baseline of "what code is actually running" — which is the information you need before you can meaningfully debug anything.

If you had tagged your releases, this question would be answered in five seconds: look at what tag is deployed, git show v2.3.1 to see the commit and its context, git log v2.3.2..v2.3.4 to see what changed since the last known-good version.

What a Tag Is

A tag in Git is a named reference to a specific commit. Unlike branches, tags don't move — they point to one commit permanently. They're meant to mark specific moments: releases, deployment points, audit checkpoints.

Two types:

Lightweight tags — just a pointer, like a branch that never moves.

git tag v2.3.1
git tag v2.3.1 a3f9d24  # tag a specific commit

Annotated tags — a full Git object with a tagger, date, message, and optionally a GPG signature. This is what you want for releases.

git tag -a v2.3.1 -m "Release v2.3.1: payment retry and fix for duplicate charges"

Annotated tags show up in git describe output, are stored as objects in the object store, and include metadata that lightweight tags don't have. For any tag that marks a release, use annotated tags.

The Tagging Convention

Semantic Versioning (semver.org) is the standard: MAJOR.MINOR.PATCH.

  • PATCH — bug fixes, no API changes (v2.3.1 → v2.3.2)
  • MINOR — new features, backward compatible (v2.3.1 → v2.4.0)
  • MAJOR — breaking changes (v2.3.1 → v3.0.0)

The v prefix is conventional for application releases (v2.3.1). Library releases sometimes omit it (2.3.1) because package managers use the version number directly.

For pre-releases: v2.4.0-rc.1, v2.4.0-beta.2, v2.4.0-alpha.1. These sort correctly and signal stability level to anyone looking at the tag list.

Pushing Tags to the Remote

Tags are not pushed automatically with git push. You have to push them explicitly:

# Push a specific tag
git push origin v2.3.1

# Push all local tags
git push origin --tags

# Push all tags in a single command (if configured)
git push --follow-tags  # only pushes annotated tags

Configure --follow-tags as the default to avoid forgetting:

git config --global push.followTags true

With this setting, git push automatically includes any annotated tags reachable from the pushed commits.

Tags as Deployment Triggers

In a CI/CD pipeline, a tag push is the cleanest trigger for a production deployment:

# GitHub Actions: deploy to production on version tag
on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: |
          echo "Deploying ${GITHUB_REF_NAME} to production"
          ./deploy.sh production ${GITHUB_REF_NAME}

GITHUB_REF_NAME contains the tag name (e.g., v2.3.1). This gives your deployment system the version number automatically — no manual version bumping, no configuration, no ambiguity about what's being deployed.

Generating Changelogs From Tags

With proper tagging, generating a changelog between releases is trivial:

# Commits between two tags
git log v2.3.0..v2.3.1 --oneline

# Full diff between two releases
git diff v2.3.0..v2.3.1

# All tags, with dates
git tag -l --sort=-version:refname \
  --format='%(refname:short) %(taggerdate:short)'

With Conventional Commits, tools like git-cliff, conventional-changelog, or release-please can generate formatted changelogs automatically from the commit messages between tags. The convention investment pays off in automated release notes.

Hotfix Tagging

When you cherry pick a hotfix to an older release:

# You're on release/2.3 branch, which tracks the v2.3.x line
git checkout release/2.3
git cherry-pick <hotfix-sha>
git tag -a v2.3.2 -m "Hotfix: fix critical payment processing error"
git push origin release/2.3 --follow-tags

The tag clearly marks what the hotfix release contains and when it was made. Anyone querying "what's the difference between v2.3.1 and v2.3.2?" gets a precise answer.

Finding What's Deployed With git describe

If your build process embeds the Git description in the artifact:

git describe --tags
# v2.3.1-4-ga3f9d24
# v2.3.1: last tag
# 4: commits since that tag
# g: "g" prefix for git
# a3f9d24: SHA of current commit

This string uniquely identifies any built artifact relative to the tag history. A build from exactly v2.3.1 shows v2.3.1. A build from four commits after that shows v2.3.1-4-ga3f9d24. Embed this in your service's /health or /version endpoint and you can always answer "what exactly is running" without consulting deployment logs.

Tags are two minutes of work per release. The cost of not having them is paid in every incident that starts with "what's actually deployed right now?"

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

Java Memory Leaks in Practice — How They Form and How to Find Them

Java memory leaks are not about forgetting to free memory — the GC handles that. They are about holding references longer than necessary. Here are the specific patterns that cause them and the tooling that finds them.

Read more

Red Flags That Predict Software Project Failure

“It’s probably fine… we just need a bit more time.” That sentence has quietly preceded more failed projects than anyone admits.

Read more

Ruby Modules and Mixins — Composition Over Inheritance in Practice

Inheritance hierarchies in Ruby tend to collapse under their own weight. Modules give you a way out, but only if you understand method lookup, hook methods, and where the pattern breaks down.

Read more

A Good API Is One Developers Never Have to Ask Questions About

APIs fail when they require interpretation instead of execution. The best APIs eliminate ambiguity through consistent design, predictable behavior, and self-evident contracts.

Read more