Tagging Releases in Git Is Not Optional in a Real Project

by Eric Hanson, Backend Developer at Clean Systems Consulting

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

Service Objects in Ruby — How I Structure Business Logic

Service objects are the most argued-about pattern in Rails codebases and the least defined. Here is a concrete structure that handles initialization, result signaling, and error propagation without pulling in a framework.

Read more

Who Is Responsible for Auth in a Microservices Architecture

Auth responsibility in microservices is frequently undefined, duplicated across teams, or delegated to the wrong layer. The ambiguity is not an org problem — it is an architecture problem with a clear solution.

Read more

How to Measure Your Skills Without a Manager or HR

No performance review, no feedback cycle, no promotion ladder. So… how do you know if you’re actually getting better?

Read more

Why Chicago Startups Are Rethinking the Full-Time Backend Hire and Winning With Async Contractors

Some Chicago startups have stopped competing for senior backend engineers in a market that favors their biggest competitors. Here's what they're doing instead.

Read more