Semantic Versioning: Stop Shipping Version 1.0.0 Forever š¢
Semantic Versioning: Stop Shipping Version 1.0.0 Forever š¢
Confession time: I maintained an open source PHP security library for almost two years and literally every release was either 1.0.0, 1.0.1, or 1.0.1-hotfix2. š¬
Users couldn't tell if they were running something ancient or something bleeding-edge. Dependabot opened PRs with zero context. And one very frustrated contributor left a comment: "Does this project even have versioning?"
That was my wake-up call.
As a full-time developer who contributes to open source, release management is the most ignored skill in the entire ecosystem. Everyone obsesses over writing code, opening PRs, and getting stars ā but the moment you ask "how do you version your project?", most people go suspiciously quiet.
Let me fix that! š
What Even IS Semantic Versioning? š
You've definitely seen it: 2.4.1, 0.12.3, 5.0.0-beta.1.
The format is simple: MAJOR.MINOR.PATCH
MAJOR = Breaking changes (stuff WILL break for users)
MINOR = New features (backwards compatible)
PATCH = Bug fixes (nothing breaks)
Real example:
# You added a new helper function ā MINOR bump
v1.2.0 ā v1.3.0
# You fixed a regex bug ā PATCH bump
v1.3.0 ā v1.3.1
# You renamed the main class ā MAJOR bump
v1.3.1 ā v2.0.0
That's literally it. But the CONSEQUENCES of getting it wrong are enormous!
The Day I Broke 47 People's Builds š„
Early in my open source journey, I was maintaining a Laravel request validation helper. Someone opened a PR to rename the main facade from Validator to RequestGuard ā cleaner, more descriptive.
I merged it. Bumped to v1.1.0.
Within 24 hours: 47 GitHub issues. Angry users. Failed CI pipelines. One guy even opened a PR to revert it titled "PLEASE REVERT - production is on fire." š„
I had shipped a BREAKING CHANGE as a minor bump.
Users expected 1.1.0 to be safe to upgrade to. It wasn't. Their composer.json had "^1.0" which happily pulled in 1.1.0 and exploded on deploy.
That incident taught me more about versioning than any blog post ever could.
Balancing work and open source taught me: Your users TRUST version numbers. A wrong bump destroys that trust instantly ā and rebuilding it takes months.
The Semver Rules That Actually Matter šÆ
Rule #1: Start at 0.x.x If You're Still Figuring It Out
# Not ready for stability promises yet?
v0.1.0 # Initial release
v0.2.0 # More features, might break
v0.9.0 # Feature complete, pre-stable
# Ready to commit to stability?
v1.0.0 # The "I promise not to break you" release
The 0.x.x range is your playground. Breaking changes in minor bumps? Fine! Users know 0.x is unstable. But once you ship 1.0.0, the contract is real.
# DO NOT do this to your users
v0.9.9 ā v1.0.0 (fine)
v1.0.0 ā v1.0.1 (fix a typo in README)
v1.0.1 ā v1.1.0 (rename main class) ā THIS IS A BREAKING CHANGE
Rule #2: Everything In CHANGELOG.md š
# Changelog
## [2.1.0] - 2026-03-08
### Added
- `validateRequest()` now accepts custom rule sets
- New `--strict` mode for tighter validation
### Fixed
- Fixed crash when input contains null bytes
## [2.0.0] - 2026-02-14
### Breaking Changes
- Renamed `Validator` facade to `RequestGuard`
- Dropped support for PHP 7.x
### Migration Guide
```php
// Before (v1.x)
use Acme\Validator;
Validator::check($request);
// After (v2.x)
use Acme\RequestGuard;
RequestGuard::check($request);
**In the security community**, we follow strict disclosure timelines and changelogs for CVE patches. The same discipline applies to library versioning ā users need to know exactly what changed and why. š
### Rule #3: Use Git Tags ā Actually Use Them
```bash
# Too many open source devs just... push to main
# and consider it "released"
# The RIGHT way:
git tag -a v2.1.0 -m "feat: custom rule sets for validateRequest"
git push origin v2.1.0
# Even better ā use GitHub Releases
gh release create v2.1.0 \
--title "v2.1.0 ā Custom Rule Sets" \
--notes "See CHANGELOG.md for full details"
GitHub Releases let you:
- Attach binary downloads
- Auto-generate release notes from PRs
- Notify watchers automatically
- Give Dependabot proper context for updates
The Tools That Make This Easy š ļø
standard-version / release-it
npm install --save-dev release-it
# Now releasing is literally one command:
npx release-it
# It will:
# 1. Bump version in package.json
# 2. Generate CHANGELOG.md entries from commits
# 3. Create a git tag
# 4. Create a GitHub Release
For PHP/Composer projects, I use a simple script that reads my commits since the last tag and drafts the CHANGELOG entry. Five minutes to release properly vs. an hour of manual editing.
Conventional Commits (The Secret Sauce š¤«)
# Write commits like this:
git commit -m "feat: add custom rule set support"
git commit -m "fix: crash with null byte inputs"
git commit -m "feat!: rename Validator to RequestGuard"
# ^ the ! means BREAKING CHANGE
# And automated tools can determine your next version:
# feat ā MINOR bump
# fix ā PATCH bump
# feat! or BREAKING CHANGE ā MAJOR bump
I converted my team's commit convention to this format 8 months ago. Now our CI pipeline literally calculates the next version and opens a release PR automatically. Zero manual version debates.
When To Do a Major Release (Without Terror) š°
This is the one that paralyzes everyone. Here's my framework:
Ask yourself:
ā” Does existing code break without user changes? ā MAJOR
ā” Did I remove a public API method? ā MAJOR
ā” Did I change a method signature? ā MAJOR
ā” Did I drop a language/framework version? ā MAJOR
ā” Everything else? ā MINOR or PATCH
Major releases aren't evil ā they're HONEST. A library that never does a major release is either stagnant or secretly breaking users on every "minor" update.
My personal rule: Ship major versions confidently, but always provide:
- A migration guide
- At least 3 months of overlap where both APIs work
- Deprecation warnings in the old code
/** @deprecated Use RequestGuard::check() instead. Will be removed in v3.0. */
public function validate(Request $request): bool
{
// still works, but logs a deprecation warning
trigger_error('Validator is deprecated. Use RequestGuard.', E_USER_DEPRECATED);
return RequestGuard::check($request);
}
The GitHub Release Checklist I Use š
Before every release, I run through this:
Pre-release:
ā” All tests pass on CI
ā” CHANGELOG.md updated with changes
ā” Breaking changes clearly labeled
ā” Migration guide written (for MAJOR)
ā” README updated if needed
Release:
ā” Version bumped in all relevant files
ā” Git tag created and pushed
ā” GitHub Release published with notes
ā” Notify users on Discussions (for breaking changes)
Post-release:
ā” Check Packagist/npm shows new version
ā” Verify Dependabot picks it up
ā” Respond to any quick feedback in issues
Balancing work and open source taught me: The checklist adds 10 minutes. Debugging a bad release costs 10 hours. Do the math! ā±ļø
TL;DR ā The Version Number Contract š”
Your version number is a promise to your users:
| What you changed | What to bump | What users expect |
|---|---|---|
| Bug fix, typo | PATCH | Safe to upgrade immediately |
| New feature, new option | MINOR | Safe to upgrade, get new things |
| Breaking change, removal | MAJOR | Must read migration guide first |
Three things to do this week:
- Audit your current version ā does it accurately reflect your project's stability?
- Write a CHANGELOG.md if you don't have one (use
git logto reconstruct the history) - Try conventional commits on your next three commits ā you'll never go back
The open source projects people TRUST and RELY ON aren't just the ones with great features ā they're the ones that communicate change with honesty and clarity.
Version your code like you'd want others to version theirs. š¤
Got a versioning horror story? I know you do. Drop it in the LinkedIn comments ā I'll personally reply to every one! š
Want to see proper versioning in practice? Check out my GitHub ā my security libraries use everything in this post.
Now go bump that version and ship it! š