Git Tagging & Semantic Versioning Guide

Learn how to implement consistent, automated versioning for your projects using semantic versioning, conventional commits, and git tags.


What is Semantic Versioning?

Semantic Versioning (SemVer) is a versioning scheme that uses a three-part number: MAJOR.MINOR.PATCH

Version Format: MAJOR.MINOR.PATCH

v2.3.1
│ │ └─ PATCH: Bug fixes (backward compatible)
│ └─── MINOR: New features (backward compatible)
└───── MAJOR: Breaking changes (not backward compatible)

Version Increment Rules

Change TypeIncrementExample
Breaking changesMAJORv1.5.3v2.0.0
New features (backward compatible)MINORv1.5.3v1.6.0
Bug fixes (backward compatible)PATCHv1.5.3v1.5.4

Understanding Version Numbers

MAJOR Version (Breaking Changes)

Increment when you make incompatible API changes that will break existing implementations.

Examples:

  • Removing a public API endpoint
  • Changing function signatures (adding required parameters)
  • Removing support for a major dependency
  • Renaming classes or modules that users import
// v1.x.x
function login(username: string): User;

// v2.0.0 - BREAKING CHANGE
function login(username: string, password: string): Promise<User>;

Real-world scenarios:

  • v1.5.3v2.0.0: Migrated from REST API to GraphQL
  • v2.8.1v3.0.0: Removed deprecated authentication methods
  • v3.2.0v4.0.0: Changed database schema, requires migration

MINOR Version (New Features)

Increment when you add new functionality in a backward-compatible manner.

Examples:

  • Adding new API endpoints
  • Adding optional parameters to functions
  • Introducing new classes or modules
  • Adding new configuration options
// v1.5.x
interface UserConfig {
  theme: "light" | "dark";
}

// v1.6.0 - New feature (backward compatible)
interface UserConfig {
  theme: "light" | "dark";
  language?: string; // Optional - won't break existing code
}

Real-world scenarios:

  • v1.5.3v1.6.0: Added export to PDF feature
  • v2.3.0v2.4.0: Added OAuth2 authentication alongside existing auth
  • v1.8.2v1.9.0: Introduced real-time notifications

PATCH Version (Bug Fixes)

Increment when you make backward-compatible bug fixes.

Examples:

  • Fixing incorrect calculations
  • Resolving memory leaks
  • Correcting typos in error messages
  • Fixing edge cases that caused crashes
// v1.5.3 - Bug: crashes on null input
function formatName(name: string): string {
  return name.toUpperCase(); // Crashes if name is null
}

// v1.5.4 - Fixed null handling
function formatName(name: string): string {
  return (name || "").toUpperCase(); // Safe null handling
}

Real-world scenarios:

  • v1.5.3v1.5.4: Fixed null pointer exception in user profile
  • v2.3.0v2.3.1: Corrected timezone calculation bug
  • v1.8.9v1.8.10: Fixed memory leak in background workers

Pre-release Versions

Use pre-release identifiers for testing before stable releases:

v1.0.0-alpha.1    → Early testing (not feature complete)
v1.0.0-beta.1     → Feature complete, testing for bugs
v1.0.0-rc.1       → Release candidate (final testing)
v1.0.0            → Stable release

Development versions:

  • v0.x.x indicates pre-1.0 (unstable API, breaking changes allowed in MINOR)
  • v1.0.0 is your first stable, production-ready release

Conventional Commits

To automate semantic versioning, use Conventional Commits to structure commit messages.

Commit Message Format

<type>[optional scope]: <description>

[optional body]

[optional footer]

Commit Types

TypeDescriptionVersion Impact
featNew featureMINOR bump
fixBug fixPATCH bump
feat! or fix!Breaking changeMAJOR bump
docsDocumentation onlyNo bump
styleCode formattingNo bump
refactorCode restructuringNo bump
perfPerformance improvementPATCH bump
testAdding testsNo bump
choreMaintenance tasksNo bump
ciCI/CD changesNo bump
buildBuild system changesNo bump

Examples

Feature (MINOR Bump)

git commit -m "feat(auth): add OAuth2 authentication

Implemented OAuth2 flow with GitHub and Google providers.
Includes token refresh and scope management.

Closes #123"

Result: v1.5.3v1.6.0

Bug Fix (PATCH Bump)

git commit -m "fix(api): handle null values in user response

Previously crashed when user.name was null.
Now returns empty string as fallback."

Result: v1.6.0v1.6.1

Breaking Change (MAJOR Bump)

git commit -m "feat!: refactor authentication module

BREAKING CHANGE: login() now returns Promise<User> instead of User.
All authentication methods now require async/await.

Migration guide: https://docs.example.com/migration/v2"

Result: v1.6.1v2.0.0

Multiple Commits

When analyzing multiple commits, the highest priority change determines the bump:

# Since last tag v1.5.3:
git commit -m "docs: update API documentation"
git commit -m "fix(ui): correct button alignment"
git commit -m "feat(dashboard): add export to CSV"
git commit -m "test: add integration tests"

Result: v1.5.3v1.6.0 (MINOR wins because of feat)


Creating Git Tags

Annotated Tags (Recommended)

Always use annotated tags for releases. They include metadata and are stored as full objects in Git.

# Create annotated tag
git tag -a v1.2.3 -m "Release v1.2.3: Add user profile management"

# Push tag to remote
git push origin v1.2.3

Tag Message Format

Release v{version}: {brief summary}

{Optional: Key features or fixes}
{Optional: Breaking changes warning}

Example:

git tag -a v2.0.0 -m "Release v2.0.0: GraphQL API

- Migrated from REST to GraphQL
- Improved query performance
- BREAKING: All REST endpoints removed"

Lightweight Tags (Not Recommended)

Lightweight tags are just pointers to commits without metadata:

# ❌ Avoid for releases
git tag v1.2.3

# ✅ Use annotated instead
git tag -a v1.2.3 -m "Release v1.2.3"

Manual Tagging Process

Step 1: Review Changes

# Checkout main branch
git checkout main
git pull origin main

# View commits since last tag
git log $(git describe --tags --abbrev=0)..HEAD --oneline

Step 2: Determine Version Bump

Analyze commit messages:

  • Any feat! or BREAKING CHANGE:? → MAJOR
  • Any feat:? → MINOR
  • Only fix:, perf:? → PATCH
  • Only docs:, chore:, test:? → No bump

Step 3: Create Tag

# Determine new version based on step 2
# Current: v1.5.3

# Example: MINOR bump
git tag -a v1.6.0 -m "Release v1.6.0: Add dashboard widgets"

Step 4: Push Tag

# Push single tag
git push origin v1.6.0

# Or push all tags
git push --tags

Step 5: Verify

  • Check GitHub Releases or Tags section
  • Tag should appear with full annotation

Automated Tagging

Option 1: VS Code Extension

Install: Conventional Commits Extension

Features:

  • Commit message templates
  • Real-time validation
  • Type and scope suggestions

Usage:

  1. Stage your changes
  2. Open Source Control panel
  3. Click "Conventional Commits" icon
  4. Select commit type, scope, and description
  5. Extension formats message automatically

Option 2: Cursor Rules (AI-Assisted)

Add to your .cursor/rules:

## Commit Message Format

Always structure commits as:

- `feat(scope): description` for new features
- `fix(scope): description` for bug fixes
- `docs(scope): description` for documentation
- Add `!` for breaking changes: `feat!:`
- Add `BREAKING CHANGE:` footer for details

Cursor will help format your commit messages correctly.

Option 3: Git Hooks with Husky

Install dependencies:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky

Setup Husky:

# Initialize Husky
npx husky install

# Add commit-msg hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Create commitlint.config.js:

module.exports = {
  extends: ["@commitlint/config-conventional"],
  rules: {
    "type-enum": [
      2,
      "always",
      ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "ci", "build"],
    ],
    "subject-case": [2, "never", ["upper-case"]],
  },
};

Result: Commits are automatically validated before accepting.

Option 4: Semantic Release (Full Automation)

Install:

npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git

Create .releaserc.json:

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

Add to package.json:

{
  "scripts": {
    "release": "semantic-release"
  }
}

Setup GitHub Actions (.github/workflows/release.yml):

name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      issues: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

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

      - run: npm ci

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx semantic-release

What this does:

  1. Analyzes commits on every push to main
  2. Determines next version based on commit types
  3. Generates changelog
  4. Creates git tag
  5. Publishes GitHub release
  6. Updates package.json version

Best Practices

✅ Do

  • Always use annotated tags for releases
  • Follow semantic versioning strictly
  • Write meaningful commit messages following Conventional Commits
  • Review commits before creating tags
  • Tag only on main branch after tests pass
  • Push tags immediately after creation
  • Document breaking changes clearly in commit footer
  • Use pre-release versions for testing

❌ Don't

  • Create tags on feature branches
  • Use lightweight tags for releases
  • Skip tag annotations
  • Bump versions arbitrarily without commits
  • Create tags for work-in-progress code
  • Delete pushed tags unless absolutely necessary
  • Use inconsistent versioning schemes

Common Scenarios

Scenario 1: Hotfix in Production

# Current production: v1.5.3
# Critical bug found

# Create hotfix branch
git checkout -b hotfix/auth-crash main

# Fix the bug
git commit -m "fix(auth): prevent crash on invalid token"

# Merge to main
git checkout main
git merge hotfix/auth-crash

# Create patch tag
git tag -a v1.5.4 -m "Release v1.5.4: Fix auth crash on invalid token"
git push origin v1.5.4

# Result: v1.5.3 → v1.5.4

Scenario 2: Multiple Features in Sprint

# Current: v1.5.4
# Sprint includes 3 features

git commit -m "feat(dashboard): add real-time updates"
git commit -m "feat(reports): add CSV export"
git commit -m "feat(users): add bulk actions"
git commit -m "fix(ui): correct button styles"
git commit -m "docs: update API documentation"

# After sprint, merge to main
# Create minor version tag
git tag -a v1.6.0 -m "Release v1.6.0: Dashboard updates and user management

- Add real-time dashboard updates
- Add CSV export for reports
- Add bulk user actions
- Fix button styling issues"

git push origin v1.6.0

# Result: v1.5.4 → v1.6.0

Scenario 3: Major Refactor

# Current: v1.6.0
# Complete API rewrite

git commit -m "feat!: migrate to GraphQL API

BREAKING CHANGE: All REST endpoints have been removed.
Use GraphQL queries and mutations instead.

Migration guide: https://docs.example.com/migration/v2
Deprecation notice was in v1.5.0 (6 months ago)"

# Create major version tag
git tag -a v2.0.0 -m "Release v2.0.0: GraphQL API

BREAKING: Migrated from REST to GraphQL.
See migration guide for upgrade instructions."

git push origin v2.0.0

# Result: v1.6.0 → v2.0.0

Integration with ReleaseRay

ReleaseRay uses git tags to generate intelligent release notes:

How ReleaseRay Uses Tags

  1. Define Release Ranges: Select v1.0.0v1.1.0 to analyze that range
  2. Fetch Commits: Retrieves all commits between tags
  3. Analyze Changes: Categorizes changes by type (features, fixes, breaking)
  4. Generate Notes: Creates persona-specific release notes (engineer, customer, internal)

Best Practices for ReleaseRay

  • Tag frequently: Tag every significant release or sprint
  • Use clear messages: Tag annotations appear in release notes
  • Follow conventions: Conventional commits improve AI analysis
  • Push tags immediately: ReleaseRay needs tags on GitHub
  • Maintain consistency: Consistent tagging = better release notes

Troubleshooting

Problem: "Commit message invalid"

Error:

⧗   input: updated some stuff
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

Solution: Use conventional commit format:

git commit -m "fix(ui): update button styles"

Problem: Tag Already Exists

Error:

fatal: tag 'v1.2.3' already exists

Solution: Delete and recreate:

# Delete locally
git tag -d v1.2.3

# Delete remotely (if already pushed)
git push --delete origin v1.2.3

# Recreate with correct version
git tag -a v1.2.3 -m "Release v1.2.3"
git push origin v1.2.3

Problem: Wrong Version Number

Example: Accidentally created v1.7.0 instead of v1.6.0

Solution:

# Delete wrong tag
git tag -d v1.7.0
git push --delete origin v1.7.0

# Create correct tag
git tag -a v1.6.0 -m "Release v1.6.0"
git push origin v1.6.0

Problem: Forgot to Annotate

Created:

git tag v1.2.3  # Lightweight tag (bad)

Solution:

# Delete lightweight tag
git tag -d v1.2.3

# Recreate as annotated
git tag -a v1.2.3 -m "Release v1.2.3: Add user management"
git push origin v1.2.3

Version History Cheat Sheet

v0.1.0        → First development version
v0.2.0        → Added features (pre-stable)
v0.9.0        → Feature freeze, preparing for v1.0
v0.9.1        → Bug fixes before v1.0
v1.0.0        → First stable release! 🎉
v1.0.1        → Patch: Bug fixes
v1.1.0        → Minor: New features
v1.1.1        → Patch: More bug fixes
v2.0.0        → Major: Breaking changes
v2.0.0-rc.1   → Release candidate testing
v2.0.0        → Stable v2 release

Further Reading


Quick Reference Card

# Create annotated tag
git tag -a v1.2.3 -m "Release message"

# Push tag
git push origin v1.2.3

# List all tags
git tag -l

# View tag details
git show v1.2.3

# Delete local tag
git tag -d v1.2.3

# Delete remote tag
git push --delete origin v1.2.3

# View commits since last tag
git log $(git describe --tags --abbrev=0)..HEAD

# Commit with conventional format
git commit -m "feat(scope): description"
git commit -m "fix(scope): description"
git commit -m "feat!: breaking change"

Need help with versioning? Contact us or check out ReleaseRay for automated release note generation from your git tags.

We value your privacy

We use cookies to enhance your experience. Essential cookies are required for the site to function. You can choose to accept all cookies or manage your preferences.