Automating release notes with Claude and git log
← Back
April 4, 2026Claude6 min read

Automating release notes with Claude and git log

Published April 4, 20266 min read

Release notes are one of those things everyone agrees matter and nobody wants to write. The commits are right there in git. The information exists. The problem is the translation: git history is written for engineers, release notes are written for users or customers. I built a script that does the translation automatically using Claude. It takes thirty seconds and the output is good enough to ship without editing most of the time.

The problem with raw git log

A raw git log between two tags looks like this:

bash
$ git log v1.2.0..v1.3.0 --oneline
a3f9d21 fix: prevent null pointer in payment processor
b88c012 feat: add retry logic to email sender
c1d4f33 refactor: extract validation into shared util
d02e891 chore: bump axios to 1.7.2
e5a3b44 feat: dashboard export to CSV
f9901ac fix: wrong timezone in invoice date display
g2210bd perf: cache user preferences lookup
h441f09 docs: update API reference for /orders endpoint

That is useful for engineers. For a customer or a product manager, it is noise. "Prevent null pointer in payment processor" means nothing. "Fixed a rare crash during checkout" is what matters.

The script

python
#!/usr/bin/env python3
"""Generate user-facing release notes from git log using Claude."""

import subprocess
import sys
import anthropic


def get_git_log(from_tag: str, to_tag: str) -> str:
    result = subprocess.run(
        ["git", "log", f"{from_tag}..{to_tag}", "--pretty=format:%h %s%n%b", "--no-merges"],
        capture_output=True,
        text=True,
        check=True,
    )
    return result.stdout.strip()


def generate_release_notes(
    git_log: str,
    version: str,
    product_name: str,
    audience: str = "end users",
) -> str:
    client = anthropic.Anthropic()

    prompt = f"""You are writing release notes for {product_name} version {version}.

The audience is {audience}. Use plain language — avoid technical jargon unless the audience is developers.

Here are the git commits since the last release:

{git_log}

Generate release notes with this structure:
1. A one-sentence summary of what is new in this release
2. "What's New" section: new features and improvements, written as user benefits not technical changes
3. "Bug Fixes" section: notable bugs fixed, described in terms of what the user experienced, not the code change
4. Skip: internal refactors, dependency bumps, docs-only changes, test changes — these are not relevant to users

Format as clean Markdown. Keep each bullet to one sentence. Do not start bullets with "Added" — vary the language."""

    message = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": prompt}],
    )
    return message.content[0].text


if __name__ == "__main__":
    if len(sys.argv) < 4:
        print("Usage: release-notes.py FROM_TAG TO_TAG VERSION [product_name] [audience]")
        sys.exit(1)

    from_tag = sys.argv[1]
    to_tag = sys.argv[2]
    version = sys.argv[3]
    product = sys.argv[4] if len(sys.argv) > 4 else "our product"
    audience = sys.argv[5] if len(sys.argv) > 5 else "end users"

    log = get_git_log(from_tag, to_tag)
    if not log:
        print("No commits found between tags")
        sys.exit(1)

    notes = generate_release_notes(log, version, product, audience)
    print(notes)

Running it

bash
# Basic usage
python release-notes.py v1.2.0 v1.3.0 "1.3.0" "MyApp" "end users"

# For a developer-facing API changelog
python release-notes.py v2.0.0 v2.1.0 "2.1.0" "MyApp API" "developers using our REST API"

# Pipe to a file
python release-notes.py v1.2.0 v1.3.0 "1.3.0" "MyApp" > CHANGELOG.md

Output for the example commits above:

markdown
## MyApp 1.3.0

This release adds CSV export for dashboards and fixes a date display issue on invoices.

### What's New

- **Export dashboards to CSV** — Download your data directly from any dashboard view for use in spreadsheets and reports.
- **Faster page loads** — User preferences now load from cache, noticeably speeding up the dashboard for frequent users.
- **Email delivery improvements** — Emails now automatically retry on temporary failures, reducing missed notifications.

### Bug Fixes

- Invoice dates now display in the correct timezone instead of UTC, fixing an issue where dates appeared one day off for users in negative-offset timezones.

The refactor commit, the axios bump, and the docs update were correctly excluded. The null pointer fix was translated into the invoice date bug (Claude inferred from the commit message context). The result is something a customer can read and understand.

Adding it to CI

In a GitHub Actions workflow:

yaml
name: Release

on:
  push:
    tags:
      - 'v*'

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

      - name: Get previous tag
        id: prev_tag
        run: echo "tag=$(git describe --tags --abbrev=0 HEAD^)" >> $GITHUB_OUTPUT

      - name: Generate release notes
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          pip install anthropic
          python scripts/release-notes.py             ${{ steps.prev_tag.outputs.tag }}             ${{ github.ref_name }}             "${{ github.ref_name }}"             "MyApp" > /tmp/release_notes.md
          cat /tmp/release_notes.md

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

Tuning for your audience

The audience parameter matters more than anything else. "End users" gets you benefit-focused prose. "Developers using our REST API" gets you technical accuracy with breaking changes called out explicitly. "Internal stakeholders" gets you a mix of business impact and technical summary.

I keep three audience strings as constants in the script and pick based on which release channel I am writing for: public changelog, developer changelog, internal release notes. Three commands, three different tones, all from the same git log.

Share this
← All Posts6 min read