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