Versioning & Releases
Version numbers communicate expectations. When a user sees a jump from v2.3.1 to v2.4.0, they expect new features but no breakage. When they see v3.0.0, they prepare for breaking changes and check the migration guide. Semantic versioning makes this possible by encoding meaning into the version number itself. Combined with automated releases and clear changelogs, a disciplined versioning strategy builds trust with your users and reduces the maintenance burden of shipping updates.
Semantic Versioning
Semantic versioning (SemVer) is the dominant versioning scheme in open source. A version number takes the form MAJOR.MINOR.PATCH, and each component has a precise meaning.
MAJOR.MINOR.PATCH
MAJOR — increment when you make breaking changes
Users must change their code, configuration, or behavior to upgrade.
Examples: removing a public API method, changing a function's
return type, dropping support for a platform.
MINOR — increment when you add new features (backward compatible)
Users can upgrade without changing anything. New functionality
is available but not required.
Examples: adding a new CLI flag, supporting a new output format,
adding a new method to a class.
PATCH — increment when you fix bugs (backward compatible)
No new features, no breaking changes. Just fixes.
Examples: fixing a crash, correcting a calculation error,
patching a security vulnerability.
SemVer Rules
Core rules:
1. Once a version is released, it is immutable.
You cannot change the contents of v2.3.1 after publishing.
If you made a mistake, release v2.3.2.
2. Major version zero (0.x.y) is for initial development.
Anything may change at any time. The public API is not stable.
This is where most projects start.
3. Version 1.0.0 defines the public API.
Once you release 1.0.0, you are committing to backward
compatibility within major versions.
4. Patch version must only contain backward-compatible bug fixes.
A bug fix is a change that corrects incorrect behavior.
5. Minor version must be incremented for new backward-compatible
functionality. It may include patch-level changes.
6. Major version must be incremented for any backward-incompatible
change. It may include minor and patch-level changes.
7. Precedence: 1.0.0 < 1.0.1 < 1.1.0 < 2.0.0
This ordering is how package managers resolve dependencies.
The 0.x Problem
Many projects stay on 0.x for years because incrementing to 1.0.0 feels like a commitment. This is understandable but problematic. Users cannot distinguish between "not yet stable" and "stable but the maintainer is afraid of 1.0."
When to release 1.0.0:
You should release 1.0.0 when:
- The project is used in production by real users
- The API has been stable for several releases
- You are willing to follow SemVer properly going forward
You should NOT release 1.0.0 when:
- The API is genuinely in flux and will change frequently
- You are still exploring the design space
- The project is a prototype or proof of concept
The cost of staying on 0.x too long:
- Users treat your project as unstable even when it is not
- Package managers treat 0.x differently (caret ranges behave
differently for 0.x in npm)
- Other projects hesitate to depend on yours
Go famously released 1.0 in 2012 with a strong compatibility promise: code written for Go 1.0 would continue to compile and run correctly in all future Go 1.x releases. This commitment was a major factor in Go's adoption for production systems.
Pre-Release Versions
Pre-release versions let you ship unstable work for testing without affecting stable users. SemVer supports pre-release identifiers appended with a hyphen.
Pre-release version progression:
3.0.0-alpha.1 — early testing, feature incomplete, expect breakage
3.0.0-alpha.2 — more features, still unstable
3.0.0-alpha.3 — feature complete, still unstable
3.0.0-beta.1 — feature complete, looking for bugs
3.0.0-beta.2 — bug fixes from beta testing
3.0.0-rc.1 — release candidate, believed ready for release
3.0.0-rc.2 — last-minute fixes
3.0.0 — stable release
Pre-release precedence:
3.0.0-alpha.1 < 3.0.0-alpha.2 < 3.0.0-beta.1 < 3.0.0-rc.1 < 3.0.0
Package managers handle pre-releases specially:
npm: npm install package@next (pre-release tag)
npm install package@3.0.0-beta.1 (specific version)
pip: pip install package==3.0.0b1
cargo: explicit opt-in required
Alpha releases gather early feedback and expect breakage (React 18 shipped multiple alphas for concurrent features). Beta releases are feature-complete but may have bugs (Next.js ships betas for major versions). Release candidates are believed ready for release and only get critical fixes (Python ships multiple RCs before each minor version).
Changelogs
A changelog is a file (typically CHANGELOG.md) that records notable changes for each version. It exists for users, not developers. If your changelog is a raw git log, it is not a changelog.
Conventional Commits
Conventional Commits is a specification for commit messages that enables automated changelog generation. Each commit message follows a structured format.
Conventional commit format:
type(scope): description
Types:
feat — new feature (maps to MINOR)
fix — bug fix (maps to PATCH)
docs — documentation only
style — formatting, no logic change
refactor — code change that neither fixes nor adds
perf — performance improvement
test — adding or correcting tests
chore — build process, tooling changes
Examples:
feat(cli): add --output flag for JSON export
fix(parser): handle escaped quotes in string literals
docs: update installation instructions for Windows
feat!: remove deprecated --legacy flag
Breaking changes:
Add ! after the type: feat!: remove legacy mode
Or add BREAKING CHANGE in the commit footer
Angular, Vue, and many other projects use conventional commits. The consistency enables tooling that parses commits and generates changelogs automatically.
Tools like conventional-changelog (JavaScript), release-please (Google's GitHub Action), cliff (Rust), and goreleaser (Go) can parse conventional commits and generate changelogs automatically. Auto-generated changelogs are a starting point. For major releases, edit them into something readable.
Changelog format (keepachangelog.com style):
## [3.5.0] - 2026-04-15
### Added
- JSON output format for the export command (#423)
- Autocomplete for CLI commands (#430)
### Fixed
- Crash when parsing files with null bytes (#456)
- Incorrect tokenization of escaped quotes (#461)
### Changed
- Default output format changed from CSV to table (#440)
### Deprecated
- The --legacy flag is deprecated and will be removed in v4.0
### Removed
- Dropped support for Node.js 16 (end-of-life)
### Security
- Updated dependency X to fix CVE-2026-1234
Release Automation
Manual releases are error-prone and time-consuming. Automating the release process reduces mistakes and makes it possible to release frequently.
GitHub Actions Release Workflow
Typical automated release pipeline:
1. Merge PR to main
2. CI runs tests
3. Release workflow triggers:
a. Determine version bump from conventional commits
b. Update version in package.json / Cargo.toml / etc.
c. Generate changelog
d. Create git tag
e. Build artifacts (binaries, packages)
f. Publish to package registry (npm, PyPI, crates.io)
g. Create GitHub Release with changelog and artifacts
4. Announce on social media / blog (optional, manual)
Tool-Specific Release Automation
JavaScript/TypeScript (npm):
- changesets: manages versioning across monorepos
- semantic-release: fully automated, no manual steps
- release-please: creates release PRs for review before publishing
Go:
- goreleaser: builds binaries, creates GitHub releases, publishes
to Homebrew, Snapcraft, Docker, and more
- Configuration in .goreleaser.yml
Rust:
- cargo-release: bumps version, creates tag, publishes to crates.io
- release-plz: automated release PRs with changelog generation
Python:
- python-semantic-release: automated versioning and publishing to PyPI
- flit / poetry: build and publish, version management
For security-sensitive projects, consider signing releases with GPG-signed git tags, Sigstore/cosign for keyless signing, SHA-256 checksums alongside binaries, or SLSA provenance to prove build integrity. GitHub Actions has built-in support for SLSA provenance generation.
Release Cadence
The frequency of releases affects user trust, contributor motivation, and maintenance burden. There is no single right answer, but consistency matters more than frequency.
Release cadence options:
Time-based releases (fixed schedule):
Example: release every 6 weeks (Rust, Firefox)
Advantages: predictable, users can plan upgrades, creates rhythm
Disadvantages: features that are not ready get delayed,
pressure to ship incomplete work
Feature-based releases (when ready):
Example: release when a set of features is complete
Advantages: no artificial deadlines, features ship when done
Disadvantages: unpredictable, users cannot plan, long gaps
between releases feel like abandonment
Continuous releases (every merged PR):
Example: deploy to npm on every merge to main
Advantages: fixes reach users immediately, no release overhead
Disadvantages: users must handle frequent updates, harder to
write changelogs, harder to communicate changes
Hybrid:
Example: nightly/canary for cutting edge, stable releases
on a fixed schedule, LTS for conservative users
Used by: Node.js, Chrome, VS Code
Long-Term Support (LTS)
For projects used in production, LTS releases provide stability for users who cannot upgrade frequently.
LTS strategy:
Current release: v5.x — latest features, latest fixes
LTS release: v4.x — security patches and critical bugs only
End of life: v3.x — no longer maintained
Node.js LTS schedule:
Even-numbered releases become LTS (18, 20, 22)
LTS receives 30 months of support
Odd-numbered releases are current only (19, 21, 23)
When to offer LTS:
- Your project is used in production environments
- Users have long upgrade cycles (enterprises, infrastructure)
- Breaking changes are frequent enough that users need stability
Common Pitfalls
-
Breaking changes in minor or patch releases. This violates semantic versioning and breaks user trust. If you remove a method, it is a major version bump, period. Even if the method was undocumented.
-
Staying on 0.x forever. If your project has real users in production, version 0.x sends the wrong signal. It tells users the API is unstable even when it has been stable for years. Commit to 1.0.
-
No changelog. Users should not have to read git logs to understand what changed. Maintain a CHANGELOG.md or use GitHub Releases to document every version.
-
Manual releases with no automation. Manual steps mean mistakes: forgotten version bumps, missed changelog entries, unpublished packages. Automate everything from version bump to package publishing.
-
Releasing too rarely. Long gaps between releases accumulate risk. A release with 200 changes is harder to debug than 20 releases with 10 changes each. Ship small, ship often.
-
No pre-release testing. Shipping a major version without alpha or beta testing puts your entire user base at risk. Use pre-releases to catch issues before they affect stable users.
Key Takeaways
- Semantic versioning encodes meaning into version numbers: MAJOR for breaking changes, MINOR for new features, PATCH for bug fixes. Follow it strictly to maintain user trust.
- Pre-release versions (alpha, beta, RC) let you test unstable work without affecting stable users. Use them for major releases.
- Conventional commits enable automated changelog generation and version bumping. The small overhead of structured commit messages pays for itself in release automation.
- Automate your release pipeline end-to-end: version bump, changelog generation, artifact building, package publishing, and GitHub Release creation.
- A consistent release cadence builds trust. Whether you release weekly, monthly, or on a fixed schedule, consistency matters more than frequency.
- For projects used in production, consider an LTS strategy that gives conservative users a stable version with long-term security patches.