devbook
Advanced Topics

Software Versioning

Semantic versioning, release management, and version control strategies

Software Versioning

Software versioning helps communicate changes, manage dependencies, and maintain compatibility.

Semantic Versioning (SemVer)

Format

MAJOR.MINOR.PATCH

Example: 2.4.1

Version Components

MAJOR (2.x.x)

Breaking changes that require user action.

// v1.0.0
function getUser(id: number): User

// v2.0.0 - MAJOR: Changed parameter type
function getUser(id: string): User

MINOR (x.4.x)

New features, backward compatible.

// v2.4.0 - MINOR: Added new optional parameter
function getUser(id: string, options?: { includeDeleted: boolean }): User

PATCH (x.x.1)

Bug fixes, backward compatible.

// v2.4.1 - PATCH: Fixed email validation bug
function validateEmail(email: string): boolean {
  // Fixed regex that was rejecting valid emails
}

Version Ranges

npm/pnpm Package Versioning

{
  "dependencies": {
    "exact": "1.2.3",              // Exactly 1.2.3
    "patch": "~1.2.3",             // >=1.2.3 <1.3.0
    "minor": "^1.2.3",             // >=1.2.3 <2.0.0
    "major": "*",                  // Any version
    "range": ">=1.2.3 <2.0.0",     // Custom range
    "latest": "latest"             // Latest version
  }
}

Caret (^) - Minor Updates

^1.2.3  →  >=1.2.3 <2.0.0
^0.2.3  →  >=0.2.3 <0.3.0
^0.0.3  →  >=0.0.3 <0.0.4

Tilde (~) - Patch Updates

~1.2.3  →  >=1.2.3 <1.3.0
~1.2    →  >=1.2.0 <1.3.0
~1      →  >=1.0.0 <2.0.0

Pre-release Versions

Alpha

Early development, unstable.

1.0.0-alpha.1
1.0.0-alpha.2

Beta

Feature complete, testing.

1.0.0-beta.1
1.0.0-beta.2

Release Candidate

Almost ready for release.

1.0.0-rc.1
1.0.0-rc.2

Example Progression

1.0.0-alpha.1  →  Development starts
1.0.0-alpha.2  →  More features
1.0.0-beta.1   →  Feature complete
1.0.0-beta.2   →  Bug fixes
1.0.0-rc.1     →  Final testing
1.0.0-rc.2     →  Last minute fixes
1.0.0          →  Stable release

Version Management

package.json

{
  "name": "my-package",
  "version": "1.2.3",
  "description": "My awesome package",
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

Updating Versions

Manual

// package.json
{
  "version": "1.2.3"  // → "1.2.4"
}

npm version

# Patch update (1.2.3 → 1.2.4)
npm version patch

# Minor update (1.2.3 → 1.3.0)
npm version minor

# Major update (1.2.3 → 2.0.0)
npm version major

# Pre-release
npm version prerelease --preid=alpha

Automatic with Commitizen

npm install -g commitizen

# Interactive commit
git cz

# Automatically determines version bump
npm run release

Changelog

Keep a Changelog Format

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
- New feature X

### Changed
- Updated feature Y

### Deprecated
- Feature Z will be removed in v2.0.0

### Removed
- Removed deprecated feature A

### Fixed
- Fixed bug in feature B

### Security
- Fixed security vulnerability in auth

## [1.2.0] - 2024-01-15

### Added
- OAuth2 authentication support
- User profile customization
- Dark mode theme

### Changed
- Improved performance of dashboard loading
- Updated UI components for better accessibility

### Fixed
- Resolved memory leak in real-time updates
- Fixed calculation error in checkout

## [1.1.0] - 2024-01-01

### Added
- API rate limiting
- Request logging

### Changed
- Optimized database queries

## [1.0.0] - 2023-12-15

Initial release

### Added
- User authentication
- Core API endpoints
- Admin dashboard

Generate Changelog Automatically

# Using conventional-changelog
npm install -g conventional-changelog-cli

conventional-changelog -p angular -i CHANGELOG.md -s

# Or with package.json script
{
  "scripts": {
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
  }
}

Git Tags

Creating Tags

# Lightweight tag
git tag v1.2.3

# Annotated tag (recommended)
git tag -a v1.2.3 -m "Release version 1.2.3"

# Tag specific commit
git tag -a v1.2.3 abc123 -m "Release version 1.2.3"

# Push tags
git push origin v1.2.3

# Push all tags
git push --tags

Listing Tags

# List all tags
git tag

# List tags matching pattern
git tag -l "v1.2.*"

# Show tag details
git show v1.2.3

Deleting Tags

# Delete local tag
git tag -d v1.2.3

# Delete remote tag
git push origin --delete v1.2.3

Release Process

Manual Release

# 1. Update version
npm version minor

# 2. Update changelog
npm run changelog

# 3. Commit changes
git add .
git commit -m "chore: release v1.3.0"

# 4. Create tag
git tag -a v1.3.0 -m "Release v1.3.0"

# 5. Push changes
git push origin main --tags

# 6. Build and publish
npm run build
npm publish

Automated Release (GitHub Actions)

name: Release

on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
      
      - name: Install dependencies
        run: pnpm install
      
      - name: Build
        run: pnpm build
      
      - name: Semantic Release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release

semantic-release Configuration

// .releaserc.js
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    '@semantic-release/changelog',
    '@semantic-release/npm',
    '@semantic-release/github',
    [
      '@semantic-release/git',
      {
        assets: ['package.json', 'CHANGELOG.md'],
        message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
      }
    ]
  ]
}

API Versioning

URL Versioning

// v1
app.get('/api/v1/users', getUsers)

// v2
app.get('/api/v2/users', getUsersV2)

Header Versioning

app.get('/api/users', (req, res) => {
  const version = req.headers['api-version']
  
  if (version === '2') {
    return getUsersV2(req, res)
  }
  
  return getUsers(req, res)
})

Accept Header

app.get('/api/users', (req, res) => {
  const accept = req.headers['accept']
  
  if (accept.includes('application/vnd.api.v2+json')) {
    return getUsersV2(req, res)
  }
  
  return getUsers(req, res)
})

Breaking Changes

Documenting Breaking Changes

## [2.0.0] - 2024-02-01

### BREAKING CHANGES

#### Changed Function Signatures
- `getUser(id: number)``getUser(id: string)`
- Update all calls to use string IDs instead of numbers

#### Removed Deprecated APIs
- Removed `getUserProfile()` (use `getUser()` instead)
- Removed `updateUserData()` (use `updateUser()` instead)

#### Changed Default Behavior
- `createUser()` now throws on duplicate email (previously returned existing user)
- Add error handling for duplicate emails

### Migration Guide

#### Step 1: Update User ID Types
```typescript
// Before
const user = await getUser(123)

// After
const user = await getUser('123')

Step 2: Replace Deprecated Functions

// Before
const profile = await getUserProfile(userId)

// After
const user = await getUser(userId)

Step 3: Handle Duplicate Email Errors

// Before
const user = await createUser({ email, password })

// After
try {
  const user = await createUser({ email, password })
} catch (error) {
  if (error.code === 'DUPLICATE_EMAIL') {
    // Handle duplicate
  }
}

### Deprecation Process
```typescript
/**
 * @deprecated Use getUser() instead. Will be removed in v2.0.0
 */
function getUserProfile(id: string): User {
  console.warn('getUserProfile() is deprecated. Use getUser() instead.')
  return getUser(id)
}

Version in Code

Exposing Version

// package.json
{
  "version": "1.2.3"
}

// version.ts
import packageJson from '../package.json'

export const VERSION = packageJson.version

// api.ts
app.get('/version', (req, res) => {
  res.json({ version: VERSION })
})

Version Check

function checkCompatibility(clientVersion: string): boolean {
  const [clientMajor] = clientVersion.split('.')
  const [serverMajor] = VERSION.split('.')
  
  return clientMajor === serverMajor
}

Monorepo Versioning

Independent Versioning

// packages/core/package.json
{
  "name": "@myapp/core",
  "version": "1.2.3"
}

// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "2.0.1"
}

Fixed Versioning

// All packages share same version
{
  "name": "@myapp/core",
  "version": "1.0.0"
}
{
  "name": "@myapp/ui",
  "version": "1.0.0"
}

Using Changesets

# Install changesets
npm install -D @changesets/cli

# Initialize
npx changeset init

# Create changeset
npx changeset

# Version packages
npx changeset version

# Publish
npx changeset publish

Best Practices

Do

✅ Follow semantic versioning ✅ Maintain detailed changelog ✅ Tag releases in git ✅ Document breaking changes ✅ Provide migration guides ✅ Deprecate before removing ✅ Test compatibility

Don't

❌ Change versions arbitrarily ❌ Skip versions ❌ Break compatibility in minor/patch ❌ Forget to update changelog ❌ Remove features without deprecation ❌ Use 0.x for production ❌ Publish without testing

Tools

  • semantic-release: Automated versioning
  • standard-version: Changelog generation
  • Changesets: Monorepo versioning
  • Release Please: Automated releases
  • Conventional Commits: Commit conventions

Version Strategy

For Libraries

  • Strict semantic versioning
  • Deprecation warnings
  • Migration guides
  • Long support windows

For Applications

  • Date-based versioning (2024.01.15)
  • Calendar versioning (CalVer)
  • Marketing versions (Windows 11)
  • More flexible approach