Tags and releases: version numbers people can trust
Mark a commit as a version with an annotated tag, push and delete tags, choose version numbers with semantic versioning, and publish GitHub Releases with generated notes that can trigger a deployment.
By the end of this lesson you'll be able to
0 of 6 completeAnnotated and lightweight tags
A tag is a permanent name for one commit: "this is version 1.0.0". Unlike a branch, it never moves. There are two kinds:
$ git tag v1.0.0-light # lightweight: just a name for a commit $ git tag -a v1.0.0 -m "First stable release" # annotated: a real object with metadata $ git cat-file -t v1.0.0-light commit $ git cat-file -t v1.0.0 tag $ git show v1.0.0 tag v1.0.0 Tagger: Rajeev Kumar <rajeev@example.com> Date: Wed Oct 14 04:38:37 2026 +0000 First stable release commit f5b6404cff08e75f5798c0fa368f916d154b4fc1
git tag v1.0.0-lightgitversion control
| Lightweight | Annotated (-a) | |
|---|---|---|
| Stores | only the commit it points at | tagger, date, message, optional signature |
Used by git describe | only with --tags | yes |
| Use for | private bookmarks | every release |
Tag an older commit by naming it: git tag -a v0.9.0 9ece149 -m "…". Sign tags with git tag -s (GPG
or SSH signing set up), and GitHub marks them Verified.
Pushing, listing and deleting tags
git push sends branches, not tags. Push them on purpose:
$ git push origin v1.0.0 # one tag * [new tag] v1.0.0 -> v1.0.0 $ git push --follow-tags # commits, plus annotated tags pointing at them $ git config --global push.followTags true # make that the default $ git ls-remote --tags origin # the tags GitHub has
git push origin v1.0.0gitversion controlpushupload commits
git config --global push.followTags trueconfigread or set settings--globalfor your user, in every repo
$ git tag -l "v1.*" --sort=-v:refname # list, newest version first v1.0.1 v1.0.0 $ git log --oneline v1.0.0..v1.0.1 # what changed between two releases 9ece149 Fix the login redirect $ git checkout v1.0.0 # look at a release (detached HEAD) $ git tag -d v1.0.0-light # delete locally $ git push origin --delete v1.0.1 # delete on GitHub
git tag -l "v1.*" --sort=-v:refnamegitversion control
git log --oneline v1.0.0..v1.0.1loghistory
git push origin --delete v1.0.1pushupload commits
Naming every build: git describe
$ git describe --tags v1.0.1-1-gf367a0f
git describe --tagsgitversion control
Read it as: nearest tag v1.0.1, 1 commit after it, at commit f367a0f (the g is for Git). Pipelines
use it to stamp builds between releases, so a bug report says exactly which code was running.
Semantic versioning
SemVer gives version numbers a meaning: MAJOR.MINOR.PATCH.
| Change since 2.4.1 | Next version |
|---|---|
| fix a bug, nothing else changes for users | 2.4.2 |
| add an optional setting, a new endpoint | 2.5.0 |
| remove or rename something users rely on | 3.0.0 |
| a test build of the next major | 3.0.0-rc.1 (pre-release, sorts before 3.0.0) |
0.x versions mean "not stable yet: anything may change". Tag with a v prefix (v2.4.1): it's the
common convention and what most tools expect. Conventional Commits (from the Commits lesson) make
this automatic: fix: means a patch, feat: a minor, feat!: or BREAKING CHANGE: a major, and tools
like release-please read your commits to pick the number and write the changelog.
GitHub Releases
A release is a tag plus a page on GitHub: a title, release notes, and optional files (binaries, archives). It's what users see under Releases, and what they get notified about.
$ gh release create v1.0.1 --generate-notes # notes built from merged pull requests $ gh release create v2.0.0-rc.1 --prerelease --generate-notes $ gh release create v1.1.0 --draft --notes-from-tag # a draft, using the annotated tag's message $ gh release create v1.1.0 ./dist/app-linux-amd64 --generate-notes # attach a build $ gh release list $ gh release view v1.0.1 --web
If the tag doesn't exist yet, gh release create creates it on the default branch (use --verify-tag to
refuse instead). --generate-notes lists every pull request merged since the previous release, grouped by
label if you add a .github/release.yml, with credit to each contributor.
Releases that deploy
A published release is also an event that GitHub Actions can react to. This site works that way:
merging to main deploys to a staging environment, and publishing a GitHub Release vX.Y.Z runs a
workflow that moves the production branch to that tag, which deploys it to the live site. Production
changes only when someone deliberately cuts a release:
# .github/workflows/release.yml (shortened from this site's real workflow) on: release: types: [published] jobs: promote: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 with: { fetch-depth: 0 } - name: The release must be on main run: | sha=$(git rev-list -n 1 "refs/tags/$GITHUB_REF_NAME") git merge-base --is-ancestor "$sha" origin/main || exit 1 echo "SHA=$sha" >> "$GITHUB_ENV" - name: Move production to the release (fast-forward only) run: git push origin "$SHA:refs/heads/production"
The real file also checks that CI passed on that commit and applies database migrations first. Because
the push isn't forced, production can only move forward: a rollback means redeploying an older build,
then releasing a fix.
The CI/CD course builds pipelines like this from scratch.
Cut your first two releases
Release the shop repository as v1.0.0, then fix a bug and release v1.0.1. Use annotated tags, push them, publish both as GitHub Releases with generated notes, and show what changed between them.
Two releases on GitHub; git describe naming later buildsReveal one safe solution
$ git switch main && git pull $ git tag -a v1.0.0 -m "First stable release" $ git push origin v1.0.0 $ gh release create v1.0.0 --generate-notes --title "v1.0.0" $ echo "Fixed: login redirect" >> README.md && git commit -am "Fix the login redirect" $ git push $ git tag -a v1.0.1 -m "Fix the login redirect" $ git push --follow-tags $ gh release create v1.0.1 --generate-notes $ git log --oneline v1.0.0..v1.0.1 $ git tag -l --sort=-v:refname v1.0.1 v1.0.0 $ echo "search" >> README.md && git commit -am "Add search" $ git describe --tags v1.0.1-1-g…
git switch main && git pullgitversion control&&run the next command only if this one workedpulldownload and merge
git push origin v1.0.0pushupload commits
echo "Fixed: login redirect" >> README.md && git commit -am "Fix the login redirect"echoprint text>>append output to a filecommitsave staged changes
git log --oneline v1.0.0..v1.0.1loghistory
The bug fix is a PATCH bump because nothing changed for users except the fix. On GitHub, Releases now shows v1.0.1 as Latest, with the fix listed in its notes.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
Why use an annotated tag (git tag -a) for a release instead of a lightweight one?
Official reading for this lesson
Or get every quiz answer right and it completes itself.