On May thirtieth, two thousand eleven, Linus Torvalds sent an email to the Linux kernel mailing list that would have been unremarkable coming from anyone else. He was releasing a new version of the kernel. But the version number was wrong. The previous release had been two-point-six-point-thirty-nine. The next should have been two-point-six-point-forty. Instead, Linus called it three-point-zero.
No major rewrite. No architectural overhaul. No breaking changes. Just a new number. And yet it made headlines in every technology publication on the planet.
Why? Because version numbers mean something. Or at least, we act like they do. And the story of how we got from arbitrary labels to a global social contract about what a dot and a digit communicate is one of the stranger chapters in the history of software.
It starts, like so many things in this series, with Git. Because Git is the tool that actually creates version numbers. Every time a maintainer types git tag and presses enter, they are making a promise to everyone who uses their code. And the question of what that promise means has been debated, codified, satirized, and occasionally ignored for decades.
When we left Git's internals back in episode five, we talked about the four building blocks: blobs, trees, commits, and tags. The first three do the mechanical work of tracking files, directories, and snapshots. Tags are different. Tags are for humans.
A tag in Git is a permanent marker. It says: this specific commit, this exact snapshot of the entire project, has a name. Not a branch name that moves forward with each new commit, but a fixed name that points to one moment in time forever. Branches are rivers. Tags are monuments.
Git offers two kinds of tags, and the difference matters more than most developers realize. A lightweight tag is just a pointer, a name that points to a commit. Nothing more. You type git tag v-one-point-oh, and Git creates a reference. No metadata, no record of who created it or when or why. It's a sticky note on a commit.
An annotated tag is something richer. You type git tag dash-a v-one-point-oh, and Git creates a full object in its database. That object contains the tagger's name, the date, a message explaining what this release is, and optionally a cryptographic signature proving the tag is authentic. An annotated tag is not a sticky note. It's a signed certificate.
The distinction seems academic until you think about trust. When you download version two-point-three-point-one of a library that your entire application depends on, you're trusting that the person who created that tag had the authority to release it, that they tested it, that they stand behind it. A lightweight tag gives you none of that provenance. An annotated tag gives you a name, a date, and optionally a mathematical proof.
Most projects use annotated tags for releases and lightweight tags for internal bookmarks. But plenty of projects use lightweight tags for everything, which means their release history is a collection of anonymous sticky notes. Git doesn't enforce a policy here. It gives you both tools and lets you choose.
There's one more command worth knowing. Git describe looks at your current commit and tells you where you are relative to the nearest tag. If you're exactly on a tagged commit, it returns the tag name. If you're three commits past version two-point-one, it returns something like v-two-point-one-dash-three, the tag name plus the number of commits since that tag. It's Git's way of answering the question: how far are we from the last release? And it only works if you've been creating tags. Without them, Git has no landmarks to measure against.
By the late two thousands, the open source world had a versioning problem. Every project chose its own numbering scheme. Some used dates. Some used sequential integers. Some used whatever felt right that morning. There was no shared language for what a version bump meant. If your dependency jumped from one-point-two to one-point-three, did that mean new features? Bug fixes? Breaking changes that would destroy your build? You had no way of knowing without reading the changelog, assuming there was one.
Python's jump from two to three in two thousand eight showed how bad it could get. The version number looked like a routine bump. But the change was catastrophic. Strings became Unicode by default. Print turned into a function. Entire libraries stopped working overnight. The migration took over a decade. Libraries had to maintain two versions simultaneously. Companies delayed upgrading for years. The Python community split into camps, and the pain was so severe that Guido van Rossum, the language's creator, said there would never be a Python four. The version number was not just a label. It was a trauma.
This was the world that needed a contract.
Tom Preston-Werner, who we met back in episode ten as one of GitHub's co-founders, was living this chaos daily. GitHub was growing fast, pulling in thousands of open source projects, and the version numbers on those projects were meaningless noise. A major version bump in one project meant nothing. A patch version bump in another project could break everything downstream.
So he wrote a specification. Not software, not a tool. Just a document. A social contract written in plain English that said: here is what version numbers should mean, and here is how they should change.
He called it Semantic Versioning, and the rules are deceptively simple. A version number has three parts separated by dots: major, minor, patch. Bump the patch number when you fix bugs without changing any behavior. Bump the minor number when you add new features that don't break existing behavior. Bump the major number when you make changes that break backward compatibility.
That's it. Three rules. And those three rules created a language that millions of developers now use to communicate trust across projects they will never personally interact with. When you see a library go from two-point-three-point-one to two-point-three-point-two, semver promises you: nothing broke. You can upgrade safely. When it goes from two-point-three to two-point-four, semver promises: there's something new, but everything you rely on still works. When it goes from two to three, semver warns: something changed, and you need to check your code.
Either SemVer means something, or it doesn't. I choose to believe that it does.
The specification went through its own versions. Tom published the first draft around two thousand nine. It sat on his blog for months. Then npm, the package manager for Node.js, adopted it as a default. Suddenly, every JavaScript developer had to follow semver's rules. Rust's Cargo followed. So did RubyGems and Composer. By two thousand fourteen, semver was not just a suggestion. It was the law of the land for anyone who wanted their code to be used. When you run npm install and it automatically picks compatible versions, it is trusting that every maintainer in your dependency tree is following the semver contract.
The brilliance of semver is that it turned version numbers from labels into communication. Before semver, a version number told you almost nothing. After semver, a version number tells you exactly what kind of change to expect. It is not just bookkeeping. It is empathy encoded in three integers.
Not everyone plays by semver's rules. The most famous version number rebel is Linus Torvalds himself.
The Linux kernel has never followed semantic versioning. It can't, really. The kernel has thousands of internal interfaces, and almost every release changes some of them. If the kernel followed strict semver, every release would be a major version bump, and the version number would be somewhere in the thousands by now.
Instead, the kernel has used its own scheme. For most of its history, the convention was that even minor numbers meant stable releases and odd minor numbers meant development releases. So version two-point-four was stable, two-point-five was the development branch leading to two-point-six.
Then two-point-six happened, and it never ended. The kernel stayed on two-point-six from December two thousand three through May two thousand eleven, nearly eight years. Minor releases piled up: two-point-six-point-twelve, two-point-six-point-twenty, two-point-six-point-thirty, two-point-six-point-thirty-nine. The numbers were getting ridiculous.
The real reason is just that I can no longer comfortably count as high as forty.
That was Linus's explanation for bumping to three-point-oh. Not a technical milestone. Not a feature celebration. He was just tired of the number being so big. The twentieth anniversary of Linux was coming up, which made a nice narrative, but Linus was characteristically blunt about the real motivation: the minor version number had gotten unwieldy, and he wanted a fresh start.
This drove some people nuts. Version numbers were supposed to mean something. A jump from two to three should signal a fundamental change. Linus was treating the major version number like an odometer rolling over, a cosmetic reset with no semantic content. But that was exactly his point. The Linux kernel's version numbers had never carried semantic meaning in the way semver imagines. They were labels, not contracts. And when a label stops being useful, you change it.
He did the same thing again in two thousand nineteen, jumping from four-point-twenty to five-point-oh. And again in early two thousand twenty-five, going from six-point-nineteen to seven-point-oh. Each time, the stated reason was the same: the minor number was getting uncomfortably high. No breaking changes, no grand rewrite. Just a preference for smaller numbers.
This isn't a failure of versioning. It's a different philosophy. Semver treats version numbers as a contract between maintainer and user. Linus treats them as human-readable labels that should be convenient to work with. Both approaches have logic behind them. The tension between them reveals something important: there is no universal agreement on what a version number is for.
Here is where versioning gets philosophical. When should a project reach one-point-oh?
In semver's world, zero-point-anything means the software is pre-release. The API isn't stable. Anything can change at any time. There are no promises. Zero is a disclaimer: use this at your own risk.
One-point-oh is a commitment. It says: this is stable. This is real. We will not break your code without warning. If we do break it, we will increment the major version so you know. One-point-oh is the moment a project goes from "something we're building" to "something you can depend on."
And this is exactly why so many projects never get there.
Mahmoud Hashemi created a satirical website called ZeroVer, cataloguing what he calls zero-based versioning: the practice of keeping your major version at zero forever. The site lists over two hundred projects that have adopted this approach, many of them critical infrastructure used by millions of developers. HashiCorp's Terraform, which manages cloud infrastructure for some of the largest companies in the world, lived at zero-point-something for years. FastAPI, one of the most popular Python web frameworks, still sits at zero-point-something. Even Tom Preston-Werner's own project TOML, the configuration file format he created after GitHub, stayed at zero-point-five from two thousand thirteen to two thousand twenty-one.
Our collective hesitance to bump the major version of a package is so strong that we sometimes concoct elaborate justifications as to why a breaking change can be included in a minor version release.
Preston-Werner was calling out the whole industry, himself included. The fear is real. Calling something one-point-oh feels like a promise you might not be able to keep. What if there's a bug? What if the API is wrong? What if you need to make a breaking change next month? Better to stay at zero, where the rules are loose and expectations are low. ZeroVer is comfortable. One-point-oh is a commitment.
But Tom argued in a two thousand twenty-two blog post that major version numbers are not sacred. They're not marketing events. They're not promises of perfection. They're just signals. Going from one to two means something broke. Going from two to three means something else broke. The numbers can be high. That's fine. He suggested using code names for marketing milestones, the way Ubuntu uses animal names, and letting version numbers do their mechanical job of communicating compatibility.
And then there are the version numbers that carry so much weight they break communities in half. Perl six was announced in two thousand, designed as the next version of Perl. But it diverged so far from Perl five that it became a different language entirely. There was no upgrade path. No migration guide. In two thousand nineteen, nearly twenty years after the announcement, Perl six was renamed to Raku, a separate language with a separate identity, because the version number had been a lie all along. It was never version six of anything. It was version one of something new, wearing its predecessor's name like an ill-fitting coat.
Every one of these stories is about the same thing: the gap between what a version number technically means and what people emotionally hear. Semver says a major version bump means breaking changes. Developers hear "the old way is dead." Semver says zero-point-anything means pre-release. Developers hear "not ready." Semver says the numbers are mechanical signals. But humans don't process numbers mechanically. We attach meaning, expectation, and fear to them.
This is why version numbers matter. Not because of some abstract specification, but because they are one of the few channels of communication between a maintainer and every person who depends on their code. A well-chosen version number says: I understand that you built something on top of my work, and I respect that relationship enough to tell you clearly when something changes. A careless version number says: good luck.
Git doesn't care about any of this. Git just stores tags as objects. Lightweight or annotated, semver or not, meaningful or arbitrary. Git is a content-addressed storage system. It holds your tags the same way it holds your commits: as data, without judgment.
But when you type git tag dash-a v-one-point-oh and write a message explaining what this release means and why it matters, you're doing something that no machine can do for you. You're making a promise to people you've never met, that this version of your code is worth depending on, that you've thought about what changed and what didn't, and that you respect the downstream developers enough to tell them the truth in three small numbers.
That is not a technical act. That is an act of empathy. It is the same impulse that drove the first code librarians, back in Act One, to carefully label their magnetic tapes, a desire to leave a clear trail for the next person. And as we will see when we examine supply chain security and incidents like the XZ backdoor, this trail of trust, signed tags, verified releases, becomes the bedrock of everything we build. In a world where software depends on software depends on software, stacked hundreds of layers deep, that empathy is the only thing holding the tower together.
Git tag is how a maintainer says "this one counts." Two words, a name, and suddenly a commit stops being just another node in the graph. It becomes a release. The annotated form, git tag dash a, adds a signature: who tagged it, when, and why. Most developers use lightweight tags for everything and wonder why their release history feels like a pile of anonymous sticky notes. The difference matters more than you think, because tags are the only part of Git that exists purely for human trust, not for the machine.