← Blog

How to Build a Changelog Generator With Claude

2026-10-29 · 5 min read · SubToAPI Team

Writing changelogs by hand is tedious and inconsistent: one release gets a detailed writeup, the next gets "bug fixes." The fix is to build a changelog generator with Claude that reads your git history or merged pull requests and turns them into structured, human-readable release notes automatically.

This article walks through the full approach: pulling commit data, prompting Claude to classify and summarize it, formatting the output as Markdown, and wiring it into a CI pipeline so changelogs get generated on every release without anyone touching a text editor.

Why Claude works well for this

Changelog generation is a summarization and classification problem, not a creative one. Claude is good at:

The hard part isn't the AI call — it's getting clean input data and a prompt that produces consistent, parseable output every time.

Step 1: Collect the raw data

You have two reasonable sources:

Git log between tags, for a straightforward commit-based changelog:

git log v1.2.0..v1.3.0 --pretty=format:"%h|%s|%an" > commits.txt

Merged pull requests, if your team squashes commits and relies on PR titles/descriptions (more structured, usually better input):

gh pr list --state merged --base main --json title,body,number,mergedAt \
  --search "merged:>2024-01-01" > prs.json

PR data is generally higher quality because PR titles and descriptions are written for humans, not just for git log. If your team enforces conventional commits (feat:, fix:, chore:), the raw git log approach works fine too.

Step 2: Design the prompt

The goal is a prompt that reliably produces structured output you can parse or paste directly into CHANGELOG.md. Give Claude a fixed schema and a clear instruction to omit noise.

You are generating a changelog for a software release.

Below is a list of merged pull requests between the previous release and this one.
Classify each into one of: Added, Changed, Fixed, Removed, Security.
Ignore internal-only changes (CI config, formatting, dependency bumps with no
user-facing effect) unless they fix a security issue.

Write each entry as a single concise sentence, present tense, no PR numbers,
written for end users of the product — not for engineers.

Output valid Markdown using this exact structure:

## [VERSION]

### Added
- ...

### Changed
- ...

### Fixed
- ...

Pull requests:
{{pr_data}}

Feeding the model a fixed schema is the single biggest lever for consistency. Without it, you'll get wildly different formatting between runs — sometimes bullet points, sometimes paragraphs, sometimes extra commentary before the output.

Step 3: Call the API

Here's a minimal script that reads PR data, calls Claude, and writes the result to a file. If you're using SubToAPI to turn your Claude access into an HTTPS API with a stable key and usage tracking, the call looks like this:

import fs from "fs";

const prs = JSON.parse(fs.readFileSync("prs.json", "utf-8"));
const prText = prs
  .map((pr) => `#${pr.number}: ${pr.title}\n${pr.body ?? ""}`)
  .join("\n\n");

const response = await fetch("https://api.subtoapi.app/v1/messages", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-4",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: PROMPT_TEMPLATE.replace("{{pr_data}}", prText).replace(
          "VERSION",
          process.env.RELEASE_VERSION
        ),
      },
    ],
  }),
});

const data = await response.json();
const changelog = data.content[0].text;

fs.writeFileSync(`changelog-${process.env.RELEASE_VERSION}.md`, changelog);

The request structure follows the standard Messages API format, so if you've already integrated Claude elsewhere, this slots into the same code path. Full setup steps are in the quickstart.

Step 4: Prepend to your existing CHANGELOG.md

Generating a fresh file per release is fine for review, but you'll usually want it merged into one running file:

const existing = fs.readFileSync("CHANGELOG.md", "utf-8");
fs.writeFileSync("CHANGELOG.md", `${changelog}\n\n${existing}`);

Run this as a step before committing and tagging the release, so the changelog entry lands in the same commit as the version bump.

Step 5: Automate it in CI

Trigger the generator whenever a release tag is pushed:

name: Generate Changelog
on:
  push:
    tags:
      - "v*"

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: node generate-changelog.js
        env:
          SUBTOAPI_KEY: ${{ secrets.SUBTOAPI_KEY }}
          RELEASE_VERSION: ${{ github.ref_name }}
      - run: |
          git config user.name "changelog-bot"
          git add CHANGELOG.md
          git commit -m "docs: update changelog for ${{ github.ref_name }}"
          git push

This keeps the entire process — fetch, generate, commit — tied to the tagging workflow with no manual step.

Handling edge cases

A few things worth building in from the start:

If you want to go further, you can add tool use so Claude fetches PR data itself via a function call instead of you pre-fetching it — useful if you're generating changelogs on demand from a Slack command or internal tool rather than purely in CI.

Running this at team scale — multiple repos, multiple release trains — is where having actual API infrastructure (keys, usage metadata, per-project limits) matters more than a one-off script. That's the gap SubToAPI fills: your existing Claude access becomes a proper API with keys and dashboards, which is useful once more than one person or one repo depends on the generator. Check pricing or just sign up and get a key in a few minutes.

questions

Do I need conventional commits for this to work? No. Structured commit messages help, but Claude can extract intent from messy, free-form commit messages. PR titles and descriptions tend to produce better results than raw commit logs regardless of format.

How do I prevent inconsistent formatting between releases? Give the model a fixed Markdown schema in the prompt and keep it identical across runs. Most formatting drift comes from vague instructions, not model inconsistency.

Can this replace a human review step entirely? For internal tools, yes. For public-facing release notes, treat the generated changelog as a strong first draft — have someone skim it before publishing, especially for breaking changes or security fixes.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →