From Manual Maintenance to Automation: Building a Keep a Changelog Generator
From Manual Maintenance to Automation: Building a Keep a Changelog Generator#
Recently took over a legacy project. Checking the Git history, I found every release was just git log --oneline > CHANGELOG.md. The result? A CHANGELOG full of meaningless entries like “fix bug” and “update code”. Users had no idea what actually changed in each version.
So I decided to adopt the Keep a Changelog format and build an online tool for it.
The Core Philosophy of Keep a Changelog#
The format boils down to one principle: Changelogs are for humans, not machines.
Standard format:
# Changelog
All notable changes to this project will be documented in this file.
## [1.2.0] - 2024-01-15
### Added
- Add user export functionality
### Fixed
- Fix mobile layout issue on login page
### Security
- Upgrade lodash to fix prototype pollution vulnerability
Six change types:
- Added: New features
- Changed: Changes to existing features
- Deprecated: Features slated for removal
- Removed: Features that have been removed
- Fixed: Bug fixes
- Security: Security-related changes
Version Auto-Increment Algorithm#
The tool supports automatic version incrementing. Core logic:
function incrementVersion(current: string): string {
const parts = current.split('.').map(Number)
// Default: increment patch version
parts[2] = (parts[2] || 0) + 1
return parts.join('.')
}
// '1.2.3' => '1.2.4'
// '2.0.0' => '2.0.1'
In real projects, you should decide which part to increment based on change types:
function suggestVersionBump(changes: ChangeEntry[]): 'major' | 'minor' | 'patch' {
const hasBreaking = changes.some(c => c.description.includes('BREAKING'))
const hasNewFeature = changes.some(c => c.type === 'added')
if (hasBreaking) return 'major' // 1.0.0 -> 2.0.0
if (hasNewFeature) return 'minor' // 1.0.0 -> 1.1.0
return 'patch' // 1.0.0 -> 1.0.1
}
This follows Semantic Versioning (SemVer): MAJOR.MINOR.PATCH.
Markdown Generation Details#
Generating Markdown looks simple, but there are some pitfalls:
1. Change Grouping#
Changes of the same type should be grouped together:
function generateMarkdown(versions: VersionEntry[]): string {
let md = '# Changelog\n\n'
md += 'All notable changes to this project will be documented in this file.\n\n'
versions.forEach(v => {
md += `## [${v.version}] - ${v.date}\n\n`
// Group by type
const grouped: Record<string, string[]> = {}
v.changes.forEach(c => {
if (!c.description.trim()) return // Skip empty descriptions
if (!grouped[c.type]) grouped[c.type] = []
grouped[c.type].push(c.description)
})
// Output in fixed order
const typeOrder = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security']
typeOrder.forEach(type => {
if (grouped[type] && grouped[type].length > 0) {
md += `### ${type.charAt(0).toUpperCase() + type.slice(1)}\n\n`
grouped[type].forEach(item => {
md += `- ${item}\n`
})
md += '\n'
}
})
})
return md.trim()
}
2. Type Coloring#
In the editor interface, different types use different colors:
const typeColors: Record<string, string> = {
added: 'text-green-500', // Green - new features
changed: 'text-blue-500', // Blue - changes
deprecated: 'text-yellow-500', // Yellow - deprecations
removed: 'text-red-500', // Red - removals
fixed: 'text-purple-500', // Purple - fixes
security: 'text-orange-500', // Orange - security
}
Visual distinction reduces errors.
3. File Download#
Generated Markdown can be downloaded directly:
function handleDownload(content: string) {
const blob = new Blob([content], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'CHANGELOG.md'
a.click()
URL.revokeObjectURL(url) // Free memory
}
Note the URL.revokeObjectURL call - skip it and you’ll have a memory leak.
Integration with Git Workflow#
Ideally, CHANGELOGs should be auto-generated from Git commits. But there are issues:
1. Inconsistent Commit Message Quality#
fix bug ❌ Doesn't say what was fixed
update code ❌ Doesn't say what was updated
WIP ❌ Temporary commit
feat: add export ✅ Follows Conventional Commits
Solution: Add Commitlint + Husky to enforce commit message standards.
2. Auto-generation vs Manual Maintenance#
Auto-generation saves time but has drawbacks:
- Can’t categorize change types (Added/Fixed/Security)
- Can’t filter trivial commits
- Can’t add context
My recommendation: Auto-generate a draft, then manually review before publishing.
# Generate change draft
git log v1.0.0..HEAD --pretty=format:"- %s" > draft.md
# Manually edit, then merge into CHANGELOG.md
A Real-World Example#
My team recently used this tool to maintain our changelog. Here’s v1.5.0:
## [1.5.0] - 2024-01-20
### Added
- Support batch export of user data to Excel
- Add operation log audit feature
- Add rate limiting to API endpoints
### Changed
- Optimize database queries, list page load time improved by 50%
- Upgrade dependencies to latest versions
### Fixed
- Fix date formatting issue in IE11
- Fix filename encoding issue during file upload
### Security
- Fix XSS vulnerability (thanks to security researcher feedback)
Users immediately see this version is worth upgrading (new features, performance boost, security fixes).
Tool Implementation#
Based on these ideas, I built an online tool: Changelog Generator
Key features:
- Automatic version incrementing
- Six change type categories
- Real-time Markdown preview
- One-click CHANGELOG.md download
The implementation isn’t complex, but getting the details right takes some thought. Hope this helps!
Related Tools: Git Cheatsheet | Markdown Editor