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 โจ
๐ฏ Introduction
Welcome to this exciting tutorial on changelog generation with conventional commits! ๐ In this guide, weโll explore how to automatically create beautiful, meaningful changelogs from your commit history.
Youโll discover how conventional commits can transform your development workflow. Whether youโre building libraries ๐, applications ๐, or contributing to open source ๐ค, understanding conventional commits is essential for maintaining clear project history and automating your release process.
By the end of this tutorial, youโll be generating professional changelogs automatically! Letโs dive in! ๐โโ๏ธ
๐ Understanding Conventional Commits
๐ค What are Conventional Commits?
Conventional commits are like a standardized language for your git messages ๐ฃ๏ธ. Think of them as a consistent format that both humans and machines can understand - like having a universal translator for your project history!
In TypeScript terms, conventional commits follow a structured format that enables automated tooling. This means you can:
- โจ Generate changelogs automatically
- ๐ Determine semantic version bumps
- ๐ก๏ธ Trigger CI/CD workflows based on commit types
- ๐ Create better project documentation
๐ก Why Use Conventional Commits?
Hereโs why developers love conventional commits:
- Automated Changelogs ๐: No more manual changelog writing
- Semantic Versioning ๐ข: Automatic version bumps based on changes
- Better Collaboration ๐ฅ: Clear communication about changes
- Tool Integration ๐ง: Works with many automation tools
Real-world example: Imagine releasing a new version of your TypeScript library ๐ฆ. With conventional commits, your changelog is generated automatically, showing exactly what features, fixes, and breaking changes are included!
๐ง Basic Syntax and Usage
๐ The Conventional Commit Format
Letโs start with the basic structure:
// ๐ Basic format
type ConventionalCommit = {
type: string; // ๐ฏ The type of change
scope?: string; // ๐ฆ Optional scope
breaking?: boolean; // ๐ฅ Breaking change indicator
description: string; // ๐ Short description
body?: string; // ๐ Optional detailed explanation
footer?: string; // ๐ Optional footer (issues, etc.)
};
// ๐จ Example commit messages
const examples = [
"feat: add user authentication system",
"fix(auth): resolve login timeout issue",
"docs: update API documentation",
"feat!: redesign authentication flow",
"chore(deps): update typescript to v5.0"
];
๐ก Explanation: The format is type(scope): description
. The !
indicates a breaking change!
๐ฏ Common Commit Types
Here are the standard types youโll use:
// ๐๏ธ Commit type definitions
type CommitType =
| "feat" // โจ New feature
| "fix" // ๐ Bug fix
| "docs" // ๐ Documentation only
| "style" // ๐จ Code style (formatting, etc.)
| "refactor" // ๐ง Code change without feat/fix
| "perf" // ๐ Performance improvement
| "test" // ๐งช Adding/updating tests
| "build" // ๐๏ธ Build system changes
| "ci" // ๐ค CI configuration
| "chore" // ๐งน Other changes
| "revert"; // โช Reverting previous commit
// ๐ TypeScript interface for validation
interface ConventionalCommitConfig {
types: CommitType[];
scopes?: string[];
allowBreaking: boolean;
requireScope?: boolean;
}
๐ก Practical Examples
๐ Example 1: E-commerce Platform Changelog
Letโs build a changelog generator for an online store:
// ๐๏ธ Define our commit parser
interface ParsedCommit {
type: string;
scope: string | null;
subject: string;
breaking: boolean;
emoji: string;
}
// ๐จ Map commit types to emojis
const typeEmojis: Record<string, string> = {
feat: "โจ",
fix: "๐",
docs: "๐",
style: "๐",
refactor: "โป๏ธ",
perf: "โก",
test: "๐งช",
build: "๐๏ธ",
ci: "๐ค",
chore: "๐งน"
};
// ๐ Changelog generator class
class ChangelogGenerator {
private commits: ParsedCommit[] = [];
// ๐ฏ Parse a conventional commit
parseCommit(message: string): ParsedCommit {
const pattern = /^(\w+)(?:\(([^)]+)\))?(!?): (.+)$/;
const match = message.match(pattern);
if (!match) {
throw new Error("Invalid conventional commit format! ๐ฑ");
}
const [, type, scope, breaking, subject] = match;
return {
type,
scope: scope || null,
subject,
breaking: breaking === "!",
emoji: typeEmojis[type] || "๐"
};
}
// ๐ Generate changelog section
generateSection(version: string, date: Date): string {
const features = this.commits.filter(c => c.type === "feat");
const fixes = this.commits.filter(c => c.type === "fix");
const breaking = this.commits.filter(c => c.breaking);
let changelog = `## ๐ [${version}] - ${date.toISOString().split('T')[0]}\n\n`;
if (breaking.length > 0) {
changelog += "### ๐ฅ BREAKING CHANGES\n\n";
breaking.forEach(commit => {
changelog += `- ${commit.subject}\n`;
});
changelog += "\n";
}
if (features.length > 0) {
changelog += "### โจ Features\n\n";
features.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}**: ` : "";
changelog += `- ${scope}${commit.subject}\n`;
});
changelog += "\n";
}
if (fixes.length > 0) {
changelog += "### ๐ Bug Fixes\n\n";
fixes.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}**: ` : "";
changelog += `- ${scope}${commit.subject}\n`;
});
}
return changelog;
}
}
// ๐ฎ Let's use it!
const generator = new ChangelogGenerator();
console.log("๐ Generating changelog for your e-commerce platform!");
๐ฏ Try it yourself: Add support for grouping commits by scope and generating anchor links!
๐ฎ Example 2: Game Development Changelog
Letโs create a more advanced changelog for a game project:
// ๐ Advanced changelog generator for games
interface GameCommit extends ParsedCommit {
category: "gameplay" | "graphics" | "audio" | "ui" | "performance";
priority: "high" | "medium" | "low";
}
class GameChangelogGenerator {
private commits: GameCommit[] = [];
private versionHistory: Map<string, GameCommit[]> = new Map();
// ๐ฎ Categorize commits automatically
categorizeCommit(commit: ParsedCommit): GameCommit {
const scopeCategories: Record<string, GameCommit["category"]> = {
gameplay: "gameplay",
player: "gameplay",
enemy: "gameplay",
level: "gameplay",
graphics: "graphics",
render: "graphics",
shader: "graphics",
audio: "audio",
sound: "audio",
music: "audio",
ui: "ui",
menu: "ui",
hud: "ui",
perf: "performance",
optimization: "performance"
};
const category = commit.scope
? scopeCategories[commit.scope] || "gameplay"
: "gameplay";
const priority = commit.breaking ? "high" :
commit.type === "feat" ? "medium" : "low";
return { ...commit, category, priority };
}
// ๐จ Generate pretty changelog with categories
generateGameChangelog(version: string): string {
const commits = this.versionHistory.get(version) || [];
const byCategory = new Map<string, GameCommit[]>();
// ๐ Group by category
commits.forEach(commit => {
const list = byCategory.get(commit.category) || [];
list.push(commit);
byCategory.set(commit.category, list);
});
let changelog = `# ๐ฎ Game Update v${version}\n\n`;
changelog += `*Released on ${new Date().toLocaleDateString()}*\n\n`;
// ๐ Category emojis
const categoryEmojis: Record<string, string> = {
gameplay: "๐ฏ",
graphics: "๐จ",
audio: "๐ต",
ui: "๐ผ๏ธ",
performance: "โก"
};
// ๐ Generate sections by category
byCategory.forEach((commits, category) => {
const emoji = categoryEmojis[category];
changelog += `## ${emoji} ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
const highPriority = commits.filter(c => c.priority === "high");
const others = commits.filter(c => c.priority !== "high");
if (highPriority.length > 0) {
changelog += "### ๐ฅ Major Changes\n";
highPriority.forEach(commit => {
changelog += `- ${commit.emoji} ${commit.subject}\n`;
});
changelog += "\n";
}
if (others.length > 0) {
changelog += "### ๐ Other Updates\n";
others.forEach(commit => {
changelog += `- ${commit.emoji} ${commit.subject}\n`;
});
changelog += "\n";
}
});
changelog += `\n---\n๐ Thank you for playing! Share your feedback in our Discord! ๐ฎ\n`;
return changelog;
}
// ๐ Version bump calculator
calculateVersionBump(currentVersion: string): string {
const hasBreaking = this.commits.some(c => c.breaking);
const hasFeatures = this.commits.some(c => c.type === "feat");
const [major, minor, patch] = currentVersion.split(".").map(Number);
if (hasBreaking) {
return `${major + 1}.0.0`; // ๐ฅ Major version
} else if (hasFeatures) {
return `${major}.${minor + 1}.0`; // โจ Minor version
} else {
return `${major}.${minor}.${patch + 1}`; // ๐ Patch version
}
}
}
๐ Advanced Concepts
๐งโโ๏ธ Automated Changelog CI/CD Integration
When youโre ready to automate everything:
// ๐ฏ Advanced changelog automation
interface ChangelogConfig {
preset: "conventional" | "angular" | "custom";
releaseRules: ReleaseRule[];
writerOptions: WriterOptions;
}
interface ReleaseRule {
type: string;
release: "major" | "minor" | "patch" | false;
}
interface WriterOptions {
headerPattern: RegExp;
headerCorrespondence: string[];
noteKeywords: string[];
revertPattern: RegExp;
}
// ๐ช TypeScript-powered changelog automation
class ChangelogAutomation {
private config: ChangelogConfig;
constructor(config: ChangelogConfig) {
this.config = config;
}
// ๐ Generate changelog with validation
async generateChangelog(
fromTag: string,
toTag: string
): Promise<string> {
const commits = await this.getCommitRange(fromTag, toTag);
const validated = commits.filter(this.validateCommit);
const grouped = this.groupCommits(validated);
return this.formatChangelog(grouped);
}
// ๐ก๏ธ Type-safe commit validation
private validateCommit(commit: string): boolean {
const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?:/;
return pattern.test(commit);
}
// ๐ Smart commit grouping
private groupCommits(commits: string[]): Map<string, string[]> {
const groups = new Map<string, string[]>();
commits.forEach(commit => {
const type = commit.split(/[(:]/)[0];
const list = groups.get(type) || [];
list.push(commit);
groups.set(type, list);
});
return groups;
}
// โจ Format with templates
private formatChangelog(groups: Map<string, string[]>): string {
const sections = {
feat: "โจ Features",
fix: "๐ Bug Fixes",
perf: "โก Performance",
refactor: "โป๏ธ Code Refactoring",
docs: "๐ Documentation",
test: "๐งช Tests",
build: "๐๏ธ Build System",
ci: "๐ค CI/CD"
};
let output = "";
Object.entries(sections).forEach(([type, title]) => {
const commits = groups.get(type);
if (commits && commits.length > 0) {
output += `### ${title}\n\n`;
commits.forEach(commit => {
output += `- ${this.formatCommitLine(commit)}\n`;
});
output += "\n";
}
});
return output;
}
// ๐จ Pretty commit formatting
private formatCommitLine(commit: string): string {
const match = commit.match(/^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
if (!match) return commit;
const [, , scope, , subject] = match;
const scopeText = scope ? `**${scope}:** ` : "";
return `${scopeText}${subject}`;
}
// ๐ Mock function for getting commits
private async getCommitRange(from: string, to: string): Promise<string[]> {
console.log(`๐ Fetching commits from ${from} to ${to}`);
return []; // Would use git commands in real implementation
}
}
๐๏ธ Custom Changelog Templates
For complete control over your changelog format:
// ๐ Advanced template system
type TemplateFunction = (data: TemplateData) => string;
interface TemplateData {
version: string;
date: Date;
commits: ParsedCommit[];
previousVersion?: string;
author?: string;
}
class ChangelogTemplateEngine {
private templates = new Map<string, TemplateFunction>();
// ๐จ Register custom template
registerTemplate(name: string, template: TemplateFunction): void {
this.templates.set(name, template);
console.log(`โ
Registered template: ${name}`);
}
// ๐ Built-in templates
constructor() {
// ๐ฏ Default template
this.registerTemplate("default", (data) => {
return `# Version ${data.version}
Released on ${data.date.toLocaleDateString()}
${this.generateCommitSections(data.commits)}
`;
});
// ๐ Fancy template
this.registerTemplate("fancy", (data) => {
const emoji = data.version.startsWith("1.0") ? "๐" : "๐";
return `${emoji} **${data.version}** - *${data.date.toLocaleDateString()}*
---
${this.generateFancySections(data.commits)}
Made with ๐ by the team
`;
});
}
// ๐ง Helper methods
private generateCommitSections(commits: ParsedCommit[]): string {
// Group and format commits
return ""; // Implementation here
}
private generateFancySections(commits: ParsedCommit[]): string {
// Fancy formatting with emojis
return ""; // Implementation here
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Inconsistent Commit Messages
// โ Wrong - inconsistent formats
const badCommits = [
"Fixed bug in auth", // ๐ฅ No type prefix!
"feat add new feature", // ๐ฅ Missing colon!
"FEAT: Add Feature", // ๐ฅ Wrong case!
"feature(auth): add login" // ๐ฅ Wrong type!
];
// โ
Correct - consistent conventional commits
const goodCommits = [
"fix(auth): resolve login bug",
"feat: add new dashboard feature",
"feat(ui): implement dark mode",
"fix(api): handle null responses"
];
// ๐ก๏ธ Validation helper
const validateCommitMessage = (message: string): boolean => {
const pattern = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\(.+\))?(!)?:\s.+/;
if (!pattern.test(message)) {
console.log("โ ๏ธ Invalid commit format!");
console.log("โ
Use: type(scope): description");
return false;
}
return true;
};
๐คฏ Pitfall 2: Missing Breaking Changes
// โ Dangerous - breaking change not marked!
const hiddenBreaking = "feat(api): change response format";
// โ
Safe - clearly marked breaking change
const clearBreaking = "feat(api)!: change response format";
// ๐ฏ Better - with footer explanation
const detailedBreaking = `feat(api)!: change response format
BREAKING CHANGE: Response now returns data in 'results' field instead of 'items'.
Migration guide:
- Before: response.items
- After: response.results
`;
// ๐ก๏ธ Breaking change detector
class BreakingChangeDetector {
private breakingKeywords = [
"BREAKING CHANGE:",
"BREAKING:",
"BC:",
"!"
];
detectBreaking(commit: string): boolean {
return this.breakingKeywords.some(keyword =>
commit.includes(keyword)
);
}
suggestVersion(current: string, hasBreaking: boolean): string {
const [major, minor, patch] = current.split(".").map(Number);
if (hasBreaking) {
console.log("๐ฅ Breaking change detected! Major version bump required.");
return `${major + 1}.0.0`;
}
return `${major}.${minor}.${patch + 1}`;
}
}
๐ ๏ธ Best Practices
- ๐ฏ Be Consistent: Always follow the same format
- ๐ Be Descriptive: Write clear, meaningful descriptions
- ๐ก๏ธ Use Scopes: Group related changes with scopes
- โจ Automate Validation: Use commit hooks to enforce format
- ๐ Review Regularly: Check your changelog makes sense
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Full Changelog System
Create a complete changelog generation system:
๐ Requirements:
- โ Parse conventional commits from git history
- ๐ท๏ธ Support custom commit types and scopes
- ๐ค Include author information
- ๐ Group commits by date/version
- ๐จ Generate multiple output formats (Markdown, JSON, HTML)
๐ Bonus Points:
- Add commit message validation hooks
- Implement automatic version bumping
- Create a CLI tool for changelog generation
๐ก Solution
๐ Click to see solution
// ๐ฏ Complete changelog generation system!
interface CommitAuthor {
name: string;
email: string;
}
interface FullCommit extends ParsedCommit {
hash: string;
date: Date;
author: CommitAuthor;
body?: string;
}
class CompleteChangelogSystem {
private commits: FullCommit[] = [];
private customTypes: Set<string> = new Set([
"feat", "fix", "docs", "style", "refactor",
"perf", "test", "build", "ci", "chore"
]);
// ๐จ Add custom commit type
addCustomType(type: string, emoji: string): void {
this.customTypes.add(type);
console.log(`โ
Added custom type: ${type} ${emoji}`);
}
// ๐ Parse full commit with metadata
parseFullCommit(
hash: string,
message: string,
author: CommitAuthor,
date: Date
): FullCommit | null {
const headerPattern = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
const lines = message.split("\n");
const header = lines[0];
const match = header.match(headerPattern);
if (!match) return null;
const [, type, scope, breaking, subject] = match;
if (!this.customTypes.has(type)) {
console.warn(`โ ๏ธ Unknown commit type: ${type}`);
return null;
}
return {
hash: hash.substring(0, 7),
type,
scope: scope || null,
subject,
breaking: breaking === "!",
emoji: this.getEmoji(type),
date,
author,
body: lines.slice(2).join("\n").trim() || undefined
};
}
// ๐ฏ Generate changelog in multiple formats
generateChangelog(format: "markdown" | "json" | "html" = "markdown"): string {
switch (format) {
case "json":
return this.generateJSON();
case "html":
return this.generateHTML();
default:
return this.generateMarkdown();
}
}
// ๐ Markdown generation
private generateMarkdown(): string {
const grouped = this.groupByVersion();
let output = "# ๐ Changelog\n\n";
output += "All notable changes to this project will be documented here.\n\n";
grouped.forEach((commits, version) => {
output += `## [${version}] - ${new Date().toISOString().split('T')[0]}\n\n`;
const byType = this.groupByType(commits);
byType.forEach((typeCommits, type) => {
output += `### ${this.getEmoji(type)} ${this.getTypeTitle(type)}\n\n`;
typeCommits.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}:** ` : "";
const author = ` (${commit.author.name})`;
output += `- ${scope}${commit.subject}${author}\n`;
if (commit.body) {
output += ` ${commit.body.split("\n").join("\n ")}\n`;
}
});
output += "\n";
});
});
return output;
}
// ๐ HTML generation
private generateHTML(): string {
const grouped = this.groupByVersion();
let html = `<!DOCTYPE html>
<html>
<head>
<title>Changelog</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; }
h1 { color: #333; }
h2 { color: #666; border-bottom: 1px solid #eee; }
h3 { color: #888; }
.commit { margin: 10px 0; }
.scope { font-weight: bold; color: #0066cc; }
.author { color: #999; font-size: 0.9em; }
</style>
</head>
<body>
<h1>๐ Changelog</h1>
`;
grouped.forEach((commits, version) => {
html += `<h2>${version}</h2>\n`;
const byType = this.groupByType(commits);
byType.forEach((typeCommits, type) => {
html += `<h3>${this.getEmoji(type)} ${this.getTypeTitle(type)}</h3>\n`;
html += "<ul>\n";
typeCommits.forEach(commit => {
const scope = commit.scope ? `<span class="scope">${commit.scope}:</span> ` : "";
html += `<li class="commit">
${scope}${commit.subject}
<span class="author">(${commit.author.name})</span>
</li>\n`;
});
html += "</ul>\n";
});
});
html += "</body></html>";
return html;
}
// ๐ JSON generation
private generateJSON(): string {
const grouped = this.groupByVersion();
const output: any = { versions: {} };
grouped.forEach((commits, version) => {
output.versions[version] = {
date: new Date().toISOString(),
changes: {}
};
const byType = this.groupByType(commits);
byType.forEach((typeCommits, type) => {
output.versions[version].changes[type] = typeCommits.map(commit => ({
scope: commit.scope,
subject: commit.subject,
author: commit.author.name,
hash: commit.hash,
breaking: commit.breaking
}));
});
});
return JSON.stringify(output, null, 2);
}
// ๐ง Helper methods
private groupByVersion(): Map<string, FullCommit[]> {
// Simplified - in reality would use git tags
const map = new Map<string, FullCommit[]>();
map.set("1.0.0", this.commits);
return map;
}
private groupByType(commits: FullCommit[]): Map<string, FullCommit[]> {
const map = new Map<string, FullCommit[]>();
commits.forEach(commit => {
const list = map.get(commit.type) || [];
list.push(commit);
map.set(commit.type, list);
});
return map;
}
private getEmoji(type: string): string {
const emojis: Record<string, string> = {
feat: "โจ",
fix: "๐",
docs: "๐",
style: "๐",
refactor: "โป๏ธ",
perf: "โก",
test: "๐งช",
build: "๐๏ธ",
ci: "๐ค",
chore: "๐งน"
};
return emojis[type] || "๐";
}
private getTypeTitle(type: string): string {
const titles: Record<string, string> = {
feat: "Features",
fix: "Bug Fixes",
docs: "Documentation",
style: "Styles",
refactor: "Code Refactoring",
perf: "Performance Improvements",
test: "Tests",
build: "Build System",
ci: "Continuous Integration",
chore: "Chores"
};
return titles[type] || type;
}
}
// ๐ฎ Test it out!
const changelog = new CompleteChangelogSystem();
// Add some test commits
changelog.parseFullCommit(
"abc123",
"feat(auth): add OAuth2 support",
{ name: "Sarah Dev", email: "[email protected]" },
new Date()
);
console.log("๐ Generating changelog...");
console.log(changelog.generateChangelog("markdown"));
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Write conventional commits with confidence ๐ช
- โ Generate changelogs automatically from commit history ๐ก๏ธ
- โ Implement version bumping based on changes ๐ฏ
- โ Create custom changelog formats for your needs ๐
- โ Integrate with CI/CD for automated releases! ๐
Remember: Consistent commit messages make collaboration smoother and automation possible! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered changelog generation with conventional commits!
Hereโs what to do next:
- ๐ป Set up conventional commits in your project
- ๐๏ธ Install tools like
commitizen
orstandard-version
- ๐ Create your first automated changelog
- ๐ Share your beautiful changelogs with your team!
Remember: Good commit messages today mean easy changelogs tomorrow. Keep committing, keep documenting, and most importantly, have fun! ๐
Happy coding! ๐๐โจ