Thuta Learning
IntermediateDevOps & Toolsintermediate

Tags and Semantic Versioning

What you'll walk away with

  • Explain the core ideas behind Tags and Semantic Versioning
  • Read the diagram and trace how state or data flows through the Git/GitHub workflow
  • Decide how this applies to your own project or team

Build the mental model

A branch label moves forward with every new commit — that's the entire point of a branch. A tag does the opposite: it marks one specific commit permanently, and never moves again no matter how many commits get added afterward.

Tags exist for moments that matter — the exact commit shipped to production, the exact commit a customer is running, the exact commit that closed out a milestone.

Tag typeWhat it is
Lightweight tagA named pointer to a commit, nothing more — `git tag <name>`
Annotated tagA full object with its own author, date, message, and can be signed — `git tag -a <name> -m "..."`

For anything worth keeping or sharing with a team, annotated tags are the better default — they leave a record of who tagged what and why.

Once tags mark meaningful points in history, a team needs a shared naming scheme so a tag name communicates something by itself. That's what semantic versioning solves: a version number written as MAJOR.MINOR.PATCH, like v2.4.1.

  • MAJOR — a breaking change that could break code depending on you
  • MINOR — a new feature added in a backward-compatible way
  • PATCH — a bug fix that changes no behavior anyone relies on

Convention, not enforcement

Git enforces none of this. You could tag a single typo fix v99.0.0 and Git would not object — semantic versioning is a convention humans and tools agree to follow, not a rule Git checks.

Tag
A named pointer to one specific commit that never moves, used to mark meaningful points like releases.
Semantic Versioning
A version-numbering convention, MAJOR.MINOR.PATCH, where each segment signals a specific kind of change — breaking, feature, or fix.
text
TAG VS BRANCH: WHO MOVES
------------------------
TAG VS BRANCH: WHO MOVES
-------------------------
main (branch) ---------------------------> moves forward
  |
  o---o---o---o---o---o   <- new commits keep landing here
              ^
              |
           v1.0.0 (tag)   <- stays pinned to this one commit
                             forever, even as main moves on

Connect it to a real scenario

Tag the commit

On the commit you want to mark, run `git tag v1.0.0` for a lightweight tag or `git tag -a v1.1.0 -m "message"` for an annotated one.

List tags

`git tag` alone lists every tag in the repo, alphabetically.

Inspect a tag

`git show v1.0.0` prints the commit it points to; for annotated tags it also shows the tag's own author, date, and message first.

Push the tag

Push it explicitly with `git push origin v1.1.0`, or push everything with `git push --tags`.

Tags don't push automatically

A plain `git push` sends commits, not tags. Teammates won't see a new tag until you push it explicitly.

Try the working example

bash
git init -q -b main
git config user.name "Thuta Learner"
git config user.email "learner@example.com"

export GIT_AUTHOR_NAME="Thuta Learner"
export GIT_AUTHOR_EMAIL="learner@example.com"
export GIT_COMMITTER_NAME="Thuta Learner"
export GIT_COMMITTER_EMAIL="learner@example.com"

echo "console.log('v1');" > app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T09:00:00" GIT_COMMITTER_DATE="2026-01-01T09:00:00" \
  git commit -m "Initial release logic"

echo "console.log('v1.1');" >> app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T10:00:00" GIT_COMMITTER_DATE="2026-01-01T10:00:00" \
  git commit -m "Add greeting feature"

echo "console.log('fix');" >> app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T11:00:00" GIT_COMMITTER_DATE="2026-01-01T11:00:00" \
  git commit -m "Fix off-by-one bug"

git log --oneline

git tag v1.0.0 HEAD~2
git tag -a v1.1.0 -m "Add greeting feature and bug fix"

git tag
git show v1.0.0 --stat
git show v1.1.0 --stat
You should see
$ git log --oneline
fba08e9 Fix off-by-one bug
7744f86 Add greeting feature
9b07fe7 Initial release logic

$ git tag
v1.0.0
v1.1.0

$ git show v1.0.0 --stat
commit 9b07fe7c5e1dc5e087a0f2c53bf8bd78caf6e93f
Author: Thuta Learner <learner@example.com>
Date:   Thu Jan 1 09:00:00 2026 +0900

    Initial release logic

 app.js | 1 +
 1 file changed, 1 insertion(+)

$ git show v1.1.0 --stat
tag v1.1.0
Tagger: Thuta Learner <learner@example.com>
Date:   Thu Jan 1 11:30:00 2026 +0900

Add greeting feature and bug fix

commit fba08e984fb5ba335e158b32b88bdcd5f003ea7b
Author: Thuta Learner <learner@example.com>
Date:   Thu Jan 1 11:00:00 2026 +0900

    Fix off-by-one bug

 app.js | 1 +
 1 file changed, 1 insertion(+)

5-minute try-it

In a repo of your own, make two commits, then tag the earlier one as v1.0.0 (lightweight) and the later one as v1.1.0 (annotated, with a message describing what changed). Run `git tag` to confirm both exist, then use `git show` on each and note the difference in what gets printed for a lightweight tag versus an annotated one.

One important caution

Tags don't move — if you tag the wrong commit, you have to delete the tag (`git tag -d`) and re-create it; there's no 'update in place'.

A plain `git push` does not send tags to the remote; forgetting `git push --tags` means teammates never see the tag you created.

Git Documentation: git-tagGit & GitHub

Easy traps

  • Tags don't move — if you tag the wrong commit, you have to delete the tag (`git tag -d`) and re-create it; there's no 'update in place'.
  • A plain `git push` does not send tags to the remote; forgetting `git push --tags` means teammates never see the tag you created.
  • Always run `git status` before any destructive or history-rewriting command, to know exactly what state you're in.

Exercise

In a repo of your own, make two commits, then tag the earlier one as v1.0.0 (lightweight) and the later one as v1.1.0 (annotated, with a message describing what changed). Run `git tag` to confirm both exist, then use `git show` on each and note the difference in what gets printed for a lightweight tag versus an annotated one.

You'll know it worked when: $ git log --oneline fba08e9 Fix off-by-one bug 7744f86 Add greeting feature 9b07fe7 Initial release logic $ git tag v1.0.0 v1.1.0 $ git show v1.0.0 --stat commit 9b07fe7c5e1dc5e087a0f2c53bf8bd78caf6e93f Author: Thuta Learner <learner@example.com> Date: Thu Jan 1 09:00:00 2026 +0900 Initial release logic app.js | 1 + 1 file changed, 1 insertion(+) $ git show v1.1.0 --stat tag v1.1.0 Tagger: Thuta Learner <learner@example.com> Date: Thu Jan 1 11:30:00 2026 +0900 Add greeting feature and bug fix commit fba08e984fb5ba335e158b32b88bdcd5f003ea7b Author: Thuta Learner <learner@example.com> Date: Thu Jan 1 11:00:00 2026 +0900 Fix off-by-one bug app.js | 1 + 1 file changed, 1 insertion(+)

Tags and Semantic Versioning | Thuta Learning