DevOpsGitHub

Git Tagging & Releases: Complete Versioning Guide

TT
TopicTrick Team
Git Tagging & Releases: Complete Versioning Guide

Git Tagging & Releases: Complete Versioning Guide

A tag in git is a named pointer to a specific commit. Unlike branches, tags do not move — they permanently mark a point in history. Combined with Semantic Versioning and GitHub Releases, tags become the foundation of a professional software delivery workflow.

This guide covers lightweight vs annotated tags, Semantic Versioning rules, how to automate versioning with Conventional Commits, creating GitHub Releases with binary assets, and a complete GitHub Actions release pipeline.


Lightweight vs Annotated Tags

Lightweight Tags

A lightweight tag is just a name for a commit — a file in .git/refs/tags/ that contains a SHA.

bash
git tag v1.0-beta
git tag v1.0-beta abc1234    # Tag a specific commit
git push origin v1.0-beta    # Push to GitHub

Lightweight tags have no author, no date, no message. Use them for temporary local markers or quick personal bookmarks.

Annotated Tags (Use for Releases)

An annotated tag is a full git object stored in the object database, containing:

  • Tagger name and email
  • Tagging date and time
  • Tag message
  • Optional GPG signature
bash
# Create an annotated tag
git tag -a v1.2.0 -m "Release v1.2.0: Add user authentication and dashboard"

# Tag a specific commit (for retroactive tagging)
git tag -a v1.1.0 abc1234 -m "Release v1.1.0"

# Sign the tag with GPG (for high-security releases)
git tag -s v1.2.0 -m "Release v1.2.0"

Inspect an annotated tag:

bash
git show v1.2.0
text
tag v1.2.0
Tagger: Alice Smith <alice@example.com>
Date:   Mon Apr 18 10:30:00 2026 +0000

Release v1.2.0: Add user authentication and dashboard

commit abc1234def5678...
Author: Alice Smith <alice@example.com>
Date:   Mon Apr 18 10:00:00 2026 +0000

    feat: complete dashboard implementation

Always use annotated tags for releases — they provide a permanent record of who released what and when.


Semantic Versioning (SemVer)

Semantic Versioning (semver.org) defines a standard format: MAJOR.MINOR.PATCH.

The Three Numbers

MAJOR — Breaking changes. Existing code that uses the previous version may need to be updated.

text
v1.5.3 → v2.0.0

Examples of breaking changes:

  • Renamed or removed a public function
  • Changed a function's return type or parameter types
  • Removed a REST endpoint
  • Changed a database schema in a way that requires migration

MINOR — New functionality, backwards compatible. Users can upgrade without changing their code.

text
v1.5.3 → v1.6.0

Examples of minor additions:

  • Added a new API endpoint
  • Added an optional parameter to an existing function
  • Added a new UI page that doesn't change existing flows

PATCH — Bug fixes, backwards compatible. No new features.

text
v1.5.3 → v1.5.4

Examples of patches:

  • Fixed a crash on null input
  • Corrected a wrong calculation
  • Fixed a typo in error messages

Pre-release and Build Metadata

text
v1.0.0-alpha        # Alpha: unstable, API may change
v1.0.0-alpha.1      # Alpha iteration 1
v1.0.0-beta         # Beta: feature-complete, testing
v1.0.0-beta.2       # Beta iteration 2
v1.0.0-rc.1         # Release candidate: production-ready candidate
v1.0.0              # Stable release
v1.0.0+build.123    # Build metadata (ignored for version precedence)

Version Precedence

text
1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0

Pre-release versions have lower precedence than the associated normal version.


Git Tag Commands Reference

bash
# List all tags
git tag

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

# Create annotated tag at HEAD
git tag -a v1.2.0 -m "Release v1.2.0"

# Create annotated tag at a specific commit
git tag -a v1.1.0 abc1234 -m "Retroactive tag"

# Push a single tag
git push origin v1.2.0

# Push all tags
git push origin --tags

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

# Delete a remote tag (two-step)
git tag -d v1.2.0-beta
git push origin --delete v1.2.0-beta

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

# List tags with their commit messages
git tag -n

# Find which tag contains a commit
git describe --tags abc1234

# Check out the state of the repo at a tag (detached HEAD)
git checkout v1.1.0

Conventional Commits: Automated Versioning

Conventional Commits is a standard for writing structured commit messages that automated tools can parse to determine the next version number.

Format

text
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types that affect versioning:

TypeSemVer impactExample
featMINOR bumpfeat: add user authentication
fixPATCH bumpfix: correct tax calculation for EU
feat! or BREAKING CHANGE:MAJOR bumpfeat!: redesign API response format
docs, style, refactor, test, choreNo releasedocs: update README

Example Commit History

text
feat: add stripe payment integration
fix: handle null user in profile endpoint
docs: add API documentation
fix: correct order total rounding
feat: add dark mode to dashboard
feat!: rename /api/v1/user to /api/v2/users

An automated tool reads these commits since the last tag and calculates:

  • 1 breaking change → MAJOR bump
  • 2 features → (already covered by MAJOR)
  • 2 fixes → (already covered by MAJOR)
  • Result: v1.0.0 → v2.0.0

Automated Release Pipeline with GitHub Actions

Using semantic-release

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

on:
  push:
    branches: [main]

permissions:
  contents: write
  issues: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # Full history needed for semantic-release
          persist-credentials: false

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci

      - run: npm run build

      - run: npm test

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}   # If publishing to npm
        run: npx semantic-release

.releaserc.json configuration:

json
{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    "@semantic-release/changelog",
    "@semantic-release/npm",
    "@semantic-release/github",
    ["@semantic-release/git", {
      "assets": ["CHANGELOG.md", "package.json"],
      "message": "chore(release): ${nextRelease.version} [skip ci]"
    }]
  ]
}

This workflow:

  1. Analyses all commits since the last tag
  2. Determines the next version number
  3. Generates CHANGELOG.md
  4. Creates the git tag
  5. Creates a GitHub Release with auto-generated notes
  6. Publishes to npm (if configured)

Manual Release Workflow

For teams that prefer manual control:

yaml
# .github/workflows/manual-release.yml
name: Create Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Release version (e.g. v1.2.0)'
        required: true
      prerelease:
        description: 'Is this a pre-release?'
        type: boolean
        default: false

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Validate version format
        run: |
          if ! echo "${{ inputs.version }}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then
            echo "ERROR: Version must be in format v1.2.3"
            exit 1
          fi

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci
      - run: npm test
      - run: npm run build

      # Build release artifacts
      - name: Package release
        run: |
          tar -czf app-${{ inputs.version }}-linux-amd64.tar.gz -C dist .
          zip -r app-${{ inputs.version }}-windows-amd64.zip dist/

      # Create annotated tag
      - name: Create tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git tag -a ${{ inputs.version }} -m "Release ${{ inputs.version }}"
          git push origin ${{ inputs.version }}

      # Create GitHub Release with artifacts
      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ inputs.version }}
          name: Release ${{ inputs.version }}
          prerelease: ${{ inputs.prerelease }}
          generate_release_notes: true   # Auto-generate from PRs merged since last tag
          files: |
            app-${{ inputs.version }}-linux-amd64.tar.gz
            app-${{ inputs.version }}-windows-amd64.zip

GitHub Releases

A GitHub Release is a deployable version of your software built on top of a tag. It adds:

  • A human-readable title and description
  • Auto-generated release notes (list of PRs merged since last release)
  • Binary assets (executables, archives, checksums)
  • Pre-release flag for beta/RC versions

Creating a Release Manually

  1. Go to your repository → Releases → Draft a new release
  2. Choose a tag (existing or create a new one)
  3. Set a title (e.g., "v1.2.0 — User Authentication")
  4. Click Generate release notes — GitHub automatically lists all merged PRs
  5. Edit the notes if needed
  6. Upload binary assets
  7. Check "Pre-release" for alpha/beta/RC versions
  8. Click Publish release

Auto-Generated Release Notes

GitHub generates release notes from PR titles and labels. Improve them by:

  1. Adding a .github/release.yml configuration:
yaml
# .github/release.yml
changelog:
  exclude:
    labels:
      - ignore-for-release
  categories:
    - title: "🚀 New Features"
      labels:
        - enhancement
        - feature
    - title: "🐛 Bug Fixes"
      labels:
        - bug
        - fix
    - title: "🔐 Security"
      labels:
        - security
    - title: "📝 Documentation"
      labels:
        - documentation
    - title: "⬆️ Dependencies"
      labels:
        - dependencies
  1. Use consistent PR labels that match the configuration
  2. Write clear, user-facing PR titles (not internal jargon)

Checksums and Verification

For publicly distributed releases, always provide checksums so users can verify downloads:

bash
# Generate SHA256 checksums
sha256sum app-v1.2.0-linux-amd64.tar.gz > checksums.txt
sha256sum app-v1.2.0-windows-amd64.zip >> checksums.txt

# Users verify with:
sha256sum -c checksums.txt

In GitHub Actions:

yaml
- name: Generate checksums
  run: |
    sha256sum *.tar.gz *.zip > checksums.txt
    cat checksums.txt

- uses: softprops/action-gh-release@v2
  with:
    files: |
      *.tar.gz
      *.zip
      checksums.txt

Frequently Asked Questions

Q: Can I move a tag after creating it?

Technically yes, but it is strongly discouraged. Tags are meant to be immutable records. If you find a bug in v1.0.0 after releasing, the right action is to fix it and release v1.0.1 — not to move the v1.0.0 tag to the fixed commit. Moving tags breaks everyone who has already downloaded or referenced the old version.

Q: What is the difference between git push --tags and git push origin v1.2.0?

git push --tags pushes all local tags that don't exist on the remote — including old, temporary, or test tags you never intended to publish. git push origin v1.2.0 pushes only the specified tag. Prefer explicit tag pushes in automated workflows to avoid accidentally publishing test tags.

Q: How do I find the most recent tag on the current branch?

bash
git describe --tags --abbrev=0    # Most recent tag reachable from HEAD
git describe --tags               # Most recent tag + commits since it (e.g., v1.2.0-3-gabc1234)

git describe is commonly used in build scripts to embed the version into the compiled binary.

Q: Should library versions and application versions follow the same rules?

Libraries must follow SemVer strictly because consumers depend on the version number to understand compatibility. Applications are more flexible — many teams use CalVer (calendar versioning like 2026.04.1) or simple incrementing build numbers for internal applications where API compatibility is less of a concern.


Key Takeaway

Git tags are the bridge between a codebase and a software release. Annotated tags provide a permanent, signed record of who released what version and when. Semantic Versioning gives your users a contract: patch versions are safe to upgrade, minor versions add features, major versions may break things. Automating the release process with Conventional Commits and semantic-release eliminates manual version bumping mistakes and keeps changelogs accurate. The result is a professional delivery pipeline where every deployed version is fully traceable to the exact code that produced it.

Read next: Git Submodules & External Libraries →


Part of the GitHub Mastery Course — masters of the release.