Sign in
← Git: workflows
10 min · easy · Lesson 1 of 3

Semantic Versioning

What the numbers in version 2.4.1 mean: MAJOR.MINOR.PATCH, dependency ranges and the link to git tags.

Why versions need rules

The numbers in a version like 2.4.1 carry meaning. When you have a dozen dependencies and each one updates in its own way, you need a shared language: "this version is compatible with the old one" or "this one will break the API". The agreement on how to read these numbers is called Semantic Versioning (SemVer). The full rules are at semver.org.

Without rules, the jump 1.0 → 1.1 could mean anything: a minor fix or a full rewrite. SemVer removes the guesswork.

MAJOR.MINOR.PATCH

A version is three numbers separated by dots:

  2  .  4  .  1
MAJOR MINOR PATCH

The rule is simple: break compatibility, bump MAJOR. Add something without breaking, bump MINOR. Fix something, bump PATCH.

When you bump a number, every number to its right resets to zero. After 2.7.4, a new feature gives 2.8.0, not 2.8.4. A breaking change after 2.7.4 gives 3.0.0. If a release has both fixes and a new feature, the higher level applies: that's MINOR.

Numbers compare as numbers, not as strings. 2.10.0 is newer than 2.9.0, even though a text comparison puts 2.10 before 2.9. Leading zeros are forbidden: 01.2.3 isn't a SemVer version.

Pre-releases and build metadata

Before the first stable release, a version often starts at 0.x. That means "the API isn't stable yet, it can change at any moment".

Pre-releases are marked with a dash suffix: 1.0.0-rc.1, 2.0.0-beta. A pre-release is older than its release: 1.0.0-rc.1 comes before 1.0.0. Numeric parts of the suffix compare as numbers, so the order is:

1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0-rc.2 < 1.0.0-rc.10 < 1.0.0

After a + sign comes build metadata: 1.0.0+build.5. It doesn't affect version order, 1.0.0+build.5 and 1.0.0 are equal for comparison.

Ranges in dependencies

SemVer exists so package managers can update dependencies safely. In package.json you'll see entries like:

"express": "^4.18.2",
"lodash": "~4.17.21"

Without a lock file, npm install picks the newest published version that falls within the range.

The logic relies on SemVer's promise: nothing breaks within the same MAJOR, which is why updating with ^ is relatively safe. If a package doesn't honestly follow SemVer, the promise is worthless. That's why lock files exist (package-lock.json in npm): they record the exact versions that were installed, and the next install from the lock file gives the same dependency tree.

Pre-releases don't fall into a normal range. ^1.0.0 won't take 2.0.0-beta, or even 1.5.0-rc.1: npm only lets a pre-release in when the range itself is written with a pre-release of the same version, for example ^1.5.0-rc.0.

Special case: 0.x versions

The rule "caret keeps MAJOR fixed" stops working when MAJOR is zero. For zero versions, SemVer makes no guarantees: 0.x counts as a period of active development, and a breaking change there is usually shipped in MINOR.

Package managers account for this and narrow the range:

^1.2.3   →  >=1.2.3 <2.0.0     any 1.x no lower than 1.2.3
^0.2.3   →  >=0.2.3 <0.3.0     won't go up to 0.3.0
^0.0.3   →  >=0.0.3 <0.0.4     that is, only this version

The mistake here is expensive precisely because intuition says the opposite. A person reads ^0.2.3, thinks "it'll update within zero, it'll reach 0.9", installs the dependency, and expects fresh versions. But npm honestly keeps it on the 0.2.x branch and won't pull in even a critical fix released in 0.3.1.

Practical takeaway: watch a 0.x dependency by hand. Automation won't bring its updates.

Git tags and releases

A version is pinned in git with a tag. A tag is a named marker on a specific commit. By convention a version tag starts with v:

git tag -a v2.4.1 -m "release 2.4.1"
git push origin v2.4.1

The version itself is 2.4.1, the v is only a prefix on the tag name. The FAQ on semver.org says outright that v1.2.3 isn't a semantic version, but the v prefix is accepted everywhere.

The -a flag creates an annotated tag: besides pointing at a commit, it carries an author, a date, and a message. Releases use this kind of tag. We'll cover tag types in detail in the next lesson.

The tag v2.4.1 points at the commit this release was built from, and after publishing it stays put. Git enforces this: if a tag with that name already exists in the remote repository, a plain push of another tag with the same name is rejected. CI is usually set up so that pushing a tag triggers a build and publishes an artifact for that version. That ties the version number, the commit, and the built image tightly together.

Common mistakes

What you'll need for the tasks

In the next lesson we'll cover tags and releases in git in more detail, and how release pages get built from them.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson git-workflow-01-versioning-semver