How to Build a Claude-Powered Release Notes Generator
Writing release notes is the kind of task every team knows it should do well and almost never does. Commit messages are inconsistent, PR descriptions range from detailed to nonexistent, and someone ends up manually reconstructing "what changed" the night before a release. A Claude-powered release notes generator solves this by taking raw git history — commits, PR titles, diffs — and turning it into structured, human-readable changelogs automatically, on every release.
This guide walks through the actual architecture: how to pull the right source data, how to prompt Claude so it produces consistent categorized output instead of a wall of text, and how to wire it into a release pipeline so it runs without anyone remembering to trigger it.
Why this is a good fit for an LLM
Release notes generation is fundamentally a summarization and classification task — exactly what Claude is good at. The hard part isn't "can an LLM write prose," it's getting consistent structure (Added / Fixed / Changed / Breaking) and filtering out noise like chore: bump deps or fix typo commits that nobody needs to read about.
The pattern that works reliably in practice is:
- Collect commits and/or merged PRs since the last tag.
- Feed Claude the raw list with a strict output schema.
- Have Claude return structured JSON, not freeform markdown, so you can render it consistently or feed it into other tools (Slack, a changelog page, a GitHub release body).
- Post-process: dedupe, sort by category, inject version number and date.
Step 1: Collect the input data
You need commit messages, and ideally PR titles and labels — commit messages alone are often too terse. A simple git-based approach:
git log v1.4.0..HEAD --pretty=format:"%s (%an)" > changes.txt
If you use GitHub, pulling merged PRs since the last tag gives better quality input because PR titles are usually more descriptive than individual commits:
gh pr list --state merged --base main --json title,number,labels \
--search "merged:>=2024-05-01" > prs.json
Either source works. The key is to give Claude enough raw signal — don't pre-summarize it yourself, let the model do that part.
Step 2: Design the prompt for structured output
The biggest mistake teams make here is asking for freeform markdown directly. You get inconsistent section headers, inconsistent tone, and no way to programmatically check the output. Instead, ask for JSON with a fixed schema, then render markdown yourself.
{
"version": "1.5.0",
"date": "2024-06-01",
"added": ["..."],
"fixed": ["..."],
"changed": ["..."],
"breaking": ["..."]
}
A prompt that works well:
You are generating release notes from raw commit/PR data.
Categorize each item into exactly one of: added, fixed, changed, breaking.
Ignore noise: formatting-only changes, dependency bumps, typo fixes, CI/test-only changes.
Merge near-duplicate entries into one.
Write each entry as one sentence, user-facing language, no commit hashes, no author names.
Return only valid JSON matching this schema: {added: [], fixed: [], changed: [], breaking: []}
Raw input:
<paste commit/PR list here>
Claude is reliable at this kind of filtering-and-classification task when the rules are explicit. Being explicit about what to exclude matters as much as what to include — without that instruction, models tend to include every single commit, which defeats the purpose.
Step 3: Call the API
If you're using SubToAPI as your access layer, the call looks like this — same Messages API shape, your own sub_live_ key:
curl https://api.subtoapi.app/v1/messages \
-H "Authorization: Bearer $SUBTOAPI_KEY" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-4-5",
"max_tokens": 1500,
"messages": [
{"role": "user", "content": "<your prompt + raw commit list here>"}
]
}'
For teams already running Claude through an API key, this fits directly into a release script, a CI job, or a CLI tool run before tagging a version. The full request format is in the docs, and the quickstart covers getting a key provisioned if you haven't set one up yet.
Step 4: Render and distribute
Once you have structured JSON back, rendering markdown is trivial and deterministic — no LLM needed for this part:
function renderChangelog(data) {
const section = (title, items) =>
items.length ? `### ${title}\n${items.map(i => `- ${i}`).join("\n")}\n` : "";
return `## v${data.version} — ${data.date}\n\n` +
section("Breaking Changes", data.breaking) +
section("Added", data.added) +
section("Changed", data.changed) +
section("Fixed", data.fixed);
}
From here you can push the result to a CHANGELOG.md, post it to a GitHub release body via gh release create, or send it to a Slack channel so the team sees what shipped without digging through PRs.
Step 5: Automate it in CI
Wire this into your release workflow so it runs on tag creation instead of relying on someone to do it manually:
- name: Generate release notes
run: node scripts/generate-release-notes.js
env:
SUBTOAPI_KEY: ${{ secrets.SUBTOAPI_KEY }}
The script collects commits since the last tag, calls the API, renders markdown, and attaches it to the GitHub release. Once this is in place, release notes stop being a chore and become a byproduct of tagging a version.
Keep raw data out of the model when it matters
If your repo contains sensitive internal context in commit messages — ticket numbers tied to customer names, internal server details — strip that before sending it to the API. Release notes generation is low-risk compared to something like contract analysis, but it's still worth a quick regex pass to scrub anything that shouldn't leave your infrastructure.
Where SubToAPI fits
If you already have Claude access through a personal or team subscription and want to call it from a script or CI pipeline, SubToAPI turns that into a proper HTTPS API with a sub_live_ key, streaming, and usage metadata — without needing a separate Anthropic API account. For a release notes generator specifically, usage is typically light (one call per release), which makes it a good low-cost first automation to try. See pricing for plan details, or start with the free trial at signup.
Questions
Does this work with squash-merged PRs only, or do I need full commit history? Either works. Squash-merged PR titles are often cleaner input than raw commit logs, since they're usually written with more context. Full commit history gives more detail but more noise — the filtering instructions in the prompt handle that either way.
How do I stop Claude from including internal commits like "fix lint" in the output? Be explicit in the prompt about what to exclude (chore, CI-only, formatting-only, dependency bumps) rather than relying on the model to infer it. Explicit exclusion rules are far more reliable than implicit ones.
Can I get streaming output for long changelogs? Yes — for most releases a single non-streaming call is fine since output is short, but if you're generating notes for a large multi-month release with many entries, streaming avoids timeout issues on longer generations. See streaming docs for the request format.