🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Release Notes Generator with Claude API + GitPython

Build a Python tool using Claude API + GitPython that reads git commits, categorizes them by type, and auto-generates clean Markdown release notes — runs in CI before every release.

DevOpsBoysJun 11, 20264 min read
Share:Tweet

Writing release notes manually is tedious and inconsistent. This tool reads your git log, understands what changed, and generates clean, readable release notes — automatically.


What It Generates

## v2.4.0 — June 11, 2026

### 🚀 New Features
- Added Kubernetes HPA support for automatic pod scaling based on custom metrics
- New `/api/v2/health` endpoint with dependency status checks
- Docker multi-stage build support for 60% smaller image sizes

### 🐛 Bug Fixes
- Fixed race condition in token refresh causing intermittent 401 errors
- Resolved memory leak in WebSocket connection handler
- Fixed Helm chart values not applying when using --set with nested keys

### ⚙️ Infrastructure
- Upgraded base image to node:20-alpine (from node:18)
- Added GitHub Actions caching for Docker layers (3x faster builds)
- Terraform state now uses S3 with DynamoDB locking

### 📦 Dependencies
- Upgraded express from 4.18.2 to 4.19.1
- Updated aws-sdk to v3.500.0

Setup

bash
pip install anthropic gitpython python-dotenv

Step 1: Extract Git Commits

python
# git_parser.py
import git
from datetime import datetime
 
def get_commits_since_tag(repo_path: str = ".", since_tag: str = None, limit: int = 100) -> list[dict]:
    """Get commits since the last tag or a specific tag."""
    
    repo = git.Repo(repo_path)
    
    if since_tag:
        # Commits since a specific tag
        commits = list(repo.iter_commits(f"{since_tag}..HEAD", max_count=limit))
    else:
        # Commits since last tag
        try:
            last_tag = repo.tags[-1] if repo.tags else None
            if last_tag:
                commits = list(repo.iter_commits(f"{last_tag}..HEAD", max_count=limit))
            else:
                commits = list(repo.iter_commits("HEAD", max_count=limit))
        except Exception:
            commits = list(repo.iter_commits("HEAD", max_count=limit))
    
    result = []
    for commit in commits:
        result.append({
            "hash": commit.hexsha[:8],
            "message": commit.message.strip(),
            "author": commit.author.name,
            "date": datetime.fromtimestamp(commit.committed_date).strftime("%Y-%m-%d"),
            "files_changed": len(commit.stats.files)
        })
    
    return result
 
def get_version_bump(repo_path: str = ".") -> str:
    """Get suggested version from latest tag."""
    repo = git.Repo(repo_path)
    
    if not repo.tags:
        return "v1.0.0"
    
    latest = sorted(repo.tags, key=lambda t: t.commit.committed_date)[-1]
    tag_name = str(latest)
    
    # Parse semantic version
    parts = tag_name.lstrip("v").split(".")
    try:
        major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
        return f"v{major}.{minor}.{patch + 1}"  # Auto-bump patch
    except:
        return f"{tag_name}-next"

Step 2: Claude Generates Release Notes

python
# generator.py
import anthropic
import json
import os
 
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
 
def generate_release_notes(commits: list[dict], version: str, repo_name: str = "") -> str:
    """Generate formatted release notes from commits using Claude."""
    
    if not commits:
        return f"## {version}\n\nNo changes since last release."
    
    commits_text = "\n".join([
        f"- [{c['hash']}] {c['message']} (by {c['author']}, {c['files_changed']} files changed)"
        for c in commits
    ])
    
    prompt = f"""You are generating release notes for {repo_name or 'a software project'}.
 
Version: {version}
 
Git commits since last release:
{commits_text}
 
Generate clean, user-friendly release notes in Markdown. Follow these rules:
1. Group by category: New Features, Bug Fixes, Infrastructure/DevOps, Dependencies, Breaking Changes
2. Only include categories that have relevant commits
3. Rewrite commit messages to be clear to end users (not developers) — remove ticket numbers, fix typos, expand abbreviations
4. Use past tense ("Added X", "Fixed Y", "Updated Z")
5. Skip merge commits, version bumps, and minor chores
6. Add appropriate emoji per section: 🚀 Features, 🐛 Bug Fixes, ⚙️ Infrastructure, 📦 Dependencies, ⚠️ Breaking Changes
7. Keep each bullet to one clear sentence
 
Output only the Markdown — no preamble."""
 
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1500,
        messages=[{"role": "user", "content": prompt}]
    )
    
    return response.content[0].text
 
 
def generate_slack_summary(release_notes: str, version: str) -> str:
    """Generate a short Slack announcement from the release notes."""
    
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=300,
        messages=[{
            "role": "user",
            "content": f"""Summarize these release notes into a 3-5 bullet Slack announcement for {version}. 
Be concise and highlight the most important changes only.
 
{release_notes}
 
Format: plain text bullets starting with •, no markdown headers."""
        }]
    )
    
    return response.content[0].text

Step 3: Output Options

python
# main.py
import argparse
import os
from dotenv import load_dotenv
from git_parser import get_commits_since_tag, get_version_bump
from generator import generate_release_notes, generate_slack_summary
 
load_dotenv()
 
def main():
    parser = argparse.ArgumentParser(description="AI Release Notes Generator")
    parser.add_argument("--repo", default=".", help="Path to git repository")
    parser.add_argument("--since", help="Tag to generate notes since (default: last tag)")
    parser.add_argument("--version", help="Version string (default: auto-bumped)")
    parser.add_argument("--output", default="RELEASE_NOTES.md", help="Output file")
    parser.add_argument("--slack", action="store_true", help="Also generate Slack summary")
    parser.add_argument("--append", action="store_true", help="Append to existing file")
    args = parser.parse_args()
    
    print(f"Fetching commits from {args.repo}...")
    commits = get_commits_since_tag(args.repo, since_tag=args.since)
    print(f"Found {len(commits)} commits")
    
    version = args.version or get_version_bump(args.repo)
    repo_name = os.path.basename(os.path.abspath(args.repo))
    
    print(f"Generating release notes for {version}...")
    notes = generate_release_notes(commits, version, repo_name)
    
    # Write to file
    mode = "a" if args.append else "w"
    with open(args.output, mode) as f:
        f.write(notes + "\n\n---\n\n")
    
    print(f"Release notes written to {args.output}")
    print("\n" + notes)
    
    if args.slack:
        summary = generate_slack_summary(notes, version)
        print("\n--- Slack Summary ---")
        print(summary)
        with open("slack_announcement.txt", "w") as f:
            f.write(summary)
 
if __name__ == "__main__":
    main()

Add to CI/CD

yaml
# .github/workflows/release.yml
- name: Generate release notes
  run: |
    pip install anthropic gitpython
    python release_notes/main.py \
      --version ${{ github.ref_name }} \
      --output RELEASE_NOTES.md \
      --slack
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
 
- name: Create GitHub Release
  uses: softprops/action-gh-release@v2
  with:
    body_path: RELEASE_NOTES.md

Consistent, readable release notes on every tag — zero manual work. Claude understands context in commit messages and writes for users, not just developers.

Get your Anthropic API key — generating release notes for a typical sprint costs under $0.05.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments