| |

GitHub 14 ๐ŸŒ GitHub Releases, Tags, and Versioning

โฑ๏ธ Estimated read: 10 min | |

A tag is a Git concept: a fixed pointer to a specific commit that never moves. A release is a GitHub concept layered on top of a tag: a page with release notes and downloadable binary artifacts. Confusing the two is common. You can tag a commit without creating a release, but you cannot create a release without a tag. The tag is the anchor; the release is the package that users actually download and read about.

Key point: Releases are built on annotated tags, not lightweight tags. An annotated tag records the tagger, the date, and a message, and can be signed. A lightweight tag is just a movable bookmark. For anything that will be published, use git tag -a.


Why tags and releases exist

The mutable branch problem. A branch pointer advances with every commit. If you tell someone “use version 1.0,” and main has moved 40 commits since, the reference is meaningless. A tag solves this: v1.0.0 points to one commit forever. git describe --tags uses the nearest annotated tag to produce a human-readable name like v1.2.0-14-g2414bc4โ€”14 commits after v1.2.0, at commit 2414bc4 . This is what build scripts bake into --version output.

The source-versus-artifact problem. Git stores source code, not compiled binaries. A user who wants to run your Rust binary should not have to install a toolchain, clone the repository, and compile. A GitHub Release attaches the compiled binaries (Linux, macOS, Windows) directly to the release page. The tag identifies the exact source commit; the assets are the built artifacts from that commit .

The versioning semantics problem. Semantic Versioning (SemVer) encodes meaning into the version number: MAJOR.MINOR.PATCH. A patch increment means backward-compatible bug fixes. A minor increment means backward-compatible new features. A major increment means breaking changes . When a user sees v2.0.0, they know the upgrade requires attention. When they see v1.2.4, they can upgrade without reading migration guides.

The release-notes problem. A commit message explains a single change. A release note explains the entire delta since the last release: new features, bug fixes, breaking changes, and upgrade instructions. GitHub can generate release notes automatically from merged pull requests and commit history, or you can write them manually in a CHANGELOG.md and extract the relevant section .

The automation problem. A release should not require manual clicking. A Git tag push can trigger a GitHub Actions workflow that builds binaries, creates the release, and attaches the assets. The workflow runs only when the tag matches a pattern like v*, ensuring that releases are deliberate and reproducible .


a. Creating annotated tags and pushing them

A tag is created locally with git tag. For any tag that will be published, use the -a flag and provide a message. The message becomes part of the tag object and can be read by tools.

# Tag the current commit with an annotated tag
git tag -a v1.0.0 -m "First stable release"

# Tag a specific commit (e.g., if you forgot to tag at the time)
git tag -a v0.9.0 9fceb02 -m "Beta release"

# Verify the tag
git show v1.0.0

git push does not push tags. Tags are refs outside refs/heads/, and a normal push ignores them. You must push them explicitly. The safest approach is to push a single tag, or use --follow-tags to push annotated tags reachable from the commits being pushed. A blanket git push --tags can publish local experimental tags you never intended to release .

# Push a specific tag
git push origin v1.0.0

# Push commits and their reachable annotated tags
git push --follow-tags

To delete a tag before it is widely used, delete it locally and on the remote. Deleting a published tag after users have downloaded it is antisocial and breaks their references .

git tag -d v1.0.0
git push origin --delete v1.0.0

b. Creating a release from a tag

A release is created from the GitHub web interface or the gh CLI. The web interface: navigate to the repository’s “Releases” page, click “Draft a new release,” select the existing tag (or create a new one), enter a title and description, optionally attach binary assets, and click “Publish release” . If the repository has immutable releases enabled, create the release as a draft first, attach all assets, then publishโ€”after publishing, assets cannot be added or replaced .

The gh CLI is faster for automation and scripting. The gh release create command accepts the tag as the first argument and supports flags for notes, assets, and release type.

# Create a release with explicit notes
gh release create v1.0.0 --title "v1.0.0" --notes "Initial stable release"

# Create a release with auto-generated notes from merged PRs
gh release create v1.0.0 --generate-notes

# Read notes from a file (e.g., extracted from CHANGELOG)
gh release create v1.0.0 --notes-file release-notes.md

# Mark as pre-release
gh release create v1.0.0-rc.1 --prerelease --notes "Release candidate"

# Attach binary assets with display labels
gh release create v1.0.0 ./dist/app-linux ./dist/app-macos '#MacOS Bundle'

The --generate-notes flag uses GitHub’s Release Notes API to produce a changelog from merged pull requests since the last release. The --notes-start-tag flag pins the comparison base when the default range is incorrect . If the repository uses a CHANGELOG.md, a workflow can extract the section for the current version and pass it via --notes-file .


c. Automating releases with GitHub Actions

A release workflow triggers on tag pushes matching v*. The workflow checks out the code, builds the artifacts, and creates the release with gh release create or the softprops/action-gh-release action. The critical permission is contents: writeโ€”without it, the release creation step fails .

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

permissions:
  contents: write

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

      - name: Build artifacts
        run: cargo build --release

      - name: Create Release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            target/release/my-binary

The softprops/action-gh-release action automatically creates a release from the tag, uses the tag name as the release title, and uploads the listed files as assets . The files input accepts newline-separated globs and supports display labels with the # syntax.

A subtle pitfall: if a workflow creates and pushes a tag using the default GITHUB_TOKEN, that tag push does not trigger another workflow with on: push: tags. The token’s push events are suppressed to prevent recursive workflows. The fix is to use a personal access token for the tag creation, or to dispatch the release workflow explicitly .


Complete Example Session

# ============================================
# PART 1: ENSURE THE CODE IS READY
# ============================================

git checkout main
git pull origin main
cargo test
# All tests pass.
# ============================================
# PART 2: UPDATE THE VERSION
# ============================================

# Edit Cargo.toml
# version = "1.2.0"
git add Cargo.toml Cargo.lock
git commit -m "Bump version to 1.2.0"
git push origin main
# ============================================
# PART 3: CREATE AND PUSH AN ANNOTATED TAG
# ============================================

git tag -a v1.2.0 -m "Release v1.2.0"
git push origin v1.2.0
# The tag is now on GitHub.
# ============================================
# PART 4: CREATE THE RELEASE WITH GH CLI
# ============================================

gh release create v1.2.0 \
    --title "v1.2.0" \
    --generate-notes \
    ./target/release/my-binary
# Release published with auto-generated notes and binary asset.
# ============================================
# PART 5: VERIFY THE RELEASE
# ============================================

gh release view v1.2.0
# Shows title, notes, and assets.
# ============================================
# PART 6: MARK AS PRE-RELEASE (IF NEEDED)
# ============================================

gh release edit v1.2.0 --prerelease
# Users see the pre-release warning on the releases page.
# ============================================
# PART 7: DELETE A MISTAKEN RELEASE
# ============================================

gh release delete v1.2.0 --yes
git tag -d v1.2.0
git push origin --delete v1.2.0
# Fix the issue, re-tag, and re-release.
# ============================================
# PART 8: AUTOMATE VIA GITHUB ACTIONS
# ============================================

# .github/workflows/release.yml triggers on 'v*' tags.
# The workflow builds artifacts and creates the release.
git tag -a v1.2.1 -m "Release v1.2.1"
git push origin v1.2.1
# Actions runs, release is created automatically.

The eight parts trace the manual release lifecycle: verifying the code, bumping the version, creating the tag, creating the release, verifying, marking pre-release, deleting a mistake, and automating via Actions. The manual path is for one-off releases; the automated path is for repeatability.


Quick Reference

Tag Commands

CommandPurpose
git tag -a v1.0.0 -m "msg"Create annotated tag
git tag -l "v1.*"List tags matching pattern
git show v1.0.0Inspect tag metadata
git push origin v1.0.0Push specific tag
git push --follow-tagsPush commits + reachable annotated tags
git tag -d v1.0.0Delete local tag
git push origin --delete v1.0.0Delete remote tag

Release Commands (gh CLI)

CommandPurpose
gh release create v1.0.0Interactive release creation
gh release create v1.0.0 --generate-notesAuto-generate notes
gh release create v1.0.0 --notes-file notes.mdNotes from file
gh release create v1.0.0 --prereleaseMark as pre-release
gh release create v1.0.0 ./dist/appAttach asset
gh release view v1.0.0View release details
gh release edit v1.0.0 --prereleaseEdit release
gh release delete v1.0.0Delete release

Semantic Versioning

IncrementWhenExample
MAJORBackward-incompatible changes1.0.0 โ†’ 2.0.0
MINORBackward-compatible new features1.0.0 โ†’ 1.1.0
PATCHBackward-compatible bug fixes1.0.0 โ†’ 1.0.1

Best Practices

โœ… Do This:

git tag -a v1.0.0 -m "Release v1.0.0"
<!-- Always use annotated tags for releases -->       # โœ…
gh release create v1.0.0 --generate-notes
<!-- Let GitHub generate notes from merged PRs -->    # โœ…
permissions:
  contents: write
<!-- Required for Actions to create releases -->      # โœ…

โŒ Don’t Do This:

git tag v1.0.0
<!-- Lightweight tag: no metadata, no signature -->   # โŒ
git push --tags
<!-- Pushes all local tags, including experimental --> # โŒ
# Create a release without a tag
<!-- Releases require a tag as the anchor -->         # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Tag not on GitHubForgot git push origin <tag>Push the tag explicitly
Release shows no notesNo --generate-notes or --notesAdd notes flag or edit post-publish
Actions release failsMissing contents: write permissionAdd permissions block
Tag push does not trigger workflowTag created by GITHUB_TOKENUse PAT or dispatch workflow explicitly
Cannot edit published release assetsImmutable releases enabledCreate as draft, attach assets, then publish

Real-World Examples

1. Annotated Tag

git tag -a v1.0.0 -m "Release v1.0.0"

2. Push Tag

git push origin v1.0.0

3. Create Release

gh release create v1.0.0 --title "v1.0.0" --notes "First release"

4. Auto-Generated Notes

gh release create v1.0.0 --generate-notes

5. Pre-Release

gh release create v2.0.0-rc.1 --prerelease

6. Attach Binary

gh release create v1.0.0 ./target/release/app

7. View Release

gh release view v1.0.0

8. Delete Release and Tag

gh release delete v1.0.0 --yes
git push origin --delete v1.0.0

9. Actions Release Workflow

on:
  push:
    tags: ['v*']

10. Softprops Action

uses: softprops/action-gh-release@v2
with:
  files: target/release/app

Visual

Tag vs Release

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  GIT TAG (repository concept)                โ”‚
โ”‚                                              โ”‚
โ”‚  v1.0.0 โ†’ commit abc123                      โ”‚
โ”‚  Fixed pointer, never moves.                 โ”‚
โ”‚  Metadata: tagger, date, message.            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ”‚ GitHub Release (layered on top)
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  GITHUB RELEASE (GitHub concept)             โ”‚
โ”‚                                              โ”‚
โ”‚  Tag: v1.0.0                                 โ”‚
โ”‚  Notes: "New feature X, bug fix Y"           โ”‚
โ”‚  Assets: app-linux, app-macos, checksums.txt โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The tag is the source anchor. The release adds prose and binaries.

SemVer Decision

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  What changed?                               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
           โ”‚ BREAKING             โ”‚ NEW FEATURE (compatible)
           โ–ผ                       โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  MAJOR increment     โ”‚  โ”‚  MINOR increment     โ”‚
โ”‚  v1.2.3 โ†’ v2.0.0     โ”‚  โ”‚  v1.2.3 โ†’ v1.3.0     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
           โ”‚ BUG FIX (compatible)
           โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  PATCH increment     โ”‚
โ”‚  v1.2.3 โ†’ v1.2.4     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The version number communicates the upgrade cost.


Summary

ItemValue
Tag type for releasesAnnotated (git tag -a)
Tag pushgit push origin <tag>
Release creation (web)Releases โ†’ Draft a new release
Release creation (CLI)gh release create <tag>
Auto notes--generate-notes
Attach assetsgh release create <tag> <file>
Pre-release flag--prerelease
Actions permissioncontents: write
SemVer formatMAJOR.MINOR.PATCH

Key takeaways:

  • A tag is a fixed pointer; a release is a page built on that pointer. The tag lives in Git; the release lives on GitHub and carries notes and binaries .
  • Use annotated tags for releases. They record who tagged, when, and why. Lightweight tags are bookmarks, not release anchors .
  • git push does not push tags. Push them explicitly with git push origin <tag> or --follow-tags .
  • SemVer communicates upgrade cost. MAJOR for breaking changes, MINOR for features, PATCH for fixes .
  • Automation requires contents: write. A GitHub Actions workflow that creates releases needs this permission explicitly set .
  • Tag pushes by GITHUB_TOKEN do not trigger tag workflows. This is a deliberate suppression to prevent recursive runs. Use a PAT or explicit dispatch .

Remember: A release is not a tag. The tag is the immutable source anchor; the release is the package users see. Tag the commit, push the tag, then create the release with notes and binaries. Or automate the whole chain with a tag-triggered workflow. Either way, the tag comes first.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!