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.3 → 1.2.4 (patch)
"feat: add dark mode" → 1.2.4 → 1.3.0 (minor)
"BREAKING CHANGE: new API" → 1.3.0 → 2.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:
- Automatic Versioning 🔢 - Let commit messages drive version numbers
- Changelog Generation 📋 - Beautiful release notes created automatically
- CI/CD Integration 🔄 - Seamless releases with GitHub Actions
- Custom Rules 🎨 - Tailor release behavior to your project
- Type Safety 💪 - TypeScript configurations for reliability
- Multi-channel Releases 🚀 - Support for beta, alpha, and stable releases
🤝 Next Steps
Ready to level up your release game? Here’s what’s next:
- Explore Plugins 🔌 - Check out the semantic-release plugin ecosystem
- Custom Analyzers 🔍 - Build commit analyzers for your team’s workflow
- Monorepo Magic 🏗️ - Apply semantic release to multi-package projects
- Release Automation 🤖 - Combine with other CI/CD tools
- 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! 🚀✨