Tags, Releases & Semantic Versioning
What Are Tags?
A Git tag is a named pointer to a specific commit. Unlike a branch (which moves forward as you make new commits), a tag is fixed — it always points to the same commit. Tags are used to mark specific points in history as being important: usually releases.
If you have used any open-source software, you have seen tags in action. When numpy releases version 2.0.0, they create a tag v2.0.0 pointing to exactly the commit that was released. Anyone who wants that exact version can check out the tag. The tag never moves — v2.0.0 always means the same commit.
Lightweight vs Annotated Tags
Git has two types of tags with meaningfully different behaviors:
Lightweight Tags
A lightweight tag is simply a name pointing to a commit — like a branch that never moves. It stores no additional information.
Lightweight tags are quick and simple. They are appropriate for temporary or private marks — "this is the commit I was testing when I filed this bug report."
Annotated Tags
An annotated tag is a full Git object in the object database. It contains:
- The tagger's name and email
- The date the tag was created
- A tag message (like a commit message)
- Optionally, a cryptographic signature (GPG)
The key difference: annotated tags have metadata and are treated as first-class objects. git describe only uses annotated tags by default. Push operations with --follow-tags push annotated tags automatically. For releases, always use annotated tags.
Listing Tags
Viewing a Tag's Details
For an annotated tag, this shows:
- The tag object details (tagger, date, message)
- The commit object (author, date, commit message)
- The diff of that commit
For a lightweight tag, it shows only the commit details.
Checking Out a Tag
This puts you in "detached HEAD" state — HEAD points directly to the commit, not to a branch. You can look at the code and run it, but any commits you make will not be on any branch and may be lost.
If you want to make changes based on a tagged release (e.g., a hotfix to a specific version):
Pushing Tags to a Remote
By default, git push does not push tags. You must push them explicitly:
The --follow-tags option is the safest approach: it pushes only annotated tags that are reachable from the commits being pushed. Use it when pushing release commits.
Verifying Tags on the Remote
After pushing:
Deleting Tags
Deleting tags is uncommon for release tags — it breaks anyone who has a reference to that tag. Reserve deletion for tags created by mistake or for non-release markers.
Semantic Versioning (SemVer)
Semantic Versioning is the standard versioning scheme used by the vast majority of modern software projects. The version number has the format:
For example: 2.14.3, 1.0.0, 0.9.1-beta.1
The Three Numbers
MAJOR version — incremented when you make incompatible API changes. Signals to users: "things that worked before may not work now." Users must actively review your changelog before upgrading.
Examples:
- Removing a function from your public API
- Changing the signature of an existing function
- Changing the database schema in a way that breaks existing data
MINOR version — incremented when you add functionality in a backward-compatible manner. Signals to users: "new things are available, but nothing you relied on changed."
Examples:
- Adding a new command to taskr
- Adding a new optional parameter to an existing function
- Adding a new configuration option
PATCH version — incremented when you make backward-compatible bug fixes. Signals to users: "something broken now works; nothing else changed."
Examples:
- Fixing a crash when tasks file is missing
- Correcting an off-by-one in task numbering
- Fixing a typo in output messages
Pre-release and Build Metadata
SemVer also supports pre-release identifiers and build metadata:
Pre-release versions have lower precedence than the release version: 1.0.0-rc.1 < 1.0.0.
Practical SemVer for Developers
When you are working on a library or API:
- Start at
0.1.0if the API is not yet stable - Move to
1.0.0when the API is stable and you are ready to commit to backward compatibility - Every release after
1.0.0follows the MAJOR.MINOR.PATCH rules strictly
When in doubt:
- Is any existing behavior changing? If yes: at least MINOR, possibly MAJOR
- Are you only fixing bugs? If yes: PATCH
- Are you removing or changing anything users depend on? If yes: MAJOR
git describe — Generating Version Strings
git describe generates a human-readable name for any commit based on the most recent annotated tag reachable from it:
On an exact tag: v1.0.0
Between tags: v1.0.0-3-g7a8b9c0
This reads as: "3 commits after tag v1.0.0, with commit hash 7a8b9c0".
git describe is commonly used in build systems to embed version information:
The --dirty flag appends -dirty if there are uncommitted changes, signaling the build is from a modified working tree.
A Release Workflow Using Tags
Here is a practical release workflow that professional teams use:
Step 1: Prepare the Release
Step 2: Update the Version Number
If your project has a version file (package.json, pyproject.toml, VERSION, setup.py), update it:
Step 3: Create the Annotated Tag
Step 4: Push the Release
Step 5: Create a GitHub Release (Optional)
If your project is on GitHub:
Or go to GitHub → Releases → Draft a new release, select the tag, and fill in the release notes.
Maintaining Multiple Versions
If you need to maintain older versions (provide security fixes for v1.x after releasing v2.0):
Practical Exercises
Exercise 1: Tag Your taskr Releases
Exercise 2: Push Tags
Exercise 3: Practice SemVer Decisions
For each scenario below, decide whether it warrants a PATCH, MINOR, or MAJOR version bump:
- You fix a bug where
taskr donecrashes if the task number is out of range - You add a new
taskr exportcommand that exports tasks to CSV - You change the task file format from plain text to JSON (breaking existing task files)
- You fix a typo in the help output
- You add an optional
--colorflag to thelistcommand - You change the
addcommand to require a--textflag instead of a positional argument
Write your answers and reasoning before checking: 1=PATCH, 2=MINOR, 3=MAJOR, 4=PATCH, 5=MINOR, 6=MAJOR.
Exercise 4: Use git describe
Summary
- Lightweight tags are simple name→commit pointers. Annotated tags are full objects with tagger info, date, and message. Always use annotated tags for releases.
git tag -a v1.0.0 -m "message"creates an annotated tag at the current commit.git tag -ddeletes locally;git push origin --delete <tag>deletes remotely.- Tags are not pushed by default. Use
git push origin --follow-tagsto push annotated tags along with commits. - Semantic Versioning: MAJOR for breaking changes, MINOR for new backward-compatible features, PATCH for bug fixes.
git describegenerates a human-readable version string based on the nearest annotated tag, useful in build scripts.- A typical release workflow: update version number → commit → create annotated tag → push with
--follow-tags.
The next lesson dives into Git's internals: how the object database works, what blobs, trees, and commits actually are, and how this knowledge makes you a more effective Git user.