Tags, releases and semantic versioning
Annotated versus lightweight tags, signing and pushing them, naming a build with git describe, and turning tags into releases and changelogs.
Tagging a commit
# annotated: a real object with a tagger, a date and a message
git tag -a v1.4.0 -m "Release 1.4.0: pagination and retry limits"
git tag -a v1.4.0 9f3c1ab -m "Release 1.4.0" # tag a commit that is not HEAD
# lightweight: a bare pointer, fine for local bookmarks
git tag v1.4.0-rc1
git tag -l "v1.4.*"
git show v1.4.0
git tag -d v1.4.0 # local only
git push origin --delete v1.4.0 # and on the remote| Lightweight | Annotated | |
|---|---|---|
| What it is | A plain reference to a commit | A separate tag object with metadata |
| Message | None | Yes, like a commit message |
| Signature | Cannot be signed | Can be signed with -s |
git describe | Ignored by default | Used to name builds |
| Best for | Temporary local marks | Anything published as a release |
Pushing, signing and describing
# tags are not pushed by a plain git push
git push origin v1.4.0
git push origin --tags # every local tag
git push --follow-tags # annotated tags that point into what you pushed
# sign and verify with your GPG or SSH key
git tag -s v1.4.1 -m "Release 1.4.1"
git tag -v v1.4.1
# name a build after the closest tag
git describe --tags # v1.4.0-12-g9f3c1ab
git describe --tags --dirty # adds -dirty when the tree has changes
git describe --match "v*" # ignore non-release tagsThe describe output reads as: the closest reachable tag, the number of commits since it, and the abbreviated commit id. v1.4.0-12-g9f3c1ab means twelve commits after the v1.4.0 release — a version string that is unique and still trivially traceable to a commit.
⚠️
A plain
git push does not push tags. A release tag that exists only on your machine is not a release, and it is the easiest way to ship a version nobody else can check out — push the tag, or use --follow-tags so annotated tags travel with the branch.Semantic versioning and changelogs
| Change | Bump | Example |
|---|---|---|
| Incompatible API change | MAJOR | v1.4.2 to v2.0.0 |
| New backwards-compatible feature | MINOR | v1.4.2 to v1.5.0 |
| Backwards-compatible bug fix | PATCH | v1.4.2 to v1.4.3 |
| Internal refactor, same behaviour | None | Version unchanged |
| Pre-release | Suffix | v2.0.0-rc.1 |
| Build metadata | Suffix, ignored when ordering | v1.4.0+build.42 |
# the raw material for a changelog
git log --oneline --no-merges v1.3.0..v1.4.0
# grouped by conventional-commit prefix
git log --format="%s" v1.3.0..v1.4.0 | sed -n 's/:.*//p' | sort | uniq -c | sort -rn
# publish a release with the built artefacts (GitHub CLI)
gh release create v1.4.0 dist/app.zip --notes-file RELEASE_NOTES.md- A
0.xversion signals an unstable API: a MINOR bump there is allowed to break callers. - Never reuse a published version number. Consumers cache artefacts, and two different builds under one number are impossible to untangle.
- Tag the commit you actually built and shipped — on the release branch, not necessarily on the merge commit if the two differ.
- Generate the raw list from
git logand then edit it into sentences. A hand-written summary plus a link to the compare view beats a raw commit dump every time.
FAQ
Annotated or lightweight tags?
Annotated for anything you publish or describe: it carries a message, tagger and date, can be signed, and works with
git describe. Lightweight tags are fine as private bookmarks and quick local marks.How should we maintain the changelog?
Generate the list of commits between the last tag and
HEAD, then rewrite it into human sentences grouped by type. Keep the raw list available as a link to the compare view for anyone who wants the detail.Related
Forks, pull requests and review workflows Hooks, aliases and automation
Last refreshed 2026-09-18.