EzAI
Back to Blog
Use Cases Mar 6, 2026 9 min read

Build an AI Changelog Generator from Git Commits

E

EzAI Team

Build an AI Changelog Generator from Git Commits

Writing changelogs is one of those tasks every developer knows they should do but nobody enjoys. You tag a release, stare at 47 commit messages like fix stuff and wip, and spend 30 minutes turning them into something your users can actually read. Let's automate that entirely with Claude API and Python — from raw Git log to a clean, categorized changelog in seconds.

How It Works

The approach is straightforward: extract commits between two Git tags (or a date range), send them to Claude with instructions on how to categorize and rewrite them, then output formatted Markdown. Claude is especially good at this because it understands code context — it knows that refactor auth middleware belongs under "Internal Changes" while add dark mode toggle is a user-facing feature.

AI Changelog Pipeline Flow — from Git commits to formatted release notes

The complete pipeline: Git log → Claude API → professional changelog

Here's what we're building:

  • A Python script that reads Git history between two refs
  • Claude API call that categorizes and rewrites commits into human-readable entries
  • Automatic grouping into Features, Fixes, Performance, and Breaking Changes
  • Markdown output ready for GitHub Releases or your docs site

The Core Script

First, install the dependencies:

bash
pip install anthropic

Now the main script. This extracts commits from Git and sends them to Claude for processing:

python
import subprocess
import anthropic
import sys

def get_commits(from_ref, to_ref="HEAD"):
    """Extract commits between two Git refs."""
    result = subprocess.run(
        ["git", "log", "--pretty=format:%h|%s|%an",
         f"{from_ref}..{to_ref}"],
        capture_output=True, text=True
    )
    commits = []
    for line in result.stdout.strip().split("\n"):
        if not line:
            continue
        hash, msg, author = line.split("|", 2)
        commits.append({"hash": hash, "message": msg, "author": author})
    return commits

def generate_changelog(commits, version):
    """Send commits to Claude and get a formatted changelog."""
    client = anthropic.Anthropic(
        base_url="https://ezaiapi.com"
    )

    commit_text = "\n".join(
        f"- {c['hash']}: {c['message']} ({c['author']})"
        for c in commits
    )

    message = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=2048,
        messages=[{
            "role": "user",
            "content": f"""Generate a changelog for version {version}.

Raw commits:
{commit_text}

Rules:
1. Group into: 🚀 Features, 🐛 Bug Fixes, ⚡ Performance, 🔧 Internal, 💥 Breaking Changes
2. Rewrite each commit as a clear, user-facing description
3. Skip merge commits and CI-only changes
4. If a commit message is vague (e.g. "fix stuff"), infer from context
5. Output clean Markdown, no extra commentary"""
        }]
    )

    return message.content[0].text

# Usage
if __name__ == "__main__":
    from_tag = sys.argv[1]  # e.g., v1.2.0
    version = sys.argv[2]   # e.g., v1.3.0

    commits = get_commits(from_tag)
    if not commits:
        print("No commits found between refs.")
        sys.exit(1)

    print(f"Found {len(commits)} commits. Generating changelog...")
    changelog = generate_changelog(commits, version)
    print(changelog)

Run it against your repo:

bash
python changelog.py v1.2.0 v1.3.0

Claude takes those raw commits and produces something like this:

markdown
## v1.3.0

### 🚀 Features
- Added dark mode toggle with system preference detection
- New CSV export for analytics dashboard
- Webhook retry configuration (max attempts, backoff)

### 🐛 Bug Fixes
- Fixed pagination breaking on filtered views
- Resolved memory leak in WebSocket connection handler

### ⚡ Performance
- Database queries now use prepared statements (40% faster)
- Lazy-load images on the landing page

Adding Diff Context for Better Results

Commit messages are often useless — fix, update, wip. The trick is to include the actual diff so Claude can infer what changed. Here's an enhanced version that attaches a summary of changed files:

python
def get_commits_with_diff(from_ref, to_ref="HEAD"):
    """Extract commits with file-level diff stats."""
    result = subprocess.run(
        ["git", "log", "--pretty=format:COMMIT|%h|%s|%an",
         "--stat", f"{from_ref}..{to_ref}"],
        capture_output=True, text=True
    )

    commits = []
    current = None

    for line in result.stdout.split("\n"):
        if line.startswith("COMMIT|"):
            if current:
                commits.append(current)
            parts = line.split("|", 3)
            current = {
                "hash": parts[1],
                "message": parts[2],
                "author": parts[3],
                "files": []
            }
        elif current and "|" in line:
            # Parse stat lines like: src/auth.py | 15 +++---
            filename = line.split("|")[0].strip()
            if filename:
                current["files"].append(filename)

    if current:
        commits.append(current)
    return commits

With file paths included, Claude can turn a1b2c3: fix → auth.py, middleware.py into "Fixed authentication middleware failing to refresh expired tokens." That's a massive quality improvement over guessing from commit messages alone.

Automating Releases in CI/CD

The real power is running this automatically on every release. Here's a GitHub Actions workflow that generates the changelog and creates a GitHub Release:

yaml
name: Release with AI Changelog
on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for git log

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - run: pip install anthropic

      - name: Generate Changelog
        env:
          ANTHROPIC_API_KEY: ${{ secrets.EZAI_API_KEY }}
        run: |
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^)
          python changelog.py $PREV_TAG ${{ github.ref_name }} > RELEASE_NOTES.md

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          body_path: RELEASE_NOTES.md

Set EZAI_API_KEY in your repo secrets, and every tagged release gets a professionally written changelog automatically. Using EzAI's pricing, each changelog generation costs roughly $0.01–0.03 depending on commit volume.

Handling Large Releases

If your release has 200+ commits, you'll hit token limits. The fix: batch commits and merge results.

python
def generate_changelog_batched(commits, version, batch_size=50):
    """Process large commit lists in batches."""
    client = anthropic.Anthropic(base_url="https://ezaiapi.com")
    partial_logs = []

    for i in range(0, len(commits), batch_size):
        batch = commits[i:i + batch_size]
        commit_text = "\n".join(
            f"- {c['hash']}: {c['message']}" for c in batch
        )
        resp = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=1024,
            messages=[{"role": "user",
                       "content": f"Categorize these commits as changelog entries:\n{commit_text}"}]
        )
        partial_logs.append(resp.content[0].text)

    # Final merge pass
    combined = "\n\n".join(partial_logs)
    merge = client.messages.create(
        model="claude-sonnet-4-5",
        max_tokens=2048,
        messages=[{"role": "user",
                   "content": f"Merge these partial changelogs into one clean changelog for {version}. Deduplicate and group properly:\n\n{combined}"}]
    )
    return merge.content[0].text

This two-pass approach keeps each API call under the token limit while still producing a cohesive final output. For most repos, 50 commits per batch is the sweet spot — you could also use streaming to show progress in real time.

Tips for Production Use

  • Cache results: Store generated changelogs in a .changelog/ directory keyed by commit range. No need to regenerate the same range twice. Check our caching guide for implementation patterns.
  • Use Sonnet for speed: Changelog generation doesn't need Opus-level reasoning. Sonnet produces identical quality here at 5x lower cost.
  • Add a review step: Pipe the output to a file, let a maintainer review before publishing. AI catches 95% of the formatting — humans catch the remaining 5% of context errors.
  • Include PR numbers: Add --grep or parse PR references from commit messages. Claude will naturally link them in the output if you ask it to.
  • Handle errors gracefully: Wrap the API call in retry logic. See our rate limit handling guide for robust patterns.

Why This Beats Template-Based Tools

Tools like conventional-changelog require strict commit formats. If your team doesn't follow Conventional Commits perfectly (most don't), those tools produce garbage. Claude doesn't care about format — it reads the commit, understands the intent, and writes a clear description. It handles:

  • Mixed commit styles across contributors
  • Vague messages like fix typo or update deps
  • Squashed PR commits with multi-line bodies
  • Non-English commit messages (translated automatically)

The cost is negligible. A 100-commit changelog costs about $0.02 through EzAI, and you get output that would take a human 20-30 minutes to write manually.

Wrapping Up

You now have a complete AI changelog pipeline: Git commits go in, professional release notes come out. The full script is under 80 lines of Python, runs in CI, and costs pennies per release. Fork it, customize the prompt for your project's style, and never hand-write a changelog again.

Ready to try it? Grab an API key from your dashboard and start generating changelogs in minutes. Check out our getting started guide if you haven't set up EzAI yet.


Related Posts