Skip to main content

Tags โ€” Marking Releases

What is a Tag?โ€‹

A tag is an immutable pointer to a specific commit โ€” unlike a branch, it never moves. Tags are used to mark release points (v1.2.0), milestones, or any commit you want to reference permanently.

There are two types of tags:

TypeDescription
LightweightJust a pointer to a commit โ€” like a branch that never moves
AnnotatedA full Git object with a message, tagger name, email, date, and optional GPG signature. Recommended for releases.

Creating Tagsโ€‹

git tag -a v1.2.0 -m "Release 1.2.0 โ€” transaction export feature"

Lightweight tag (simple bookmark)โ€‹

git tag v1.2.0-rc1

Tag a specific past commitโ€‹

git tag -a v1.1.5 a3f9bc2 -m "Retroactive tag for 1.1.5 hotfix"

Listing and Inspecting Tagsโ€‹

# List all tags
git tag

# List tags matching a pattern
git tag -l "v1.2.*"
git tag -l "v1.*"

# Show tag details (annotated tags show the full object)
git show v1.2.0

# List tags with their commit messages (annotated only)
git tag -n # show first line of each tag message
git tag -n5 # show up to 5 lines

Pushing Tags to Remoteโ€‹

Tags are not pushed automatically with git push:

# Push a specific tag
git push origin v1.2.0

# Push all tags
git push --tags

# Push all annotated tags only (recommended โ€” excludes lightweight tags)
git push origin 'refs/tags/v*'

Deleting Tagsโ€‹

# Delete a local tag
git tag -d v1.2.0-rc1

# Delete a remote tag
git push origin --delete v1.2.0-rc1
# or:
git push origin :refs/tags/v1.2.0-rc1

Checking Out a Tagโ€‹

Tags are not branches โ€” you cannot commit on them. Checking out a tag puts you in detached HEAD state:

git checkout v1.2.0
# HEAD is now at a3f9bc2... (detached HEAD)

# To work from a tag, create a branch from it
git switch -c hotfix/JIRA-999 v1.2.0

Semantic Versioningโ€‹

Follow Semantic Versioning for release tags:

v<MAJOR>.<MINOR>.<PATCH>[-<pre-release>][+<build>]

v1.0.0 โ† initial stable release
v1.1.0 โ† new feature, backward compatible
v1.1.1 โ† bug fix
v1.2.0-rc.1 โ† release candidate
v2.0.0 โ† breaking changes
SegmentIncrement when
MAJORBreaking changes โ€” not backward compatible
MINORNew features, backward compatible
PATCHBug fixes, backward compatible

Signed Tags (GPG)โ€‹

For high-security release pipelines, sign tags with GPG:

# Create a signed tag
git tag -s v1.2.0 -m "Release 1.2.0"

# Verify a signed tag
git tag -v v1.2.0

Release Automation with Tagsโ€‹

Use tags to trigger CI/CD release pipelines:

# .github/workflows/release.yml
name: Release

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+' # matches v1.2.0, v2.0.0, etc.

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Get tag version
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT

- name: Build and publish
run: |
mvn versions:set -DnewVersion=${{ steps.version.outputs.VERSION }}
mvn deploy -B

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
name: Release ${{ steps.version.outputs.VERSION }}
generate_release_notes: true

Tag on Main, Never on Feature Branches

Always create release tags on main (or your release branch) after merging and verifying the release build. Tagging on a feature branch creates a tag that points to an unmerged commit, which is confusing and can lead to incorrect releases.

Interview Questions (Senior Level)โ€‹

  1. How do you define a release tagging policy that supports rollback, audit, and SBOM traceability?
  2. When do you require signed tags, and how do you operationalize key rotation?
  3. How would you prevent accidental mutable-release behavior from retagging mistakes?
  4. What is your strategy for prerelease vs stable tags in automated deployment pipelines?

Short answer guide:

  • Use immutable semver tags tied to build metadata and release notes.
  • Enforce signed tags for production releases and rotate trust chains periodically.
  • Protect tag namespaces and disallow force updates to release tags.
  • Separate prerelease channels and promotion gates clearly.