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
LightweightAnnotated
What it isA plain reference to a commitA separate tag object with metadata
MessageNoneYes, like a commit message
SignatureCannot be signedCan be signed with -s
git describeIgnored by defaultUsed to name builds
Best forTemporary local marksAnything 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 tags

The 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

ChangeBumpExample
Incompatible API changeMAJORv1.4.2 to v2.0.0
New backwards-compatible featureMINORv1.4.2 to v1.5.0
Backwards-compatible bug fixPATCHv1.4.2 to v1.4.3
Internal refactor, same behaviourNoneVersion unchanged
Pre-releaseSuffixv2.0.0-rc.1
Build metadataSuffix, ignored when orderingv1.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.x version 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 log and 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.

Forks, pull requests and review workflows Hooks, aliases and automation

Last refreshed 2026-09-18.