Automatically generate changelogs from git commits following conventional commits, semantic versi...
Generates changelogs from git commits following conventional commits format. Use it when you need to create or update CHANGELOG.md files based on commit history since a specific version.
/plugin marketplace add CuriousLearner/devkit/plugin install devkit@devkit-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Automatically generate changelogs from git commits following conventional commits, semantic versioning, and best practices.
You are a changelog generation expert. When invoked:
Analyze Commit History:
Generate Changelog Entries:
Format Output:
Version Management:
Breaking Change: Any commit with BREAKING CHANGE: in body or ! after type (major version bump)
@changelog-generator
@changelog-generator --since v1.2.0
@changelog-generator --unreleased
@changelog-generator --version 2.0.0
@changelog-generator --format keep-a-changelog
@changelog-generator --include-authors
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New feature X for improved user experience
- Support for configuration option Y
### Changed
- Updated dependency Z to version 2.0
- Improved performance of data processing
### Deprecated
- Function `oldMethod()` - use `newMethod()` instead
### Removed
- Removed deprecated API endpoint `/api/v1/old`
### Fixed
- Fixed memory leak in cache implementation
- Corrected timezone handling in date formatter
### Security
- Fixed XSS vulnerability in user input handling
- Updated crypto library to address CVE-2024-1234
## [1.5.0] - 2024-01-15
### Added
- User authentication with OAuth2
- Export functionality for reports
- Dark mode theme support
### Changed
- Redesigned dashboard UI
- Optimized database queries
### Fixed
- Fixed bug in pagination logic
- Resolved CORS issues with API
## [1.4.2] - 2024-01-10
### Fixed
- Critical bug in payment processing
- Memory leak in WebSocket connections
### Security
- Patched authentication bypass vulnerability
## [1.4.1] - 2024-01-05
### Fixed
- Hotfix for broken deployment script
- Fixed typo in error messages
## [1.4.0] - 2024-01-01
### Added
- Real-time notifications
- File upload with drag and drop
- Advanced search filters
### Changed
- Migrated from REST to GraphQL
- Updated UI components library
### Deprecated
- Old REST API endpoints (will be removed in 2.0)
[Unreleased]: https://github.com/user/repo/compare/v1.5.0...HEAD
[1.5.0]: https://github.com/user/repo/compare/v1.4.2...v1.5.0
[1.4.2]: https://github.com/user/repo/compare/v1.4.1...v1.4.2
[1.4.1]: https://github.com/user/repo/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/user/repo/releases/tag/v1.4.0
#!/bin/bash
# generate-changelog.sh - Generate changelog from git commits
VERSION=${1:-"Unreleased"}
PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
echo "# Changelog"
echo ""
echo "## [$VERSION] - $(date +%Y-%m-%d)"
echo ""
# Get commits since last tag
if [ -z "$PREV_TAG" ]; then
COMMITS=$(git log --pretty=format:"%s|||%h|||%an" --reverse)
else
COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"%s|||%h|||%an" --reverse)
fi
# Arrays for different categories
declare -a features=()
declare -a fixes=()
declare -a breaking=()
declare -a docs=()
declare -a chores=()
declare -a other=()
# Parse commits
while IFS='|||' read -r message hash author; do
case "$message" in
feat:*|feat\(*\):*)
features+=("- ${message#feat*: } ([${hash}](../../commit/${hash}))")
;;
fix:*|fix\(*\):*)
fixes+=("- ${message#fix*: } ([${hash}](../../commit/${hash}))")
;;
*BREAKING*|*\!:*)
breaking+=("- ${message} ([${hash}](../../commit/${hash}))")
;;
docs:*)
docs+=("- ${message#docs: } ([${hash}](../../commit/${hash}))")
;;
chore:*|build:*|ci:*)
chores+=("- ${message#*: } ([${hash}](../../commit/${hash}))")
;;
*)
other+=("- ${message} ([${hash}](../../commit/${hash}))")
;;
esac
done <<< "$COMMITS"
# Output sections
if [ ${#breaking[@]} -gt 0 ]; then
echo "### ⚠️ BREAKING CHANGES"
echo ""
printf '%s\n' "${breaking[@]}"
echo ""
fi
if [ ${#features[@]} -gt 0 ]; then
echo "### Added"
echo ""
printf '%s\n' "${features[@]}"
echo ""
fi
if [ ${#fixes[@]} -gt 0 ]; then
echo "### Fixed"
echo ""
printf '%s\n' "${fixes[@]}"
echo ""
fi
if [ ${#docs[@]} -gt 0 ]; then
echo "### Documentation"
echo ""
printf '%s\n' "${docs[@]}"
echo ""
fi
if [ ${#chores[@]} -gt 0 ]; then
echo "### Internal"
echo ""
printf '%s\n' "${chores[@]}"
echo ""
fi
if [ ${#other[@]} -gt 0 ]; then
echo "### Other Changes"
echo ""
printf '%s\n' "${other[@]}"
echo ""
fi
# Install
npm install -g conventional-changelog-cli
# Generate changelog
conventional-changelog -p angular -i CHANGELOG.md -s
# For first release
conventional-changelog -p angular -i CHANGELOG.md -s -r 0
# With specific version
conventional-changelog -p angular -i CHANGELOG.md -s --release-count 0 \
--tag-prefix v --preset angular
package.json configuration:
{
"scripts": {
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"version": "npm run changelog && git add CHANGELOG.md"
},
"devDependencies": {
"conventional-changelog-cli": "^4.1.0"
}
}
# Install
npm install -D standard-version
# Generate changelog and bump version
npx standard-version
# Preview without committing
npx standard-version --dry-run
# First release
npx standard-version --first-release
# Specific version
npx standard-version --release-as minor
npx standard-version --release-as 1.1.0
# Pre-release
npx standard-version --prerelease alpha
package.json:
{
"scripts": {
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:alpha": "standard-version --prerelease alpha"
},
"standard-version": {
"types": [
{"type": "feat", "section": "Features"},
{"type": "fix", "section": "Bug Fixes"},
{"type": "chore", "hidden": true},
{"type": "docs", "section": "Documentation"},
{"type": "style", "hidden": true},
{"type": "refactor", "section": "Code Refactoring"},
{"type": "perf", "section": "Performance Improvements"},
{"type": "test", "hidden": true}
]
}
}
.github/workflows/release.yml:
name: Release
on:
push:
branches:
- main
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
id: release
with:
release-type: node
package-name: my-package
- uses: actions/checkout@v3
if: ${{ steps.release.outputs.release_created }}
- uses: actions/setup-node@v3
if: ${{ steps.release.outputs.release_created }}
with:
node-version: 18
registry-url: 'https://registry.npmjs.org'
- run: npm ci
if: ${{ steps.release.outputs.release_created }}
- run: npm publish
if: ${{ steps.release.outputs.release_created }}
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
#!/usr/bin/env node
// generate-changelog.js
const { execSync } = require('child_process');
const fs = require('fs');
const COMMIT_PATTERN = /^(\w+)(\([\w-]+\))?(!)?:\s(.+)$/;
const TYPES = {
feat: { section: 'Added', bump: 'minor' },
fix: { section: 'Fixed', bump: 'patch' },
docs: { section: 'Documentation', bump: null },
style: { section: 'Style', bump: null },
refactor: { section: 'Changed', bump: null },
perf: { section: 'Performance', bump: 'patch' },
test: { section: 'Tests', bump: null },
build: { section: 'Build System', bump: null },
ci: { section: 'CI', bump: null },
chore: { section: 'Chores', bump: null },
revert: { section: 'Reverts', bump: 'patch' },
};
function getCommitsSinceTag(tag) {
const cmd = tag
? `git log ${tag}..HEAD --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ai"`
: `git log --pretty=format:"%H|||%s|||%b|||%an|||%ae|||%ai"`;
try {
const output = execSync(cmd, { encoding: 'utf-8' });
return output.split('\n').filter(Boolean);
} catch (error) {
return [];
}
}
function parseCommit(commitLine) {
const [hash, subject, body, author, email, date] = commitLine.split('|||');
const match = subject.match(COMMIT_PATTERN);
if (!match) {
return {
hash,
subject,
body,
author,
email,
date,
type: 'other',
scope: null,
breaking: false,
description: subject,
};
}
const [, type, scope, breaking, description] = match;
return {
hash,
subject,
body,
author,
email,
date,
type,
scope: scope ? scope.slice(1, -1) : null,
breaking: Boolean(breaking) || body.includes('BREAKING CHANGE'),
description,
};
}
function groupCommits(commits) {
const groups = {};
const breaking = [];
for (const commit of commits) {
if (commit.breaking) {
breaking.push(commit);
}
const typeInfo = TYPES[commit.type] || { section: 'Other Changes' };
const section = typeInfo.section;
if (!groups[section]) {
groups[section] = [];
}
groups[section].push(commit);
}
return { groups, breaking };
}
function generateMarkdown(version, date, groups, breaking, options = {}) {
const lines = [];
lines.push(`## [${version}] - ${date}`);
lines.push('');
// Breaking changes first
if (breaking.length > 0) {
lines.push('### ⚠️ BREAKING CHANGES');
lines.push('');
for (const commit of breaking) {
lines.push(`- **${commit.description}** ([${commit.hash.slice(0, 7)}](../../commit/${commit.hash}))`);
if (commit.body) {
const breakingNote = commit.body.match(/BREAKING CHANGE:\s*(.+)/);
if (breakingNote) {
lines.push(` ${breakingNote[1]}`);
}
}
}
lines.push('');
}
// Other sections
const sectionOrder = [
'Added',
'Changed',
'Deprecated',
'Removed',
'Fixed',
'Security',
'Performance',
'Documentation',
];
for (const section of sectionOrder) {
if (groups[section] && groups[section].length > 0) {
lines.push(`### ${section}`);
lines.push('');
const commits = groups[section];
const grouped = {};
// Group by scope if present
for (const commit of commits) {
const key = commit.scope || '_default';
if (!grouped[key]) grouped[key] = [];
grouped[key].push(commit);
}
for (const [scope, scopeCommits] of Object.entries(grouped)) {
if (scope !== '_default') {
lines.push(`#### ${scope}`);
lines.push('');
}
for (const commit of scopeCommits) {
let line = `- ${commit.description}`;
if (options.includeHash) {
line += ` ([${commit.hash.slice(0, 7)}](../../commit/${commit.hash}))`;
}
if (options.includeAuthor) {
line += ` - @${commit.author}`;
}
lines.push(line);
}
}
lines.push('');
}
}
return lines.join('\n');
}
function getLatestTag() {
try {
return execSync('git describe --tags --abbrev=0', { encoding: 'utf-8' }).trim();
} catch (error) {
return null;
}
}
function suggestVersion(breaking, groups) {
const latestTag = getLatestTag();
if (!latestTag) return '1.0.0';
const [major, minor, patch] = latestTag.replace('v', '').split('.').map(Number);
if (breaking.length > 0) {
return `${major + 1}.0.0`;
}
const hasFeatures = groups['Added'] && groups['Added'].length > 0;
if (hasFeatures) {
return `${major}.${minor + 1}.0`;
}
return `${major}.${minor}.${patch + 1}`;
}
// Main execution
function main() {
const args = process.argv.slice(2);
const options = {
includeHash: !args.includes('--no-hash'),
includeAuthor: args.includes('--author'),
version: args.find(a => a.startsWith('--version='))?.split('=')[1],
since: args.find(a => a.startsWith('--since='))?.split('=')[1],
};
const latestTag = options.since || getLatestTag();
const commits = getCommitsSinceTag(latestTag);
const parsed = commits.map(parseCommit);
const { groups, breaking } = groupCommits(parsed);
const version = options.version || suggestVersion(breaking, groups);
const date = new Date().toISOString().split('T')[0];
const changelog = generateMarkdown(version, date, groups, breaking, options);
// Read existing changelog or create new
let existingChangelog = '';
try {
existingChangelog = fs.readFileSync('CHANGELOG.md', 'utf-8');
} catch (error) {
existingChangelog = '# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n';
}
// Insert new version after header
const lines = existingChangelog.split('\n');
const headerEnd = lines.findIndex(l => l.startsWith('## '));
const newLines = [
...lines.slice(0, headerEnd === -1 ? lines.length : headerEnd),
changelog,
...lines.slice(headerEnd === -1 ? lines.length : headerEnd),
];
fs.writeFileSync('CHANGELOG.md', newLines.join('\n'));
console.log(`✓ Generated changelog for version ${version}`);
console.log(` - ${commits.length} commits processed`);
console.log(` - ${breaking.length} breaking changes`);
console.log(` - Suggested version: ${version}`);
}
if (require.main === module) {
main();
}
module.exports = { parseCommit, groupCommits, generateMarkdown };
Usage:
# Make executable
chmod +x generate-changelog.js
# Run
./generate-changelog.js
# With options
./generate-changelog.js --version=2.0.0 --author --since=v1.5.0
#!/usr/bin/env python3
# generate_changelog.py
import re
import subprocess
from datetime import datetime
from collections import defaultdict
from typing import List, Dict, Tuple
COMMIT_PATTERN = re.compile(r'^(\w+)(\([\w-]+\))?(!)?:\s(.+)$')
TYPES = {
'feat': {'section': 'Added', 'bump': 'minor'},
'fix': {'section': 'Fixed', 'bump': 'patch'},
'docs': {'section': 'Documentation', 'bump': None},
'style': {'section': 'Style', 'bump': None},
'refactor': {'section': 'Changed', 'bump': None},
'perf': {'section': 'Performance', 'bump': 'patch'},
'test': {'section': 'Tests', 'bump': None},
'build': {'section': 'Build System', 'bump': None},
'ci': {'section': 'CI', 'bump': None},
'chore': {'section': 'Chores', 'bump': None},
}
def get_commits_since_tag(tag: str = None) -> List[str]:
cmd = ['git', 'log', '--pretty=format:%H|||%s|||%b|||%an|||%ae|||%ai']
if tag:
cmd.insert(2, f'{tag}..HEAD')
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return [line for line in result.stdout.split('\n') if line]
except subprocess.CalledProcessError:
return []
def parse_commit(commit_line: str) -> Dict:
parts = commit_line.split('|||')
if len(parts) < 6:
return None
hash_val, subject, body, author, email, date = parts
match = COMMIT_PATTERN.match(subject)
if not match:
return {
'hash': hash_val,
'subject': subject,
'body': body,
'author': author,
'type': 'other',
'scope': None,
'breaking': False,
'description': subject,
}
type_val, scope, breaking, description = match.groups()
return {
'hash': hash_val,
'subject': subject,
'body': body,
'author': author,
'type': type_val,
'scope': scope[1:-1] if scope else None,
'breaking': bool(breaking) or 'BREAKING CHANGE' in body,
'description': description,
}
def group_commits(commits: List[Dict]) -> Tuple[Dict, List]:
groups = defaultdict(list)
breaking = []
for commit in commits:
if commit['breaking']:
breaking.append(commit)
type_info = TYPES.get(commit['type'], {'section': 'Other Changes'})
section = type_info['section']
groups[section].append(commit)
return dict(groups), breaking
def generate_markdown(version: str, groups: Dict, breaking: List) -> str:
lines = [f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}", ""]
if breaking:
lines.append("### ⚠️ BREAKING CHANGES")
lines.append("")
for commit in breaking:
lines.append(f"- **{commit['description']}** ([{commit['hash'][:7]}](../../commit/{commit['hash']}))")
lines.append("")
section_order = ['Added', 'Changed', 'Fixed', 'Security', 'Performance', 'Documentation']
for section in section_order:
if section in groups and groups[section]:
lines.append(f"### {section}")
lines.append("")
for commit in groups[section]:
lines.append(f"- {commit['description']} ([{commit['hash'][:7]}](../../commit/{commit['hash']}))")
lines.append("")
return '\n'.join(lines)
def main():
commits = get_commits_since_tag()
parsed = [parse_commit(c) for c in commits if parse_commit(c)]
groups, breaking = group_commits(parsed)
version = input("Enter version number (or press Enter for auto): ").strip() or "1.0.0"
changelog = generate_markdown(version, groups, breaking)
print(changelog)
# Optionally write to file
with open('CHANGELOG.md', 'r+') as f:
content = f.read()
f.seek(0, 0)
f.write(changelog + '\n\n' + content)
if __name__ == '__main__':
main()
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Generate changelog
id: changelog
run: |
# Get previous tag
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
# Generate changelog
if [ -z "$PREV_TAG" ]; then
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --reverse)
else
CHANGELOG=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --reverse)
fi
# Set output
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
## Changes in this release
${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.