I automated changelog generation with Claude and git log — never again manually
← Back
April 4, 2026Claude5 min read

I automated changelog generation with Claude and git log — never again manually

Published April 4, 20265 min read

Writing changelogs before a release was always the task that slipped. I would commit to writing it properly, then end up with "Various bug fixes and improvements" because I ran out of time. Now a script reads the git log since the last tag and Claude turns commit messages into a proper, grouped changelog. The whole thing runs in 30 seconds.

The script

python
#!/usr/bin/env python3
# generate-changelog.py

import subprocess
import sys
import anthropic
from datetime import date

client = anthropic.Anthropic()


def get_commits_since(since_tag: str) -> str:
    """Get git log since a tag."""
    result = subprocess.run(
        [
            "git", "log",
            f"{since_tag}..HEAD",
            "--pretty=format:%h %s (%an)",
            "--no-merges",
        ],
        capture_output=True, text=True
    )
    return result.stdout.strip()


def get_latest_tag() -> str:
    result = subprocess.run(
        ["git", "describe", "--tags", "--abbrev=0"],
        capture_output=True, text=True
    )
    return result.stdout.strip()


def generate_changelog(commits: str, version: str) -> str:
    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1500,
        system="""You are writing a user-facing changelog. Transform commit messages into
clear, grouped release notes.

Group changes into:
- 🚀 New Features
- 🐛 Bug Fixes  
- ⚡ Performance
- 🔒 Security
- 🔧 Improvements
- 📚 Documentation

Rules:
- Write for end users, not developers. "Fixed crash when uploading large files" not "fixed null pointer in upload handler"
- Skip internal/tooling commits (chore, ci, deps updates) unless significant
- Combine related commits into one entry
- Omit the author names
- Use active present tense: "Adds", "Fixes", "Improves"
- Skip empty categories""",
        messages=[{
            "role": "user",
            "content": f"""Generate a changelog for version {version} from these commits:

{commits}

Today's date: {date.today().strftime('%B %d, %Y')}"""
        }]
    )
    return response.content[0].text


if __name__ == "__main__":
    version = sys.argv[1] if len(sys.argv) > 1 else "next"
    since_tag = sys.argv[2] if len(sys.argv) > 2 else get_latest_tag()
    
    print(f"Generating changelog since {since_tag}...")
    commits = get_commits_since(since_tag)
    
    if not commits:
        print("No commits since last tag")
        sys.exit(0)
    
    changelog = generate_changelog(commits, version)
    print(changelog)
    
    # Optionally prepend to CHANGELOG.md
    if "--write" in sys.argv:
        existing = ""
        try:
            with open("CHANGELOG.md") as f:
                existing = f.read()
        except FileNotFoundError:
            pass
        
        with open("CHANGELOG.md", "w") as f:
            f.write(changelog + "

" + existing)
        print("
Written to CHANGELOG.md")

Example output

Input: 23 raw commit messages like "fix: null check in user loader", "feat: add CSV export to reports", "perf: cache user lookups in auth middleware".

Output:

markdown
## Version 2.4.0 — April 4, 2026

### 🚀 New Features
- Adds CSV export to Reports — download any report as a spreadsheet from the Export button
- Adds dark mode support across all dashboard views

### ⚡ Performance  
- Improves page load time by 40% for authenticated users through smarter caching

### 🐛 Bug Fixes
- Fixes occasional blank screen when loading user profiles with incomplete data
- Fixes date filter not respecting timezone in analytics reports
- Fixes CSV upload failing silently on files larger than 10MB (now shows clear error)

Making it part of your release process

bash
# In package.json
{
  "scripts": {
    "release:changelog": "python3 scripts/generate-changelog.py",
    "release:changelog:write": "python3 scripts/generate-changelog.py --write",
    "release": "npm run release:changelog:write && npm version patch && git push --follow-tags"
  }
}

The conventional commits bonus

If your team uses conventional commits (feat:, fix:, perf: prefixes), Claude's grouping improves significantly. But it also works well with messy freeform commit messages — Claude infers intent from the description even without prefixes.

The best part: once this is set up, the changelog actually gets written. Automated is always better than "we'll do it properly next release."

Share this
← All Posts5 min read