Tags, releases, semantic versioning
Intermediate
Why this matters
Version numbers are a promise to whoever depends on your project, and release notes are the one document users actually read. Both are frequently written by the person who writes the documentation, which may well be you.
Semantic versioning
The convention is MAJOR.MINOR.PATCH, and each part is a specific promise:
| Part | Increment when | Promise |
|---|---|---|
| MAJOR | Something that used to work no longer does | Upgrading may require changes to how people use it |
| MINOR | New behaviour is added, nothing existing breaks | Safe to upgrade; there is something new |
| PATCH | A bug is fixed, nothing else changes | Safe to upgrade; nothing new to learn |
So 1.2.0 → 1.2.1 is a fix, 1.2.1 → 1.3.0 adds something, and 1.3.0 → 2.0.0 warns you to read the notes before upgrading.
Two conventions travel with it: 0.x.y means "not stable yet, anything may change", and suffixes like 1.3.0-rc.1 or -beta.2 mark pre-releases, which tools that follow "latest" are supposed to skip.
The changelog
A changelog is a file, usually CHANGELOG.md, listing what changed in each version in the user's language. The way to make it cost nothing is to write each entry when the change is merged, not at release time:
# Changelog
## Unreleased
### Added
- `--difficulty` filter on the trail list (#21).
### Fixed
- The list no longer crashes when a trail has no distance (#15).
## 1.2.0 — 2026-08-14
### Added
- Windows installation instructions (#12).At release time, the "Unreleased" heading becomes the version and the date, and a new empty one is added. That takes two minutes; reconstructing a month of changes from commit messages takes an afternoon.
Categories worth using: Added, Changed, Deprecated, Removed, Fixed, Security. They map onto what a reader wants to know, and "Changed" and "Removed" are the ones that decide the major number.
The release sequence
- Check
mainis green and contains everything the version should have. - Update the changelog: rename "Unreleased" to the version with today's date.
- Decide the number from the changelog's categories: anything under Removed or a breaking Changed means a major bump.
- Commit the changelog:
docs: prepare 1.3.0. - Tag:
git tag -a v1.3.0 -m "Release 1.3.0"andgit push origin v1.3.0. - Create the release page with the notes (lesson 9.9, lesson 10.9).
- Announce it where users are, with a link to the release page.
$ git tag -a v1.3.0 -m "Release 1.3.0: Windows FAQ and licence wording"
$ git push origin v1.3.0To https://gitlab.com/northwind-trails/trailguide.git
* [new tag] v1.3.0 -> v1.3.0Remember that a plain git push does not send tags (command page).
What goes in the notes
Users want outcomes, not commits:
| Instead of | Write |
|---|---|
| "Refactor trail parsing" | Nothing; it is invisible to users |
| "Fix #15" | "The list no longer crashes when a trail has no distance" |
"Add --difficulty" |
"Filter the list by difficulty: --difficulty hard" |
| A list of merged merge requests | Three sentences, plus the list underneath for the curious |
And always include an upgrading note when anything changed name or behaviour, even a small one. That paragraph is the difference between a smooth upgrade and a support queue.
Working out what shipped
| Question | How |
|---|---|
| What is in this release? | git log --oneline v1.2.0..v1.3.0 |
| Is my fix in 1.3? | git tag --contains <hash> |
| What changed for users? | The changelog, if it was kept |
| Which issues? | The milestone attached to the release |
How to do it
$ git switch main && git pull
$ git log --oneline v1.2.0..main
$ git tag -a v1.3.0 -m "Release 1.3.0"
$ git push origin v1.3.0The second command is where the notes start: it is the honest list of what has accumulated.
Git: Create Tag then Git: Push (Follow Tags) in the Command Palette. The release page itself is created on the platform.
Git → New Tag…, or right-click the commit in the Log. Tick Push tags in the push dialog.
Deploy → Releases → New release: choose or create the tag, attach the milestone so the page lists its issues and merge requests, and paste the notes. Code → Tags lists tags separately.
Releases → Draft a new release: Choose a tag, Generate release notes for the merged pull request list, then write the human summary above it and Publish release.
Common mistakes
- Bumping the minor number for a breaking change, which surprises everyone who upgrades.
- Writing the changelog at release time from commit messages.
- Notes written in developer language. "Refactored the parser" tells a user nothing.
- Moving a published tag. Ship a new patch version instead (command page).
- Forgetting
git push origin <tag>, so the release page cannot find the tag. - No upgrading section when something changed name or default.
Try it yourself
Goal: release a version properly, changelog and all.
- Add a
CHANGELOG.mdto your practice project with an "Unreleased" section and two entries, one under Added and one under Fixed. - Decide the version number from those entries and say why.
- Rename the heading to that version with today's date, commit it as
docs: prepare 1.3.0. - Tag and push the tag, then create the release page with notes based on the changelog.
- Run
git log --oneline <previous tag>..v1.3.0and check every user-visible entry appears in your notes.
Expected result: a release page whose notes a user could act on, and a changelog that made writing them a five-minute job.
Show solution
Step 2 is the judgement this lesson is really about. "Added" and "Fixed" alone mean a minor bump; anything under "Changed" or "Removed" that alters existing behaviour means a major one. Getting that wrong is the version-number mistake that actually costs people time.