4 min read
On this page

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.