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.
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
pip install anthropic gitpython python-dotenvStep 1: Extract Git Commits
# 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
# 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].textStep 3: Output Options
# 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
# .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.mdConsistent, 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
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
Build an AI-Powered CI/CD Pipeline Failure Analyzer with LangChain
Build a tool that automatically reads CI/CD failure logs, uses LangChain + Claude to diagnose the root cause, and posts a clear explanation with fix suggestions to your PR.
Auto-Generate Terraform Modules Using OpenAI Function Calling
Build a tool that takes plain English descriptions and generates production-ready Terraform modules using OpenAI's function calling API. No more starting from scratch.
Build an AI Cloud Cost Anomaly Detector with Claude API + AWS Cost Explorer
Cloud costs spike without warning. Build a Python bot using AWS Cost Explorer + Claude API that detects anomalies using Z-score analysis and explains the spike in plain English.