Skip to content
Home
Git Tags & Releases: Versioning, Signing & Automation

Git Tags & Releases: Versioning, Signing & Automation

Git Git 9 min read 1743 words Intermediate ExcellentWiki Editorial Team

Git tags mark specific points in a repository’s history as important. They are most commonly used for release versions (v1.0, v2.3.1), but also serve for marking milestone commits, deployment checkpoints, and historical reference points. Unlike branches, tags are immutable — they point to a single commit and should never be moved. This immutability makes them the foundation of reproducible software releases. According to GitHub’s platform data, over 40 million tagged releases exist across public repositories, with semantic versioning patterns being the most common naming convention.

Annotated vs Lightweight Tags

Git supports two types of tags with different features and use cases:

FeatureAnnotatedLightweight
Stores author, date, messageYesNo
GPG-signableYesNo
Git object in databaseFull objectJust a pointer
Recommended for releasesYesNo
Use casePublic releases, milestonesPrivate markers, temporary notes

Creating Tags

# Lightweight tag (just a pointer — no metadata)
git tag v1.0.0

# Annotated tag (recommended for releases)
git tag -a v1.0.0 -m "Release version 1.0.0"

# Annotated tag with editor
git tag -a v1.0.0

Always use annotated tags for public releases. They store the tagger’s name, email, date, and a message — essential metadata for understanding what a release contains and who created it. Lightweight tags are fine for personal bookmarks like “before-refactor” or “checkpoint-before-migration.”

Tagging Previous Commits

# Tag a specific commit by hash
git tag -a v1.0.0 9fceb02 -m "Release 1.0.0"

# Tag a commit relative to HEAD
git tag -a v1.0.0 HEAD~3 -m "Release 1.0.0"

This is essential when you forgot to tag a release at the time of merge. Find the commit hash with git log --oneline and tag it retroactively. The tag date records when the tag was created, not when the commit was made, which is usually acceptable.

Listing and Viewing Tags

# List all tags alphabetically
git tag

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

# Search tags with wildcard
git tag -l "*beta*"

# Show tag details (includes commit info)
git show v1.0.0

# Show tags with their annotation messages
git tag -n

Pattern matching is especially useful in repositories with many tags. Using git tag -l "v2.*" quickly filters to major version releases.

Pushing Tags to Remote

Git does not push tags by default with git push. You must push them explicitly:

# Push a specific tag
git push origin v1.0.0

# Push all tags (including lightweight)
git push origin --tags

# Push only annotated tags that point to pushed commits
git push origin --follow-tags

The --follow-tags flag is the safest option for release workflows. It pushes only annotated tags whose commits are being pushed, preventing accidental publication of stray lightweight tags.

Deleting Tags

# Delete local tag
git tag -d v1.0.0

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

Deleting tags in public repositories is strongly discouraged. Anyone who has already fetched the tag will retain it, and reuse of the same tag name for a different commit will cause confusion. Tag immutability is a social contract in most projects. If you must replace a tag, communicate clearly with all consumers.

Semantic Versioning with Tags

Most projects use semantic versioning (semver) for tags:

vMAJOR.MINOR.PATCH

v1.0.0     # First stable release
v1.2.0     # New features (backward compatible)
v1.2.1     # Bug fixes
v2.0.0     # Breaking changes

Pre-release suffixes:
v1.0.0-alpha.1
v1.0.0-beta.2
v1.0.0-rc.1

Semver provides a structured way to communicate the nature of changes through the version number. The specification, documented at semver.org, defines exactly when to increment each component. Tools like semver and standard-version automate version bumping and changelog generation based on conventional commits.

Release Branch Workflow

# Create release branch from main
git checkout -b release/v1.0.0 main

# Finalize with version bump commit
git commit -m "chore: bump version to 1.0.0"

# Tag the release
git tag -a v1.0.0 -m "Release v1.0.0"

# Merge back to main
git checkout main
git merge release/v1.0.0

# Push everything including tags
git push origin main --follow-tags

Version Bump Automation

Automate version determination using conventional commits:

# Determine the next version based on commits since the last tag
# Uses conventional commit analysis
npx standard-version --dry-run

Standard-version analyzes commit messages since the last tag, determines the appropriate semver bump (major for BREAKING CHANGE, minor for feat, patch for fix), updates version files, generates a changelog section, and creates a signed annotated tag automatically.

GPG-Signing Tags

Signed tags provide cryptographic verification that the tag was created by a trusted identity:

# Configure signing key
git config --global user.signingkey 3AA5C34371567BD2

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

# Verify a tag's signature
git tag -v v1.0.0

# Auto-sign all tags
git config --global tag.gpgSign true

GitHub displays a “Verified” badge on signed tags when the signing key is uploaded to the user’s GitHub account. This badge proves the tag was created by you and has not been tampered with — essential for open-source projects where users need to trust that releases are authentic. The 2024 supply chain security report found that only 12% of open-source releases use signed tags, representing a significant opportunity for security improvement across the ecosystem.

SSH Signing as an Alternative

Git 2.34+ supports SSH-based signing as an alternative to GPG:

# Configure SSH signing
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub

# Create an SSH-signed tag
git tag -s v1.0.0 -m "Release v1.0.0"

SSH signing is simpler for developers who already manage SSH keys, avoiding GPG’s complexity around key management and expiration.

GitHub Releases

GitHub Releases build on top of tags, adding release notes, binary assets, changelogs, and a dedicated landing page:

# Create a release from an existing tag
gh release create v1.0.0 \
  --title "v1.0.0" \
  --notes "First stable release" \
  --target main

# With release notes from a file
gh release create v1.0.0 \
  --notes-file CHANGELOG.md

# Attach binaries to the release
gh release create v1.0.0 \
  dist/*.tar.gz \
  dist/*.deb

# Create a draft (not published yet)
gh release create v1.0.0 --draft

# Create a prerelease
gh release create v1.0.0-rc.1 --prerelease

Automating Releases with CI

name: Create Release
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build
        run: make build
      - name: Create Release
        run: |
          gh release create ${{ github.ref_name }} \
            dist/*.tar.gz \
            --title "${{ github.ref_name }}" \
            --notes-file CHANGELOG.md
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This workflow automatically triggers on any tag matching v*, builds the project, and creates a GitHub Release with built artifacts attached.

Automated Changelog Generation

Integrate changelog generation with your tagging workflow for consistent release documentation.

Generating Changelogs from Conventional Commits

# Generate changelog since the last tag
git log $(git describe --tags --abbrev=0)..HEAD --oneline

Git-Cliff for Customizable Changelogs

# cliff.toml
[changelog]
header = "# Changelog\n"
body = "{% if version %}\n  ## {{ version }} - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% endif %}\n{% for group, commits in commits | group_by(attribute=\"group\") %}\n  ### {{ group | upper_first }}\n  {% for commit in commits %}\n    - {{ commit.message | upper_first }}\n  {% endfor %}\n{% endfor %}\n"
git-cliff -o CHANGELOG.md

Git-cliff uses a template-based approach (Tera templates) for complete control over changelog formatting. It supports custom grouping, filtering, and sorting of commits.

Release Drafter for GitHub

Release Drafter automatically drafts release notes as PRs are merged:

# .github/release-drafter.yml
name-template: 'v$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
  - title: 'Features'
    labels:
      - 'feature'
      - 'enhancement'
  - title: 'Bug Fixes'
    labels:
      - 'fix'
      - 'bugfix'
      - 'bug'
  - title: 'Maintenance'
    label: 'chore'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
  ## Changes
  $CHANGES

Release Drafter maintains a draft release that updates with every merged PR. When you are ready to release, publish the draft — it creates the tag and release notes automatically. This is the most popular approach for projects that want automated changelogs without committing to conventional commits.

Tag Best Practices Summary

  • Use annotated tags for all public releases — they carry essential metadata
  • Follow semvervMAJOR.MINOR.PATCH is the universal standard
  • Tag the release commit on your main branch, not feature branches
  • Sign your tags for open-source projects — cryptographic verification proves authenticity
  • Write good tag messages — briefly summarize changes since the last tag
  • Keep tags permanent — moving or deleting tags on public repos confuses consumers
  • Automate changelog generation — tools like standard-version, git-cliff, or release-drafter reduce manual effort
  • Automate release creation — CI pipelines should create GitHub Releases from tags automatically

FAQ

How do I rename a tag?

Delete the old tag locally and remotely, create the new tag at the same commit, push it. However, renaming tags in public repositories is disruptive — anyone who already fetched the old name will have a stale reference. Avoid renaming published tags. If you must, clearly communicate through release notes and README updates.

What is the difference between a tag and a branch?

Tags are immutable references to a single commit. Branches are mutable references that move with each new commit. A tag marks a specific point in history; a branch tracks ongoing development. You cannot commit to a tag — Git enforces immutability. This fundamental difference makes tags suitable for releases and branches suitable for active development.

Should I use lightweight or annotated tags for releases?

Always use annotated tags for releases. They store essential metadata (tagger, date, message) and support GPG signing. Lightweight tags should be reserved for personal notes and temporary markers. The Git maintainers specifically recommend annotated tags for any tag that will be shared with others.

How do I automate version bumping?

Use tools like standard-version (npm), git-cliff, or semantic-release that analyze commit messages, determine the next version number based on conventional commits, update version files, generate changelogs, and create signed tags automatically. semantic-release goes further by also publishing to package registries and creating GitHub Releases.

Can I attach files to a Git tag?

Not with plain Git. Tags are just pointers to commits. Use GitHub Releases (or equivalent on other platforms) to attach binaries, installers, and release notes to a tag-based release. GitHub Releases creates a permanent archive of release artifacts linked to the tag. GitLab and Bitbucket offer similar release features.

Conclusion

Git tags are the foundation of software release management. Annotated tags with GPG signatures provide authenticity and metadata. Semantic versioning communicates change severity. GitHub Releases builds on tags to provide a complete release artifact management system. Automate tag creation and release publication through CI/CD for consistent, auditable releases.

For more Git fundamentals, see Git branching strategies and pull request workflows.

Section: Git 1743 words 9 min read Intermediate 756 articles in section Report inaccuracy Back to top