Semantic Versioning: The Art of Not Breaking the Internet One Dot at a Time 🔢
Semantic Versioning: The Art of Not Breaking the Internet One Dot at a Time 🔢
Confession: I once bumped a Laravel package from 1.4.2 to 2.0.0 and forgot to update the changelog. Twelve people filed issues in 48 hours. One was just "WHY" in all caps. Just... "WHY". 😭
That incident taught me more about open source responsibility than any tutorial ever could.
As a full-time developer who contributes to open source, semantic versioning is the one concept I wish someone had drilled into me before I shipped my first public package. It looks deceptively simple — three numbers, separated by dots. But get it wrong and you're the person who broke a thousand production builds before breakfast! 🌅💥
So What Even IS Semantic Versioning? 🤔
It's a versioning scheme that follows the format MAJOR.MINOR.PATCH. Example: 3.7.2.
Here's the contract you make with your users:
MAJOR → Breaking changes. Users WILL need to update their code.
MINOR → New features, backwards compatible. Safe to upgrade.
PATCH → Bug fixes only. Absolutely safe, go update right now.
That's it. Three numbers. A promise. And a whole lot of drama when you break that promise.
The formal spec lives at semver.org and was authored by Tom Preston-Werner (co-founder of GitHub). So yeah, it comes from the house that built the tool you use to manage code. Pay attention! 😄
The Horror Stories Start at 1.0.0 👻
Here's where it gets fun (for me, retroactively).
Scenario A: The "oops I thought this was a patch" release
You fix a bug in a method. Looks innocent. But you also renamed the return type from array to Collection. You ship 1.2.5 → 1.2.6. Now everyone's code that did is_array($result) is broken.
Congratulations! You snuck a breaking change into a patch release. You are now Enemy #1 on three Slack channels! 🔥
Scenario B: The "it's just a new feature" trap
You add a required parameter to an existing function signature. "But I added a feature!" Yes, and you also silently broke every single caller of that function. That's a MAJOR bump. Not minor. MAJOR!
Scenario C: The 0.x.y wild west
Versions below 1.0.0 are technically a free-for-all. Anything can change in a minor version bump. But nobody reads the fine print, and they treat 0.9.1 → 0.9.2 as a safe patch. Then chaos.
In the security community, we call this "security through obscurity" — the thing that's supposed to protect you, but actually just hides the danger. 0.x.y packages have the same energy! 🔐
My Personal Semver Journey 🛤️
Balancing work and open source taught me discipline fast. When you're maintaining a package that 300 people depend on, you cannot afford to ship recklessly. Every version bump needs to be intentional.
My workflow evolved to this:
Before every release, I ask myself three questions:
- Does any existing behavior change? → MAJOR bump.
- Am I adding something new that doesn't break old code? → MINOR bump.
- Am I purely fixing a bug without changing the interface? → PATCH bump.
Sounds obvious written out. But in the heat of a coding session at 11pm? You'll convince yourself "it's fine, just a patch." Reader, it was not fine. 😅
The Tools That Save Your Sanity 🛠️
The good news: you don't have to manage this entirely in your head. The open source ecosystem has excellent tooling for semver.
semantic-release 🤖
This is the one I use for Node.js packages. It reads your conventional commit messages and automatically determines the next version number, writes a changelog, and publishes to npm.
npm install --save-dev semantic-release
# Your commits become your version bumps:
# feat: add new auth method → MINOR bump (1.2.0 → 1.3.0)
# fix: handle null user edge case → PATCH bump (1.3.0 → 1.3.1)
# feat!: remove deprecated API → MAJOR bump (1.3.1 → 2.0.0)
The ! after the type = breaking change. Simple, powerful, and now your CI does the thinking for you! 🎉
Conventional Commits
This pairs beautifully with semantic-release. The spec is straightforward:
<type>[optional scope]: <description>
fix: correct off-by-one error in pagination
feat: add OAuth2 provider support
feat!: rename config keys to snake_case
Types: fix, feat, build, chore, ci, docs, style, refactor, perf, test.
Once you adopt this on your projects, your git log becomes a readable history AND an automatic release mechanism. Two birds, one very elegant stone! 🪨🐦🐦
semver npm package / PHP equivalent
For checking version constraints programmatically:
const semver = require('semver')
semver.satisfies('1.2.3', '~1.2.0') // true
semver.satisfies('2.0.0', '^1.0.0') // false — breaking change!
semver.diff('1.0.0', '1.1.0') // 'minor'
semver.diff('1.0.0', '2.0.0') // 'major'
In PHP/Composer land, the constraints work similarly. I use this inside package test suites to verify version resolution logic. Paranoid? Maybe. But my packages haven't broken production in 18 months! 🏆
Version Ranges: The Part Users Get Wrong Too ⚠️
It's not just maintainers who need to understand semver. Users specifying dependencies need to too.
"^1.2.3" // Compatible with 1.x.x — safe for minor/patch updates
"~1.2.3" // Approximately equivalent — only patch updates
"1.2.3" // Pinned exact version — you're on your own, pal
">=1.0.0" // Danger zone unless you really know what you're doing
"*" // You enjoy chaos and suffering
I've seen "*" used in package.json files. In production. I have chosen not to name names. 😐
The Open Source Responsibility Angle 🌍
Here's the thing that hit me hardest when I started maintaining packages: people trust you.
When a developer runs composer update or npm update, they're trusting that you followed the rules. That your PATCH really is just a bug fix. That your MINOR really is backwards compatible.
Break that trust once, and they pin your package to an exact version forever. You become the "do not touch" entry in their lockfile. The haunted house on their dependency street. Nobody wants that. 😂
In the security community, responsible disclosure is about honoring a process that protects users. Semver is the same concept applied to software releases — it's a process that protects users from surprise breakage. Skip it and you're basically dropping unannounced breaking changes on unsuspecting developers. That's not shipping software, that's running a denial-of-service attack on your own users! 😤
How to Get Started as a Contributor 🚀
If you want to contribute to projects that take semver seriously, look for these signals:
- Has a
CHANGELOG.md— They track changes properly - Uses conventional commits — Disciplined release process
- Has release automation in CI — They don't cut corners on versions
1.0.0or higher — They've committed to the stability promise
Good projects to study: Laravel (masters of deprecation and major version planning), Symfony (strict semver with LTS releases), React (excellent communication around breaking changes).
Want to practice? Help a project adopt conventional commits! It's usually a docs/workflow change, perfect as a first contribution, and maintainers LOVE it. 🤝
TL;DR — The Semver Survival Guide 📋
- MAJOR = breaking change. Communicate it loudly. Write a migration guide.
- MINOR = new feature, stays backwards compatible. No surprises.
- PATCH = bug fix only. The safest update a user can make.
- Never sneak breaking changes into a minor or patch release. It's not just bad manners — it destroys trust.
- Use
semantic-release+ conventional commits and let the robots handle version numbers. - Read semver.org — it takes 10 minutes and will save you hours of angry issues.
- Before shipping: ask yourself "does this break anyone?" If yes, it's a MAJOR bump. No exceptions!
Your version number is a promise. Keep it. 🤝
Got a semver horror story? I know you do. Drop it in the comments or find me on GitHub — misery loves company, and I genuinely want to hear about the time you accidentally broke npm for a Tuesday afternoon! 😄
Want to see semver done right? Browse the release history of any mature Laravel or Symfony package. Maintainers there treat version numbers like sacred text.
Now go audit your package.json — I guarantee you'll find at least one "*" lurking in there. 🕵️♂️
P.S. The "WHY" issue I mentioned at the start? They were right. I had no excuse. I wrote the migration guide, apologized in the release notes, and have never shipped a bad major bump since. Humility is the best teacher in open source. 💚