DEV Community

Changenotes
Changenotes

Posted on

Stop Writing Changelogs by Hand: Automate Them with AI and GitHub Releases

Every dev team knows the feeling. You ship a release. Your PM asks "can you update the changelog?" You open CHANGELOG.md, stare at it for ten minutes, write something vague like "bug fixes and improvements," and close it. Then nobody reads it anyway.

But changelogs actually matter. Your users, your team, your future self — they all benefit from knowing what changed and why. The problem isn't motivation. It's friction.

In this post I'll show you how to completely automate changelog generation using GitHub releases and AI — so changelogs happen automatically every time you push a release, with zero extra work.

Why changelogs fail in practice

Most projects either skip changelogs entirely or maintain them inconsistently. The root cause is always the same: writing a good changelog is a separate task that happens after shipping, when you're already mentally done with the work.

The information already exists in your commits and PRs. You shouldn't have to write it a second time.

Here's what a typical GitHub release looks like internally:

{
  "tag_name": "v2.4.0",
  "name": "v2.4.0",
  "body": "## What's Changed\n* Fix login bug by @dev1\n* Add dark mode by @dev2\n* Bump deps by @dependabot",
  "published_at": "2026-03-05T10:00:00Z"
}
Enter fullscreen mode Exit fullscreen mode

That auto-generated release body is... fine. But it's not a changelog. It's just a commit list. There's no categorization, no context, no user-facing language. Your users don't care that you "Bump lodash from 4.17.20 to 4.17.21" — they care that you fixed the login bug.

The anatomy of a good changelog

A good changelog entry looks like this:

## v2.4.0 — March 5, 2026

### Features
- **Dark mode**: The app now supports system dark mode preference. Toggle in Settings > Appearance.

### Bug Fixes
- Fixed an issue where users would be logged out unexpectedly after 30 minutes of inactivity.

### Improvements
- Reduced initial page load time by 40% through code splitting.
- Dependencies updated to latest stable versions.
Enter fullscreen mode Exit fullscreen mode

Notice what changed:

  • Commits grouped by type (features, fixes, improvements)
  • Dependency bumps collapsed or omitted
  • User-facing language instead of developer-speak
  • Context about what the change means, not just that it happened

Writing this from scratch takes 20-30 minutes per release. Multiply by 2 releases a week and it's 2+ hours per month doing something a computer could do better.

How to automate it with GitHub webhooks

GitHub fires a release webhook event every time you publish a release. You can subscribe to this and trigger a pipeline that:

  1. Fetches the release's associated commits and merged PRs
  2. Categorizes them by type (feat/fix/chore in conventional commits, or by PR labels)
  3. Generates human-readable descriptions using an LLM
  4. Posts the result to your changelog

Here's a minimal Node.js webhook handler:

import Anthropic from "@anthropic-ai/sdk";

export async function handleGithubRelease(payload) {
  const { release, repository } = payload;

  // Fetch commits between this and the previous release
  const commits = await fetchCommitsSince(
    repository.full_name,
    release.tag_name
  );

  // Categorize by conventional commit prefix or PR labels
  const categorized = categorizeCommits(commits);

  // Generate human-readable changelog with Claude
  const client = new Anthropic();
  const response = await client.messages.create({
    model: "claude-opus-4-6",
    max_tokens: 1024,
    messages: [
      {
        role: "user",
        content: `Generate a user-facing changelog for version ${release.tag_name}.

Features: ${categorized.features.join("\n")}
Bug fixes: ${categorized.fixes.join("\n")}
Other: ${categorized.other.join("\n")}

Write in plain English. Focus on user impact, not implementation details.
Format as markdown with ### Features, ### Bug Fixes, ### Improvements sections.
Skip dependency updates unless they affect users.`,
      },
    ],
  });

  return response.content[0].text;
}

function categorizeCommits(commits) {
  return {
    features: commits
      .filter((c) => c.message.startsWith("feat"))
      .map((c) => c.message),
    fixes: commits
      .filter((c) => c.message.startsWith("fix"))
      .map((c) => c.message),
    other: commits
      .filter((c) => !c.message.match(/^(feat|fix)/))
      .map((c) => c.message),
  };
}
Enter fullscreen mode Exit fullscreen mode

This works, but there's a lot of boilerplate to handle: webhook verification, GitHub API auth, pagination, edge cases (merge commits, revert commits, bot commits), storage, hosting the changelog page, RSS feeds...

What we built: Changenotes

We were building this exact pipeline internally and decided to package it as a service: Changenotes.

The flow is:

  1. Connect your GitHub repo (OAuth, takes 10 seconds)
  2. Push a release as you normally would
  3. Changenotes generates a categorized, AI-written changelog entry automatically
  4. Review the draft, edit if needed, publish to your hosted changelog page

The generated output handles the hard parts automatically:

  • Dependency bumps are collapsed or omitted
  • Bot commits (Dependabot, Renovate) are filtered
  • Conventional commits are parsed, but non-conventional repos work too (it reads PR descriptions)
  • Everything is editable before publishing — the AI draft is a starting point, not final output

Each project gets a public changelog page at changenotes.app/your-org/your-repo with an RSS feed, so users can subscribe.

Setting it up in your project

If you want to build your own:

Option 1: GitHub Action

name: Generate Changelog
on:
  release:
    types: [published]

jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Generate changelog
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          # Fetch commits since last release
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^)
          git log ${PREV_TAG}..HEAD --oneline > commits.txt

          # Call your LLM to generate the changelog
          # (implementation left as exercise)
Enter fullscreen mode Exit fullscreen mode

Option 2: Webhook + serverless function

Deploy a Vercel/Cloudflare Worker that receives GitHub's release webhook, processes commits, and stores the changelog. You control the data, but you own the infrastructure.

Option 3: Use Changenotes

If you'd rather skip the plumbing: changenotes.app — $29/mo per workspace, 14-day free trial, no credit card required. We're also planning a free tier for open-source projects.

Conventional commits help a lot

If your team uses Conventional Commits, changelog generation becomes much more reliable. The format:

feat(auth): add OAuth2 login with GitHub
fix(api): handle rate limit errors gracefully
chore(deps): update dependencies
docs: update README with new setup steps
Enter fullscreen mode Exit fullscreen mode

The type prefix (feat, fix, chore, docs) gives the LLM strong signal for categorization. Without it, the AI has to infer from commit message content alone, which usually works but occasionally misfires on ambiguous messages.

Even if you're not using conventional commits today, it's worth adopting incrementally. Start with just feat: and fix: prefixes on important commits.

The result

After setting this up, changelogs stop being a chore. They become automatic. Your users get clear, consistent release notes. Your team never has to argue about whether something is worth documenting.

Here's what an auto-generated entry looks like for a real release:

## v3.1.0 — March 5, 2026

### Features
- **Team workspaces**: Invite collaborators to manage changelogs together.
  Roles: Owner, Editor, Viewer.
- **RSS feed**: Subscribe to changelogs at /rss. Each project now includes
  a feed URL in the header.

### Bug Fixes
- Fixed incorrect date formatting in changelog entries for non-US locales.
- Resolved an issue where webhooks would fail silently after GitHub token expiry.

### Improvements
- Changelog generation is now 2x faster due to parallel commit processing.
Enter fullscreen mode Exit fullscreen mode

That took zero minutes to write. The AI read the commits, categorized them, wrote user-facing descriptions, and formatted everything. You'd still review and edit it — but the blank-page problem is gone.


If you found this useful, Changenotes is what we built to solve this for our own projects. Happy to answer questions in the comments about the implementation details.

Top comments (0)