Help us improve
Share bugs, ideas, or general feedback.
From changelog-generator
Generates changelogs and release notes from conventional commits, bumps semantic versions, and updates CHANGELOG.md. Supports generate, update, bump, and release-notes actions.
npx claudepluginhub devsforge/marketplace --plugin changelog-generatorHow this command is triggered — by the user, by Claude, or both
Slash command
/changelog-generator:changelog-generatorThis command is limited to the following tools:
The summary Claude sees in its command listing — used to decide when to auto-load this command
# DevsForge Changelog Generator DevsForge intelligent changelog and release notes generation system that automatically creates comprehensive, well-formatted changelogs from conventional commits, manages semantic versioning, and produces professional release documentation. ## Purpose Transform changelog management from manual documentation to intelligent automation that ensures consistency, completeness, and professional presentation of project changes while maintaining semantic versioning standards. ## Features - **Conventional Commits**: Parse and categorize commits following conventi...
/devkit.generate-changelogGenerates and updates project CHANGELOG.md from Git history in Keep a Changelog format with Conventional Commits and auto-version detection from build files. Supports init, update, release, preview, validate actions.
/changelogGenerates CHANGELOG.md from git history in Keep a Changelog format using Conventional Commits. Produces release notes, migration guides, breaking change docs, and tooling setup. Supports version-specific, audience-targeted, and full regen modes.
/changelogGenerates a Keep a Changelog from git commits since last release tag or specified starting point (tag, version, commit ref, or date), parsing Conventional Commits into sections like Added, Fixed, Changed.
/changelogParses git history since last release tag, categorizes commits by type, generates Keep a Changelog markdown with next version suggestion, and offers to prepend to CHANGELOG.md.
/generate-changelogGenerates changelog from git history using conventional commits, grouping by type and version. Updates or prepends to CHANGELOG.md in Keep a Changelog format. Supports --from and --to flags.
/changelogGenerates changelogs from git commit history with automatic categorization, semantic versioning detection, and multiple output formats.
Share bugs, ideas, or general feedback.
DevsForge intelligent changelog and release notes generation system that automatically creates comprehensive, well-formatted changelogs from conventional commits, manages semantic versioning, and produces professional release documentation.
Transform changelog management from manual documentation to intelligent automation that ensures consistency, completeness, and professional presentation of project changes while maintaining semantic versioning standards.
/changelog-generator [action] [options]
Target: $ARGUMENTS (if specified, otherwise analyze current scope)
Generate Changelog:
/changelog-generator generate --from=v1.0.0 --to=HEAD --output=CHANGELOG.md
Generates comprehensive changelog with:
Update Changelog:
/changelog-generator update --version=2.1.0 --prepend=true
Updates existing CHANGELOG.md with:
Release Notes:
/changelog-generator release-notes --version=2.1.0 --highlights=true
Creates release notes including:
Version Bump:
/changelog-generator bump --type=minor --dry-run=false
Determines version bump based on:
Next Version:
/changelog-generator next-version --commits=origin/main..HEAD
Calculates next version by analyzing:
Version Validation:
/changelog-generator validate-version --version=2.1.0-beta.3
Validates semantic version format:
Parse Commits:
/changelog-generator parse --range=v1.0.0..HEAD --format=json
Parses conventional commits with:
Validate Commits:
/changelog-generator validate-commits --strict=true --from=HEAD~10
Validates commit message format:
Commit Statistics:
/changelog-generator stats --from=v1.0.0 --group-by=author
Generates commit statistics including:
Configure Templates:
/changelog-generator config --template=./changelog-template.hbs
Customizes changelog with:
Section Configuration:
/changelog-generator configure-sections --preset=angular
Defines changelog sections:
Exclusion Rules:
/changelog-generator exclude --pattern="^chore(release|deps)" --scopes=internal
Excludes commits from changelog:
interface ConventionalCommit {
type: string;
scope?: string;
breaking: boolean;
subject: string;
body?: string;
footer?: string;
references: string[];
mentions: string[];
notes: Note[];
hash: string;
author: string;
date: Date;
}
interface Note {
title: string;
text: string;
}
class ChangelogGenerator {
private commitPattern = /^(\w+)(\(([^\)]+)\))?(!)?:\s(.+)$/;
parseCommit(commit: GitCommit): ConventionalCommit | null {
const lines = commit.message.split('\n');
const headerMatch = lines[0].match(this.commitPattern);
if (!headerMatch) {
return null;
}
const [, type, , scope, breaking, subject] = headerMatch;
const body = this.extractBody(lines);
const footer = this.extractFooter(lines);
const notes = this.extractNotes(footer);
const references = this.extractReferences(footer);
const mentions = this.extractMentions(commit.message);
return {
type,
scope,
breaking: breaking === '!' || notes.some(n => n.title === 'BREAKING CHANGE'),
subject,
body,
footer,
references,
mentions,
notes,
hash: commit.hash,
author: commit.author,
date: commit.date
};
}
private extractBody(lines: string[]): string | undefined {
const bodyStart = 1;
const bodyEnd = lines.findIndex((line, i) => i > 0 && /^[A-Z][a-z-]+:/.test(line));
if (bodyEnd === -1) {
return lines.slice(bodyStart).join('\n').trim() || undefined;
}
return lines.slice(bodyStart, bodyEnd).join('\n').trim() || undefined;
}
private extractFooter(lines: string[]): string | undefined {
const footerStart = lines.findIndex((line, i) => i > 0 && /^[A-Z][a-z-]+:/.test(line));
if (footerStart === -1) {
return undefined;
}
return lines.slice(footerStart).join('\n').trim();
}
private extractNotes(footer?: string): Note[] {
if (!footer) return [];
const notes: Note[] = [];
const notePattern = /^([A-Z][A-Z\s-]+):\s(.+)$/gm;
let match;
while ((match = notePattern.exec(footer)) !== null) {
notes.push({
title: match[1].trim(),
text: match[2].trim()
});
}
return notes;
}
private extractReferences(footer?: string): string[] {
if (!footer) return [];
const references: string[] = [];
const patterns = [
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s#(\d+)/gi,
/(?:ref|refs|references)\s#(\d+)/gi,
/#(\d+)/g
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(footer)) !== null) {
references.push(match[1]);
}
});
return [...new Set(references)];
}
private extractMentions(message: string): string[] {
const mentions = message.match(/@([a-zA-Z0-9_-]+)/g);
return mentions ? mentions.map(m => m.substring(1)) : [];
}
generateChangelog(commits: ConventionalCommit[], version: string): string {
const grouped = this.groupCommits(commits);
const date = new Date().toISOString().split('T')[0];
let changelog = `## [${version}](${this.compareUrl(version)}) (${date})\n\n`;
// Breaking changes first
if (grouped.breaking.length > 0) {
changelog += '### ⚠ BREAKING CHANGES\n\n';
grouped.breaking.forEach(commit => {
changelog += `* ${commit.subject}\n`;
const breakingNote = commit.notes.find(n => n.title === 'BREAKING CHANGE');
if (breakingNote) {
changelog += `\n ${breakingNote.text}\n\n`;
}
});
changelog += '\n';
}
// Features
if (grouped.feat.length > 0) {
changelog += '### Features\n\n';
grouped.feat.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}:** ` : '';
changelog += `* ${scope}${commit.subject} ([${commit.hash.substring(0, 7)}](${this.commitUrl(commit.hash)}))\n`;
});
changelog += '\n';
}
// Bug fixes
if (grouped.fix.length > 0) {
changelog += '### Bug Fixes\n\n';
grouped.fix.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}:** ` : '';
const refs = commit.references.length > 0
? `, closes ${commit.references.map(r => `#${r}`).join(', ')}`
: '';
changelog += `* ${scope}${commit.subject} ([${commit.hash.substring(0, 7)}](${this.commitUrl(commit.hash)}))${refs}\n`;
});
changelog += '\n';
}
// Performance improvements
if (grouped.perf.length > 0) {
changelog += '### Performance Improvements\n\n';
grouped.perf.forEach(commit => {
const scope = commit.scope ? `**${commit.scope}:** ` : '';
changelog += `* ${scope}${commit.subject} ([${commit.hash.substring(0, 7)}](${this.commitUrl(commit.hash)}))\n`;
});
changelog += '\n';
}
return changelog;
}
private groupCommits(commits: ConventionalCommit[]): Record<string, ConventionalCommit[]> {
const grouped: Record<string, ConventionalCommit[]> = {
breaking: [],
feat: [],
fix: [],
perf: [],
docs: [],
style: [],
refactor: [],
test: [],
chore: []
};
commits.forEach(commit => {
if (commit.breaking) {
grouped.breaking.push(commit);
}
if (grouped[commit.type]) {
grouped[commit.type].push(commit);
}
});
return grouped;
}
determineVersionBump(commits: ConventionalCommit[]): 'major' | 'minor' | 'patch' {
const hasBreaking = commits.some(c => c.breaking);
if (hasBreaking) return 'major';
const hasFeature = commits.some(c => c.type === 'feat');
if (hasFeature) return 'minor';
return 'patch';
}
bumpVersion(currentVersion: string, bump: 'major' | 'minor' | 'patch'): string {
const [major, minor, patch] = currentVersion.split('.').map(Number);
switch (bump) {
case 'major':
return `${major + 1}.0.0`;
case 'minor':
return `${major}.${minor + 1}.0`;
case 'patch':
return `${major}.${minor}.${patch + 1}`;
}
}
private commitUrl(hash: string): string {
return `https://github.com/owner/repo/commit/${hash}`;
}
private compareUrl(version: string): string {
return `https://github.com/owner/repo/compare/v${this.previousVersion}...v${version}`;
}
}
import re
from datetime import datetime
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class Commit:
hash: str
type: str
scope: Optional[str]
subject: str
body: Optional[str]
breaking: bool
references: List[str]
author: str
date: datetime
class ReleaseNotesGenerator:
COMMIT_PATTERN = re.compile(
r'^(?P<type>\w+)'
r'(?:\((?P<scope>[^\)]+)\))?'
r'(?P<breaking>!)?'
r':\s(?P<subject>.+)$'
)
def __init__(self, repo_owner: str, repo_name: str):
self.repo_owner = repo_owner
self.repo_name = repo_name
def generate_release_notes(
self,
commits: List[Commit],
version: str,
previous_version: str
) -> str:
"""Generate comprehensive GitHub release notes"""
notes = f"# Release {version}\n\n"
# Summary section
summary = self._generate_summary(commits)
notes += f"{summary}\n\n"
# Highlights
highlights = self._extract_highlights(commits)
if highlights:
notes += "## Highlights\n\n"
for highlight in highlights:
notes += f"- {highlight}\n"
notes += "\n"
# Breaking changes
breaking = [c for c in commits if c.breaking]
if breaking:
notes += "## ⚠️ Breaking Changes\n\n"
for commit in breaking:
notes += f"- **{commit.subject}**\n"
if commit.body:
notes += f" \n {commit.body}\n"
notes += "\n"
# Features
features = [c for c in commits if c.type == 'feat']
if features:
notes += "## New Features\n\n"
for commit in self._group_by_scope(features):
scope_prefix = f"**{commit.scope}**: " if commit.scope else ""
notes += f"- {scope_prefix}{commit.subject} "
notes += f"([{commit.hash[:7]}]({self._commit_url(commit.hash)}))\n"
notes += "\n"
# Bug fixes
fixes = [c for c in commits if c.type == 'fix']
if fixes:
notes += "## Bug Fixes\n\n"
for commit in self._group_by_scope(fixes):
scope_prefix = f"**{commit.scope}**: " if commit.scope else ""
refs = f" (fixes #{', #'.join(commit.references)})" if commit.references else ""
notes += f"- {scope_prefix}{commit.subject}{refs} "
notes += f"([{commit.hash[:7]}]({self._commit_url(commit.hash)}))\n"
notes += "\n"
# Performance improvements
perf = [c for c in commits if c.type == 'perf']
if perf:
notes += "## Performance Improvements\n\n"
for commit in perf:
scope_prefix = f"**{commit.scope}**: " if commit.scope else ""
notes += f"- {scope_prefix}{commit.subject} "
notes += f"([{commit.hash[:7]}]({self._commit_url(commit.hash)}))\n"
notes += "\n"
# Contributors
contributors = self._get_contributors(commits)
notes += "## Contributors\n\n"
notes += f"This release was made possible by {len(contributors)} contributor(s):\n\n"
for author in contributors:
notes += f"- @{author}\n"
notes += "\n"
# Installation instructions
notes += self._generate_installation_instructions(version)
# Comparison link
compare_url = self._compare_url(previous_version, version)
notes += f"\n**Full Changelog**: {compare_url}\n"
return notes
def _generate_summary(self, commits: List[Commit]) -> str:
"""Generate release summary"""
feat_count = len([c for c in commits if c.type == 'feat'])
fix_count = len([c for c in commits if c.type == 'fix'])
breaking_count = len([c for c in commits if c.breaking])
summary = f"This release includes "
parts = []
if feat_count > 0:
parts.append(f"{feat_count} new feature{'s' if feat_count > 1 else ''}")
if fix_count > 0:
parts.append(f"{fix_count} bug fix{'es' if fix_count > 1 else ''}")
if breaking_count > 0:
parts.append(f"{breaking_count} breaking change{'s' if breaking_count > 1 else ''}")
if not parts:
return "This release includes minor updates and improvements."
return summary + ", ".join(parts) + "."
def _extract_highlights(self, commits: List[Commit]) -> List[str]:
"""Extract highlights from commit messages"""
highlights = []
for commit in commits:
# Look for highlights in commit body
if commit.body and 'highlight:' in commit.body.lower():
match = re.search(r'highlight:\s*(.+)', commit.body, re.IGNORECASE)
if match:
highlights.append(match.group(1))
return highlights
def _group_by_scope(self, commits: List[Commit]) -> List[Commit]:
"""Sort commits by scope"""
return sorted(commits, key=lambda c: (c.scope or '', c.subject))
def _get_contributors(self, commits: List[Commit]) -> List[str]:
"""Get unique list of contributors"""
authors = set(c.author for c in commits)
return sorted(authors)
def _commit_url(self, hash: str) -> str:
"""Generate commit URL"""
return f"https://github.com/{self.repo_owner}/{self.repo_name}/commit/{hash}"
def _compare_url(self, from_version: str, to_version: str) -> str:
"""Generate comparison URL"""
return f"https://github.com/{self.repo_owner}/{self.repo_name}/compare/v{from_version}...v{to_version}"
def _generate_installation_instructions(self, version: str) -> str:
"""Generate installation instructions"""
return f"""## Installation
### npm
```bash
npm install {self.repo_name}@{version}
yarn add {self.repo_name}@{version}
pnpm add {self.repo_name}@{version}
"""
## Best Practices
### Commit Message Format
- **Conventional Commits**: Follow conventional commits specification
- **Clear Subjects**: Write clear, concise commit subjects (50 chars)
- **Detailed Bodies**: Provide context in commit body when needed
- **Breaking Changes**: Always document breaking changes in footer
- **Issue References**: Link commits to issues with Closes/Fixes
### Changelog Management
- **Keep Updated**: Update CHANGELOG.md with every release
- **Unreleased Section**: Maintain unreleased section for upcoming changes
- **Consistent Format**: Use consistent formatting throughout
- **Version Links**: Include comparison links between versions
- **Date Stamps**: Always include release dates
### Release Process
- **Semantic Versioning**: Strictly follow semantic versioning rules
- **Release Notes**: Create detailed release notes for each version
- **Tag Creation**: Create git tags for all releases
- **Automation**: Automate changelog generation in CI/CD
- **Review Process**: Review generated changelogs before publishing
### Documentation
- **Migration Guides**: Provide migration guides for breaking changes
- **Deprecation Notices**: Document deprecated features clearly
- **Examples**: Include usage examples for new features
- **Credits**: Acknowledge contributors and community
- **Links**: Provide links to documentation and resources
---
**DevsForge Changelog Generator** - Enterprise-grade changelog automation with conventional commits, semantic versioning, and professional release documentation for modern software projects.