
A Git tag is a permanent name for one commit, usually a version like v1.4.0. A GitHub release is a page built on top of a tag, with release notes and downloadable files. Use annotated tags for releases, push them with git push --follow-tags, and let CI create the release so every version is built and documented the same way.
This guide covers the tag commands you will actually use, how to fix tagging mistakes, how to protect release tags, and a current GitHub Actions workflow that publishes a release on every version tag. It is part of our complete Git and GitHub Actions workflow guide.
Choosing how releases fit your branching model? See Git Flow vs GitHub Flow vs trunk-based development.
Lightweight vs Annotated Tags
| Lightweight | Annotated | |
|---|---|---|
| What it stores | Just a pointer to a commit | A full Git object: tagger, date, message, optional signature |
| Create | git tag v1.0.0 | git tag -a v1.0.0 -m "Release 1.0.0" |
Pushed by --follow-tags | No | Yes |
Used by git describe by default | No | Yes |
| Use for | Private, temporary markers | Releases |
Always use annotated tags for releases. They record who tagged the release and when, and several Git features ignore lightweight tags by default.
Everyday Tag Commands
git tag # list tags
git tag -l "v1.*" # list matching tags
git show v1.0.0 # show the tag and its commit
git tag -a v1.1.0 -m "Release 1.1.0" # tag the current commit
git tag -a v1.0.1 abc1234 -m "Release 1.0.1" # tag a specific commit
git push origin v1.1.0 # push one tag
git push --follow-tags # push commits plus their annotated tags
git tag -d v1.1.0 # delete locally
git push origin --delete v1.1.0 # delete on the remote
git describe --tags # e.g. v1.1.0-3-g9f2c1ab (3 commits after v1.1.0)
Prefer git push --follow-tags over git push --tags. --tags pushes every local tag, including throwaway lightweight ones; --follow-tags only pushes annotated tags that point at commits you are pushing. You can make it the default with git config --global push.followTags true.
Signed Tags
git tag -s v1.2.0 -m "Release 1.2.0" # sign with your GPG or SSH key
git tag -v v1.2.0 # verify the signature
Signed tags prove the release came from you. GitHub shows a “Verified” badge on signed tags if your signing key is added to your account. With SSH signing, set git config --global gpg.format ssh and point user.signingkey at your public key.
Fixing Tag Mistakes
Tagged the wrong commit (not pushed yet)
git tag -d v1.0.0
git tag -a v1.0.0 <correct-commit> -m "Release 1.0.0"
Tagged the wrong commit (already pushed)
git tag -d v1.0.0
git push origin --delete v1.0.0
git tag -a v1.0.0 <correct-commit> -m "Release 1.0.0"
git push origin v1.0.0
Be careful here. Anyone who already fetched the old tag keeps it; Git will not overwrite an existing tag on fetch. If the version has been downloaded or deployed, it is usually better to leave it and release v1.0.1 instead.
Tag not showing on the remote
Tags are not pushed by a plain git push. Push it by name, or use --follow-tags.
Best Practices for Release Tags
- Use semantic versioning (
MAJOR.MINOR.PATCH) with avprefix:v2.3.1. - Only tag commits that passed CI. Tag from
mainafter the merge, not from a feature branch. - Never move a published tag. Release a new patch version instead.
- Protect release tags. On GitHub, add a repository ruleset targeting tags matching
v*that restricts who can create, update or delete them. - Consider immutable releases. When enabled on GitHub, a published release’s tag and assets cannot be changed or deleted, which protects users from tampered downloads.
- Automate the release step so notes and artifacts are generated the same way every time.
Creating a GitHub Release
From the web UI: Releases → Draft a new release → choose or create a tag → click Generate release notes → publish. From the terminal, the GitHub CLI does the same in one line:
gh release create v1.1.0 --generate-notes
gh release create v1.1.0 ./dist/app.tar.gz --generate-notes # with an attached file
Automating Releases with GitHub Actions
This workflow creates a GitHub release with auto-generated notes every time you push a tag starting with v:
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Create GitHub release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
The permissions: contents: write block matters. Repositories created in recent years give GITHUB_TOKEN read-only access by default, and without it the release step fails with a 403 error. To attach build output, add a build step and pass its files to the action with files: dist/*. For more on workflow syntax, see our GitHub Actions workflow YAML guide.
Your release flow then becomes two commands:
git tag -a v1.1.0 -m "Release 1.1.0"
git push --follow-tags
Frequently Asked Questions
A lightweight tag is just a pointer to a commit; an annotated tag is a full object storing the tagger, date, message and an optional signature. Use annotated tags for releases.
Run git push origin TAGNAME to push one tag, or git push origin –tags to push all local tags.
Run git tag -d TAGNAME to delete it locally, then git push origin –delete TAGNAME to remove it from the remote.
Yes, with git tag -f TAGNAME COMMIT and a force push, but moving a published tag is discouraged because existing clones keep the old one.
You Might Also Like
- 👉 Day 1: Git Fundamentals
- 👉 Day 2: Basic Git Workflow
- 👉 Day 3: Branching and Merging
- 👉 Day 4: Remote Repositories
- 👉 Day 5: Advanced Git Operations
- 👉 Day 6: Git Workflows and Best Practices
- 👉 Day 7: Troubleshooting and Expert Techniques
- 👉 Advanced Git Commands You Need to Master (With Examples)
- 👉 Git Best Practices for Branching and Approvals
- 👉 GitHub CODEOWNERS & Permissions: Best Practices with Teams
- 👉 Git Hooks Explained: Automate Your Workflow with Examples