+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 274 of 354

📘 Release Automation: Semantic Release

Master release automation: semantic release in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻

What you'll learn

  • Understand the concept fundamentals 🎯
  • Apply the concept in real projects 🏗️
  • Debug common issues 🐛
  • Write type-safe code ✨

📘 Release Automation: Semantic Release

🎯 Introduction

Have you ever spent hours manually creating releases, updating version numbers, and writing changelogs? 😩 Say goodbye to tedious release processes! Today, we’re diving into the magical world of semantic release automation with TypeScript.

Imagine having a personal assistant that automatically handles your releases, updates version numbers, generates changelogs, and publishes packages - all based on your commit messages! That’s exactly what semantic release does. Let’s make your release process as smooth as butter! 🧈✨

📚 Understanding Semantic Release

Think of semantic release as your project’s release manager robot 🤖. It follows the principles of semantic versioning (semver) to automatically determine version numbers based on your commit messages.

Here’s how semantic versioning works:

  • Major (X.0.0): Breaking changes 💥
  • Minor (0.X.0): New features 🎁
  • Patch (0.0.X): Bug fixes 🐛

Semantic release reads your commit messages and decides:

// 🎯 Commit message → Version bump
"fix: resolve login issue"1.2.31.2.4 (patch)
"feat: add dark mode"1.2.41.3.0 (minor)
"BREAKING CHANGE: new API"1.3.02.0.0 (major)

🔧 Basic Syntax and Usage

Let’s set up semantic release in your TypeScript project:

# 📦 Install semantic-release and friends
pnpm add -D semantic-release @semantic-release/git @semantic-release/changelog @semantic-release/npm

Create a basic configuration file:

// release.config.js
module.exports = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',    // 🔍 Analyzes commits
    '@semantic-release/release-notes-generator', // 📝 Creates notes
    '@semantic-release/changelog',          // 📋 Updates CHANGELOG.md
    '@semantic-release/npm',                // 📦 Publishes to npm
    '@semantic-release/git'                 // 🔄 Commits changes
  ]
};

TypeScript configuration for better type safety:

// release.config.ts
import type { Options } from 'semantic-release';

const config: Options = {
  branches: ['main'],
  plugins: [
    ['@semantic-release/commit-analyzer', {
      preset: 'angular',
      releaseRules: [
        { type: 'docs', release: 'patch' },     // 📚 Documentation → patch
        { type: 'refactor', release: 'patch' }, // 🔧 Refactoring → patch
        { type: 'style', release: 'patch' }     // 💅 Style changes → patch
      ]
    }],
    '@semantic-release/release-notes-generator',
    ['@semantic-release/changelog', {
      changelogFile: 'CHANGELOG.md'
    }],
    '@semantic-release/npm',
    ['@semantic-release/git', {
      assets: ['CHANGELOG.md', 'package.json'],
      message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}'
    }]
  ]
};

export default config;

💡 Practical Examples

Example 1: E-commerce Package Release 🛒

// 🏪 E-commerce TypeScript library with semantic release

// types/product.ts
export interface Product {
  id: string;
  name: string;
  price: number;
  inStock: boolean;
}

// src/ShoppingCart.ts
export class ShoppingCart {
  private items: Map<string, { product: Product; quantity: number }>;

  constructor() {
    this.items = new Map();
  }

  // 🛒 Add items to cart
  addItem(product: Product, quantity: number = 1): void {
    const existing = this.items.get(product.id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.set(product.id, { product, quantity });
    }
  }

  // 💰 Calculate total
  getTotal(): number {
    let total = 0;
    this.items.forEach(({ product, quantity }) => {
      total += product.price * quantity;
    });
    return total;
  }
}

// Commit examples that trigger releases:
// git commit -m "feat: add discount calculation to cart"
// → Version: 1.0.0 → 1.1.0 (minor release)

// git commit -m "fix: correct tax calculation rounding"
// → Version: 1.1.0 → 1.1.1 (patch release)

// git commit -m "feat!: change API to use async methods"
// → Version: 1.1.1 → 2.0.0 (major release)

Example 2: Game Engine Library 🎮

// 🎯 Game engine with automated releases

// Custom semantic-release config for game engine
const gameEngineConfig: Options = {
  branches: ['main', { name: 'beta', prerelease: true }],
  plugins: [
    ['@semantic-release/commit-analyzer', {
      preset: 'conventionalcommits',
      releaseRules: [
        { type: 'perf', release: 'patch' },    // 🚀 Performance
        { type: 'asset', release: 'patch' },   // 🎨 Asset updates
        { type: 'balance', release: 'minor' }  // ⚖️ Game balance
      ]
    }],
    ['@semantic-release/release-notes-generator', {
      preset: 'conventionalcommits',
      presetConfig: {
        types: [
          { type: 'feat', section: '✨ New Features' },
          { type: 'fix', section: '🐛 Bug Fixes' },
          { type: 'perf', section: '🚀 Performance' },
          { type: 'balance', section: '⚖️ Game Balance' }
        ]
      }
    }],
    '@semantic-release/changelog',
    '@semantic-release/npm',
    ['@semantic-release/github', {
      assets: [
        { path: 'dist/*.zip', label: 'Game Engine Bundle' }
      ]
    }]
  ]
};

// Game engine code
export class GameObject {
  position: { x: number; y: number };
  velocity: { x: number; y: number };
  
  constructor(x: number = 0, y: number = 0) {
    this.position = { x, y };
    this.velocity = { x: 0, y: 0 };
  }

  update(deltaTime: number): void {
    // 🎮 Update position based on velocity
    this.position.x += this.velocity.x * deltaTime;
    this.position.y += this.velocity.y * deltaTime;
  }
}

// Commit examples:
// "perf: optimize collision detection algorithm" → patch
// "feat: add particle system support" → minor
// "balance: adjust player movement speed" → minor

Example 3: API Client Library 🌐

// 🔗 Type-safe API client with semantic releases

// Advanced configuration with multiple release channels
const apiClientConfig: Options = {
  branches: [
    'main',
    { name: 'next', prerelease: true },
    { name: 'alpha', prerelease: true }
  ],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    ['@semantic-release/changelog', {
      changelogTitle: '# API Client Changelog 📋\n\nAll notable changes to this project will be documented in this file.'
    }],
    ['@semantic-release/npm', {
      pkgRoot: 'dist' // 📦 Publish from dist folder
    }],
    ['@semantic-release/exec', {
      prepareCmd: 'pnpm build && pnpm test'
    }],
    '@semantic-release/git',
    ['@semantic-release/github', {
      successComment: '🎉 This ${issue.pull_request ? "PR" : "issue"} is included in version ${nextRelease.version}!'
    }]
  ]
};

// API client implementation
export class TypedAPIClient<T extends Record<string, any>> {
  private baseURL: string;
  
  constructor(baseURL: string) {
    this.baseURL = baseURL;
  }

  async get<K extends keyof T>(endpoint: K): Promise<T[K]> {
    const response = await fetch(`${this.baseURL}/${String(endpoint)}`);
    if (!response.ok) {
      throw new Error(`API Error: ${response.statusText}`);
    }
    return response.json();
  }
}

// Usage with automatic releases
type APIEndpoints = {
  'users': User[];
  'products': Product[];
  'orders': Order[];
};

const client = new TypedAPIClient<APIEndpoints>('https://api.example.com');

🚀 Advanced Concepts

Custom Release Rules and Analyzers

// 🎨 Custom commit analyzer for special project needs
import { PluginSpec } from 'semantic-release';

const customAnalyzer: PluginSpec = [
  '@semantic-release/commit-analyzer',
  {
    preset: 'angular',
    parserOpts: {
      noteKeywords: ['BREAKING CHANGE', 'BREAKING CHANGES', 'BREAKING']
    },
    releaseRules: [
      // 🏗️ Infrastructure changes
      { type: 'build', release: 'patch' },
      { type: 'ci', release: 'patch' },
      
      // 🎨 UI/UX improvements
      { type: 'ui', release: 'minor' },
      { type: 'ux', release: 'minor' },
      
      // 🔒 Security updates (always at least minor)
      { type: 'security', release: 'minor' },
      
      // 📱 Platform-specific
      { type: 'ios', release: 'patch' },
      { type: 'android', release: 'patch' },
      
      // 🚨 Emergency fixes
      { type: 'hotfix', release: 'patch' },
      
      // 📊 Analytics and monitoring
      { type: 'analytics', release: false }, // No release
      
      // Special handling for breaking changes
      { breaking: true, release: 'major' },
      
      // Scope-based rules
      { type: 'feat', scope: 'api', release: 'minor' },
      { type: 'fix', scope: 'core', release: 'patch' }
    ]
  }
];

Multi-Package Monorepo Releases

// 🏗️ Semantic release for monorepo with multiple packages
import { Options } from 'semantic-release';

// Configuration for workspace packages
const monorepoConfig: Options = {
  extends: 'semantic-release-monorepo',
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    ['@semantic-release/changelog', {
      changelogFile: 'packages/${packageName}/CHANGELOG.md'
    }],
    ['@semantic-release/npm', {
      pkgRoot: 'packages/${packageName}'
    }],
    ['@semantic-release/git', {
      assets: [
        'packages/${packageName}/CHANGELOG.md',
        'packages/${packageName}/package.json'
      ],
      message: 'chore(release): ${packageName}@${nextRelease.version} [skip ci]'
    }]
  ]
};

// Package-specific configuration
export const createPackageConfig = (packageName: string): Options => ({
  ...monorepoConfig,
  tagFormat: `${packageName}-v\${version}`,
  plugins: monorepoConfig.plugins?.map(plugin => {
    if (typeof plugin === 'string') return plugin;
    if (Array.isArray(plugin)) {
      const [name, config] = plugin;
      return [name, {
        ...config,
        ...(config.changelogFile && {
          changelogFile: config.changelogFile.replace('${packageName}', packageName)
        }),
        ...(config.pkgRoot && {
          pkgRoot: config.pkgRoot.replace('${packageName}', packageName)
        })
      }];
    }
    return plugin;
  })
});

Conditional Releases with TypeScript

// 🎯 Smart release conditions based on project state
interface ReleaseCondition {
  check: () => Promise<boolean>;
  message: string;
}

class ConditionalReleaseManager {
  private conditions: ReleaseCondition[] = [];

  addCondition(condition: ReleaseCondition): void {
    this.conditions.push(condition);
  }

  async canRelease(): Promise<{ allowed: boolean; reasons: string[] }> {
    const failedReasons: string[] = [];
    
    for (const condition of this.conditions) {
      const passed = await condition.check();
      if (!passed) {
        failedReasons.push(condition.message);
      }
    }
    
    return {
      allowed: failedReasons.length === 0,
      reasons: failedReasons
    };
  }
}

// Usage example
const releaseManager = new ConditionalReleaseManager();

// 🧪 Check test coverage
releaseManager.addCondition({
  check: async () => {
    const coverage = await getCoveragePercentage();
    return coverage >= 80;
  },
  message: 'Test coverage must be at least 80%'
});

// 🔍 Check code quality
releaseManager.addCondition({
  check: async () => {
    const lintErrors = await runESLint();
    return lintErrors === 0;
  },
  message: 'No linting errors allowed'
});

// 📊 Check bundle size
releaseManager.addCondition({
  check: async () => {
    const bundleSize = await getBundleSize();
    return bundleSize < 100_000; // 100KB limit
  },
  message: 'Bundle size exceeds 100KB limit'
});

⚠️ Common Pitfalls and Solutions

❌ Wrong: Inconsistent commit messages

// 😱 These commits won't trigger proper releases
git commit -m "Fixed bug"
git commit -m "added feature"
git commit -m "UPDATED: api endpoint"

✅ Right: Following conventional commits

// 🎯 Proper commit format for semantic release
git commit -m "fix: resolve authentication timeout issue"
git commit -m "feat: add user profile customization"
git commit -m "fix(api): correct response status codes"
git commit -m "feat!: migrate to new authentication system"

❌ Wrong: Manual version management

// 😓 package.json - Don't manually update version!
{
  "name": "my-package",
  "version": "1.2.3", // ❌ Don't touch this!
  "scripts": {
    "release": "npm version patch && npm publish" // ❌ Old way
  }
}

✅ Right: Let semantic-release handle it

// 🎉 package.json - Version managed automatically
{
  "name": "my-package",
  "version": "0.0.0-development", // ✅ Placeholder version
  "scripts": {
    "release": "semantic-release" // ✅ Automated releases
  }
}

🛠️ Best Practices

1. Commit Message Guidelines 📝

// 🎯 Create a commit message helper
export class CommitHelper {
  static fix(scope: string, description: string): string {
    return `fix(${scope}): ${description}`;
  }
  
  static feat(scope: string, description: string): string {
    return `feat(${scope}): ${description}`;
  }
  
  static breaking(scope: string, description: string, details: string): string {
    return `feat(${scope})!: ${description}\n\nBREAKING CHANGE: ${details}`;
  }
}

// Usage
CommitHelper.fix('auth', 'resolve token expiration issue');
// → "fix(auth): resolve token expiration issue"

CommitHelper.breaking('api', 'change response format', 
  'Response now returns data wrapped in `result` property');
// → "feat(api)!: change response format
//    
//    BREAKING CHANGE: Response now returns data wrapped in `result` property"

2. Pre-release Testing 🧪

// 🔬 Test releases before going live
const testConfig: Options = {
  branches: [
    { name: 'main', prerelease: false },
    { name: 'beta', prerelease: true },
    { name: 'alpha', prerelease: true }
  ],
  dryRun: process.env.DRY_RUN === 'true', // 🏃 Dry run mode
  ci: true,
  debug: process.env.DEBUG === 'true'     // 🐛 Debug mode
};

3. Release Notes Customization 📋

// 🎨 Beautiful release notes
const releaseNotesConfig = {
  preset: 'conventionalcommits',
  writerOpts: {
    transform: (commit: any) => {
      // Add emojis to commit types
      const emoji: Record<string, string> = {
        feat: '✨',
        fix: '🐛',
        docs: '📚',
        style: '💄',
        refactor: '♻️',
        perf: '🚀',
        test: '🧪',
        build: '🏗️',
        ci: '👷',
        chore: '🧹'
      };
      
      if (commit.type && emoji[commit.type]) {
        commit.type = `${emoji[commit.type]} ${commit.type}`;
      }
      
      return commit;
    }
  }
};

🧪 Hands-On Exercise

Create a TypeScript package with automated semantic release! 🚀

Your Challenge: Build a utility library with automatic versioning

// 📝 TODO: Create a StringUtils class with semantic release
// Requirements:
// 1. Create utility methods for string manipulation
// 2. Set up semantic-release configuration
// 3. Use proper commit messages
// 4. Add GitHub Actions for automated releases

// Your code here:
export class StringUtils {
  // Implement these methods:
  // - capitalize(str: string): string
  // - camelCase(str: string): string
  // - truncate(str: string, length: number): string
}

// Semantic release config:
// Create release.config.ts with proper setup
📌 Click to see the solution
// 🎉 StringUtils implementation
export class StringUtils {
  static capitalize(str: string): string {
    if (!str) return str;
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
  }
  
  static camelCase(str: string): string {
    return str
      .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => 
        index === 0 ? word.toLowerCase() : word.toUpperCase()
      )
      .replace(/\s+/g, '');
  }
  
  static truncate(str: string, length: number, suffix: string = '...'): string {
    if (str.length <= length) return str;
    return str.slice(0, length - suffix.length) + suffix;
  }
}

// release.config.ts
import type { Options } from 'semantic-release';

const config: Options = {
  branches: ['main'],
  plugins: [
    '@semantic-release/commit-analyzer',
    '@semantic-release/release-notes-generator',
    ['@semantic-release/changelog', {
      changelogFile: 'CHANGELOG.md',
      changelogTitle: '# String Utils Changelog 📋'
    }],
    '@semantic-release/npm',
    ['@semantic-release/git', {
      assets: ['CHANGELOG.md', 'package.json'],
      message: 'chore(release): v${nextRelease.version} 🎉 [skip ci]'
    }],
    '@semantic-release/github'
  ]
};

export default config;

// .github/workflows/release.yml
name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test
      - run: npm run build
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Excellent work! You’ve automated your release process! 🎊

🎓 Key Takeaways

You’ve mastered semantic release automation! Here’s what you learned:

  1. Automatic Versioning 🔢 - Let commit messages drive version numbers
  2. Changelog Generation 📋 - Beautiful release notes created automatically
  3. CI/CD Integration 🔄 - Seamless releases with GitHub Actions
  4. Custom Rules 🎨 - Tailor release behavior to your project
  5. Type Safety 💪 - TypeScript configurations for reliability
  6. Multi-channel Releases 🚀 - Support for beta, alpha, and stable releases

🤝 Next Steps

Ready to level up your release game? Here’s what’s next:

  1. Explore Plugins 🔌 - Check out the semantic-release plugin ecosystem
  2. Custom Analyzers 🔍 - Build commit analyzers for your team’s workflow
  3. Monorepo Magic 🏗️ - Apply semantic release to multi-package projects
  4. Release Automation 🤖 - Combine with other CI/CD tools
  5. Version Strategies 📊 - Learn about different versioning approaches

Keep automating those releases! The next tutorial will show you how to monitor and analyze your TypeScript applications in production. Until then, happy releasing! 🚀✨