Versioning Conventions

Versioning Conventions

Why Versioning Matters

Version numbers aren’t just arbitrary labels—they communicate important information about your software. When you see v2.0.0 vs v1.2.3, you should immediately know something significant changed in version 2.

Good versioning helps everyone understand:

  • Users: Whether they can safely upgrade
  • Contributors: What kind of changes are appropriate for a release
  • Maintainers: How to communicate the impact of changes

Without a versioning system, every update is a mystery. With semantic versioning, version numbers tell a story about your project’s evolution.


Semantic Versioning (SemVer) TL;DR

We use Semantic Versioning 2.0.0 for all SEAD Club projects.

Format: MAJOR.MINOR.PATCH (e.g., 1.4.2)

When to increment each number:

MAJOR (1.x.x → 2.x.x)  - Breaking changes (backwards incompatible)
MINOR (x.1.x → x.2.x)  - New features (backwards compatible)
PATCH (x.x.1 → x.x.2)  - Bug fixes (backwards compatible)

Examples:

  • Fixed a bug? → 1.2.31.2.4
  • Added a new feature? → 1.2.41.3.0
  • Changed the API in a breaking way? → 1.3.02.0.0

That’s the essence. Read on for details and edge cases.


The Full Semantic Versioning Spec

Version Number Format

A version number MUST take the form X.Y.Z where:

  • X = MAJOR version (backwards incompatible changes)
  • Y = MINOR version (new features, backwards compatible)
  • Z = PATCH version (bug fixes, backwards compatible)

Each number:

  • Must be a non-negative integer
  • Must NOT have leading zeros
  • Must increase numerically (1.9.0 → 1.10.0 → 1.11.0, not 1.9.0 → 1.10.0 → 1.9.1)

MAJOR Version (Breaking Changes)

Increment MAJOR when you make backwards-incompatible changes to your public API.

Examples of breaking changes:

  • Removing a function or method
  • Changing function signatures (parameters, return types)
  • Renaming public properties or methods
  • Changing expected behavior in ways that break existing code
  • Removing or renaming configuration options

Example:

// v1.x.x
function calculatePrice(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// v2.0.0 - Breaking: now requires tax parameter
function calculatePrice(items, taxRate) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  return subtotal * (1 + taxRate);
}

Users relying on the old signature will get errors, so this requires a major bump.

When you increment MAJOR:

  • Reset MINOR and PATCH to 0
  • Example: 1.9.82.0.0

MINOR Version (New Features)

Increment MINOR when you add functionality in a backwards-compatible way.

Examples:

  • Adding a new function or method
  • Adding new optional parameters (with defaults)
  • Adding new configuration options
  • Marking functionality as deprecated (but not removing it yet)
  • Substantial internal improvements that don’t change the API

Example:

// v1.2.x
function calculatePrice(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// v1.3.0 - New feature: optional discount
function calculatePrice(items, discount = 0) {
  const subtotal = items.reduce((sum, item) => sum + item.price, 0);
  return subtotal * (1 - discount);
}

// Old code still works: calculatePrice(items)
// New code can use: calculatePrice(items, 0.1)

When you increment MINOR:

  • Reset PATCH to 0
  • Keep MAJOR the same
  • Example: 1.2.81.3.0

PATCH Version (Bug Fixes)

Increment PATCH when you make backwards-compatible bug fixes.

A bug fix is an internal change that fixes incorrect behavior.

Examples:

  • Fixing a crash or error
  • Correcting a calculation
  • Fixing unexpected behavior
  • Performance improvements that don’t change the API
  • Security patches

Example:

// v1.2.3 - Bug: doesn't handle empty arrays
function calculatePrice(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

// v1.2.4 - Fixed: now handles empty arrays
function calculatePrice(items) {
  if (items.length === 0) return 0;
  return items.reduce((sum, item) => sum + item.price, 0);
}

When you increment PATCH:

  • Keep MAJOR and MINOR the same
  • Example: 1.2.31.2.4

Special Version Cases

Version 0.x.x (Initial Development)

Before v1.0.0: Anything goes. Your API isn’t stable yet.

During initial development (v0.x.x):

  • Increment MINOR for new features or breaking changes
  • Increment PATCH for bug fixes
  • Don’t worry too much about breaking changes—you’re still figuring things out

Example progression:

0.1.0 - Initial release
0.2.0 - Added user authentication (breaking change from 0.1.0, but that's ok)
0.2.1 - Fixed login bug
0.3.0 - Added profile system
1.0.0 - First stable release with defined API

When to release 1.0.0:

  • Your software is being used in production
  • You have a stable API that users depend on
  • You’re ready to commit to backwards compatibility

Pre-release Versions

For alpha, beta, or release candidate versions, append a hyphen and identifier:

Format: MAJOR.MINOR.PATCH-identifier

Examples:

1.0.0-alpha       - Early testing version
1.0.0-alpha.1     - First alpha iteration
1.0.0-beta        - Beta version
1.0.0-beta.2      - Second beta iteration
1.0.0-rc.1        - Release candidate 1
1.0.0             - Stable release

Precedence (lowest to highest):

1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-rc.1 < 1.0.0

When to use:

  • Testing a major release before making it official
  • Getting early feedback on new features
  • Internal testing of potentially unstable code

Build Metadata

Append a plus sign for build information (doesn’t affect version precedence):

Examples:

1.0.0+20241220
1.0.0+sha.5114f85
1.0.0-beta+exp.sha.5114f85

Build metadata is informational only. 1.0.0+001 and 1.0.0+002 are considered equivalent versions.


Practical Examples

Scenario 1: Adding a Feature

Current version: 1.2.3

You add a new function exportToCSV() that doesn’t change any existing functionality.

New version: 1.3.0 (MINOR bump)

Scenario 2: Fixing a Bug

Current version: 1.3.0

You fix a crash when users enter special characters.

New version: 1.3.1 (PATCH bump)

Scenario 3: Breaking Change

Current version: 1.3.1

You change the database schema in a way that requires users to run migrations and update their code.

New version: 2.0.0 (MAJOR bump)

Scenario 4: Multiple Changes

You’ve made several changes:

  • Added two new features
  • Fixed three bugs
  • Made one breaking change

New version: Increment MAJOR (because of the breaking change)

  • If current is 1.3.1 → new version is 2.0.0

All the features and fixes are included in the major release.

Scenario 5: Deprecation

Current version: 1.4.0

You want to remove oldFunction() but users still depend on it.

Step 1: Mark it deprecated in 1.5.0

/**
 * @deprecated Use newFunction() instead. Will be removed in v2.0.0
 */
function oldFunction() {
  console.warn('oldFunction is deprecated, use newFunction');
  return newFunction();
}

Step 2: Remove it in 2.0.0

This gives users time to migrate before the breaking change.


How to Apply Versions in Your Projects

In package.json (JavaScript/Node.js)

{
  "name": "my-project",
  "version": "1.3.0",
  ...
}

Update with: npm version major|minor|patch

In Cargo.toml (Rust)

[package]
name = "my-project"
version = "1.3.0"

In setup.py (Python)

setup(
    name="my-project",
    version="1.3.0",
    ...
)

In Git Tags

Tag releases so people can reference specific versions:

git tag -a v1.3.0 -m "Release version 1.3.0"
git push origin v1.3.0

Note: The v prefix is convention, not part of SemVer. The actual version is 1.3.0, but tagging as v1.3.0 is common practice.

In Documentation

Always document version changes in a CHANGELOG.md:

# Changelog

## [2.0.0] - 2024-12-20
### Breaking Changes
- Changed `calculatePrice()` to require `taxRate` parameter

### Added
- New `formatCurrency()` helper function

### Fixed
- Fixed crash when processing empty orders

## [1.3.0] - 2024-12-15
### Added
- Added `exportToCSV()` function
...

Use the Keep a Changelog format.


Common Questions and Mistakes

“Should I bump MAJOR for internal refactoring?”

No. If you don’t change the public API (the functions, methods, and behavior users depend on), it’s a PATCH or MINOR change depending on whether you added features.

Internal refactoring that doesn’t change behavior = PATCH Internal improvements that add capability = MINOR

“I accidentally released a breaking change as MINOR. What now?”

  1. Acknowledge the mistake
  2. Release a new MINOR version that restores backwards compatibility
  3. Document the broken version so users avoid it
  4. Consider releasing a new MAJOR version with the breaking change properly labeled

Example:

  • 1.3.0 - Accidentally broke things
  • 1.3.1 - Reverted breaking change, restoring compatibility
  • 2.0.0 - Properly implemented breaking change

“What if I need to fix a bug in an old MAJOR version?”

Branch from that version and release a PATCH:

# If current is 2.x.x but you need to fix 1.x.x
git checkout v1.5.0
git checkout -b v1-bugfix
# Fix the bug
git tag v1.5.1

“Can I skip versions?”

Technically yes, but don’t. Version numbers should increase sequentially. Skipping versions confuses users.

Bad: 1.2.01.5.0 (where did 1.3 and 1.4 go?) Good: 1.2.01.3.01.4.01.5.0

“What about marketing versions like ‘Windows 11’ or ‘macOS Ventura’?”

SemVer is for technical versioning. If you want marketing names, use both:

User-facing: “Project Phoenix” Technical: v2.3.1

Document both in your releases.


Dependency Management with SemVer

When specifying dependencies, use version ranges:

NPM (package.json)

{
  "dependencies": {
    "express": "^4.18.0",     // 4.18.0 ≤ version < 5.0.0
    "lodash": "~4.17.21",     // 4.17.21 ≤ version < 4.18.0
    "react": "18.2.0"         // Exact version
  }
}

Common patterns:

  • ^1.2.3 - Compatible with 1.2.3 (allows MINOR and PATCH updates)
  • ~1.2.3 - Approximately 1.2.3 (allows PATCH updates only)
  • 1.2.3 - Exact version
  • >=1.2.3 <2.0.0 - Explicit range

Cargo (Rust)

[dependencies]
serde = "1.0"          # ^1.0.0 (MINOR and PATCH)
tokio = "~1.35"        # ~1.35.0 (PATCH only)
reqwest = "=0.11.24"   # Exact version

Python (requirements.txt)

Django>=3.2,<4.0
requests~=2.28.0
pytest==7.4.0

Why this matters: If everyone uses SemVer correctly, you can specify dependencies like ^1.2.0 and trust that updates won’t break your code until a MAJOR version change.


Versioning Checklist

Before releasing a new version:

  • Determine the type of changes (MAJOR, MINOR, or PATCH)
  • Update version number in project files
  • Update CHANGELOG.md with changes
  • Tag the release in Git
  • Write clear release notes explaining changes
  • If MAJOR: Provide migration guide for breaking changes
  • If deprecating: Give users at least one MINOR release warning

Why This Matters for SEAD Club

Consistent versioning across our projects means:

  • Contributors know what changes are appropriate for a given release
  • Users can trust updates based on version numbers
  • We communicate professionally about our software
  • Dependencies between projects are manageable

When everyone follows SemVer, we all benefit from predictable, trustworthy version numbers.


Further Reading


Contributing to These Conventions

Have questions about versioning a specific change? Not sure if something counts as breaking? Ask in our community channels or open an issue for discussion.

These conventions work best when we all understand and apply them consistently. Your questions help us clarify and improve these guidelines for everyone.