<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Changenotes</title>
    <description>The latest articles on DEV Community by Changenotes (@changenotes_dev).</description>
    <link>https://dev.to/changenotes_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3808669%2F142b81b1-db63-418c-8892-abcb75b224c8.png</url>
      <title>DEV Community: Changenotes</title>
      <link>https://dev.to/changenotes_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/changenotes_dev"/>
    <language>en</language>
    <item>
      <title>Stop Writing Changelogs by Hand: Automate Them with AI and GitHub Releases</title>
      <dc:creator>Changenotes</dc:creator>
      <pubDate>Tue, 17 Mar 2026 11:42:12 +0000</pubDate>
      <link>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-pdf</link>
      <guid>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-pdf</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why changelogs fail in practice
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;after&lt;/em&gt; shipping, when you're already mentally done with the work.&lt;/p&gt;

&lt;p&gt;The information already exists in your commits and PRs. You shouldn't have to write it a second time.&lt;/p&gt;

&lt;p&gt;Here's what a typical GitHub release looks like internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tag_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"## What's Changed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Fix login bug by @dev1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Add dark mode by @dev2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Bump deps by @dependabot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-05T10:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a good changelog
&lt;/h2&gt;

&lt;p&gt;A good changelog entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## v2.4.0 — March 5, 2026&lt;/span&gt;

&lt;span class="gu"&gt;### Features&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Dark mode**&lt;/span&gt;: The app now supports system dark mode preference. Toggle in Settings &amp;gt; Appearance.

&lt;span class="gu"&gt;### Bug Fixes&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fixed an issue where users would be logged out unexpectedly after 30 minutes of inactivity.

&lt;span class="gu"&gt;### Improvements&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Reduced initial page load time by 40% through code splitting.
&lt;span class="p"&gt;-&lt;/span&gt; Dependencies updated to latest stable versions.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commits grouped by type (features, fixes, improvements)&lt;/li&gt;
&lt;li&gt;Dependency bumps collapsed or omitted&lt;/li&gt;
&lt;li&gt;User-facing language instead of developer-speak&lt;/li&gt;
&lt;li&gt;Context about &lt;em&gt;what&lt;/em&gt; the change means, not just that it happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to automate it with GitHub webhooks
&lt;/h2&gt;

&lt;p&gt;GitHub fires a &lt;code&gt;release&lt;/code&gt; webhook event every time you publish a release. You can subscribe to this and trigger a pipeline that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the release's associated commits and merged PRs&lt;/li&gt;
&lt;li&gt;Categorizes them by type (feat/fix/chore in conventional commits, or by PR labels)&lt;/li&gt;
&lt;li&gt;Generates human-readable descriptions using an LLM&lt;/li&gt;
&lt;li&gt;Posts the result to your changelog&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a minimal Node.js webhook handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleGithubRelease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCommitsSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;categorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;categorizeCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Generate a user-facing changelog for version &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.

Features: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Bug fixes: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Other: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;

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.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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...&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built: Changenotes
&lt;/h2&gt;

&lt;p&gt;We were building this exact pipeline internally and decided to package it as a service: &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect your GitHub repo (OAuth, takes 10 seconds)&lt;/li&gt;
&lt;li&gt;Push a release as you normally would&lt;/li&gt;
&lt;li&gt;Changenotes generates a categorized, AI-written changelog entry automatically&lt;/li&gt;
&lt;li&gt;Review the draft, edit if needed, publish to your hosted changelog page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated output handles the hard parts automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency bumps are collapsed or omitted&lt;/li&gt;
&lt;li&gt;Bot commits (Dependabot, Renovate) are filtered&lt;/li&gt;
&lt;li&gt;Conventional commits are parsed, but non-conventional repos work too&lt;/li&gt;
&lt;li&gt;Everything is editable before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project gets a public changelog page at &lt;code&gt;changenotes.app/your-org/your-repo&lt;/code&gt; with an RSS feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventional commits help a lot
&lt;/h2&gt;

&lt;p&gt;If your team uses &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt;, changelog generation becomes much more reliable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type prefix 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's what an auto-generated entry looks like for a real release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## v3.1.0 — March 5, 2026&lt;/span&gt;

&lt;span class="gu"&gt;### Features&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Team workspaces**&lt;/span&gt;: Invite collaborators to manage changelogs together.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**RSS feed**&lt;/span&gt;: Subscribe to changelogs at /rss.

&lt;span class="gu"&gt;### Bug Fixes&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fixed incorrect date formatting for non-US locales.
&lt;span class="p"&gt;-&lt;/span&gt; Resolved an issue where webhooks would fail silently after GitHub token expiry.

&lt;span class="gu"&gt;### Improvements&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Changelog generation is now 2x faster due to parallel commit processing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 — but the blank-page problem is gone.&lt;/p&gt;




&lt;p&gt;If you found this useful, &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt; is what we built to solve this for our own projects. $9/mo per workspace, 14-day free trial. Open-source projects get free access. Happy to answer questions in the comments about the implementation details.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>devops</category>
      <category>github</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Writing Changelogs by Hand: Automate Them with AI and GitHub Releases</title>
      <dc:creator>Changenotes</dc:creator>
      <pubDate>Fri, 06 Mar 2026 06:15:51 +0000</pubDate>
      <link>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-23f</link>
      <guid>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-23f</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why changelogs fail in practice
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;after&lt;/em&gt; shipping, when you're already mentally done with the work.&lt;/p&gt;

&lt;p&gt;The information already exists in your commits and PRs. You shouldn't have to write it a second time.&lt;/p&gt;

&lt;p&gt;Here's what a typical GitHub release looks like internally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tag_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v2.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"## What's Changed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Fix login bug by @dev1&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Add dark mode by @dev2&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Bump deps by @dependabot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-05T10:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a good changelog
&lt;/h2&gt;

&lt;p&gt;A good changelog entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## v2.4.0 — March 5, 2026

### Features
- **Dark mode**: The app now supports system dark mode preference. Toggle in Settings &amp;gt; 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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commits grouped by type (features, fixes, improvements)&lt;/li&gt;
&lt;li&gt;Dependency bumps collapsed or omitted&lt;/li&gt;
&lt;li&gt;User-facing language instead of developer-speak&lt;/li&gt;
&lt;li&gt;Context about &lt;em&gt;what&lt;/em&gt; the change means, not just that it happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to automate it with GitHub webhooks
&lt;/h2&gt;

&lt;p&gt;GitHub fires a &lt;code&gt;release&lt;/code&gt; webhook event every time you publish a release. You can subscribe to this and trigger a pipeline that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the release's associated commits and merged PRs&lt;/li&gt;
&lt;li&gt;Categorizes them by type (feat/fix/chore in conventional commits, or by PR labels)&lt;/li&gt;
&lt;li&gt;Generates human-readable descriptions using an LLM&lt;/li&gt;
&lt;li&gt;Posts the result to your changelog&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a minimal Node.js webhook handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleGithubRelease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch commits between this and the previous release&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCommitsSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Categorize by conventional commit prefix or PR labels&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;categorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;categorizeCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Generate human-readable changelog with Claude&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Generate a user-facing changelog for version &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.

Features: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;features&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Bug fixes: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Other: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;

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.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;categorizeCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;feat&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;fixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fix&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;other&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;feat|fix&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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...&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built: Changenotes
&lt;/h2&gt;

&lt;p&gt;We were building this exact pipeline internally and decided to package it as a service: &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect your GitHub repo (OAuth, takes 10 seconds)&lt;/li&gt;
&lt;li&gt;Push a release as you normally would&lt;/li&gt;
&lt;li&gt;Changenotes generates a categorized, AI-written changelog entry automatically&lt;/li&gt;
&lt;li&gt;Review the draft, edit if needed, publish to your hosted changelog page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated output handles the hard parts automatically:&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Setting it up in your project
&lt;/h2&gt;

&lt;p&gt;If you want to build your own:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1: GitHub Action&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate Changelog&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;changelog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate changelog&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# Fetch commits since last release&lt;/span&gt;
          &lt;span class="s"&gt;PREV_TAG=$(git describe --tags --abbrev=0 HEAD^)&lt;/span&gt;
          &lt;span class="s"&gt;git log ${PREV_TAG}..HEAD --oneline &amp;gt; commits.txt&lt;/span&gt;

          &lt;span class="s"&gt;# Call your LLM to generate the changelog&lt;/span&gt;
          &lt;span class="s"&gt;# (implementation left as exercise)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2: Webhook + serverless function&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Option 3: Use Changenotes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you'd rather skip the plumbing: &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;changenotes.app&lt;/a&gt; — $9/mo per workspace, 14-day free trial, no credit card required. Open-source projects get free access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventional commits help a lot
&lt;/h2&gt;

&lt;p&gt;If your team uses &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt;, changelog generation becomes much more reliable. The format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type prefix (&lt;code&gt;feat&lt;/code&gt;, &lt;code&gt;fix&lt;/code&gt;, &lt;code&gt;chore&lt;/code&gt;, &lt;code&gt;docs&lt;/code&gt;) 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.&lt;/p&gt;

&lt;p&gt;Even if you're not using conventional commits today, it's worth adopting incrementally. Start with just &lt;code&gt;feat:&lt;/code&gt; and &lt;code&gt;fix:&lt;/code&gt; prefixes on important commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here's what an auto-generated entry looks like for a real release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## 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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;If you found this useful, &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt; is what we built to solve this for our own projects. Happy to answer questions in the comments about the implementation details.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>github</category>
      <category>devtools</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Stop Writing Changelogs by Hand: Automate Them with AI and GitHub Releases</title>
      <dc:creator>Changenotes</dc:creator>
      <pubDate>Thu, 05 Mar 2026 21:40:28 +0000</pubDate>
      <link>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-2i8m</link>
      <guid>https://dev.to/changenotes_dev/stop-writing-changelogs-by-hand-automate-them-with-ai-and-github-releases-2i8m</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why changelogs fail in practice
&lt;/h2&gt;

&lt;p&gt;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 &lt;em&gt;after&lt;/em&gt; shipping, when you're already mentally done with the work.&lt;/p&gt;

&lt;p&gt;The information already exists in your commits and PRs. You shouldn't have to write it a second time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anatomy of a good changelog
&lt;/h2&gt;

&lt;p&gt;A good changelog entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## v2.4.0 — March 5, 2026

### Features
- **Dark mode**: The app now supports system dark mode preference.

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

### Improvements
- Reduced initial page load time by 40% through code splitting.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commits grouped by type (features, fixes, improvements)&lt;/li&gt;
&lt;li&gt;Dependency bumps collapsed or omitted&lt;/li&gt;
&lt;li&gt;User-facing language instead of developer-speak&lt;/li&gt;
&lt;li&gt;Context about &lt;em&gt;what&lt;/em&gt; the change means, not just that it happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to automate it with GitHub webhooks
&lt;/h2&gt;

&lt;p&gt;GitHub fires a &lt;code&gt;release&lt;/code&gt; webhook event every time you publish a release. You can subscribe to this and trigger a pipeline that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches the release's associated commits and merged PRs&lt;/li&gt;
&lt;li&gt;Categorizes them by type (feat/fix/chore in conventional commits, or by PR labels)&lt;/li&gt;
&lt;li&gt;Generates human-readable descriptions using an LLM&lt;/li&gt;
&lt;li&gt;Posts the result to your changelog&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a minimal Node.js webhook handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleGithubRelease&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCommitsSince&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;categorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;categorizeCommits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Generate a user-facing changelog for version &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tag_name&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; with these commits: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works, but there's a lot of boilerplate: webhook verification, GitHub API auth, pagination, edge cases (merge commits, bot commits), storage, hosting the changelog page, RSS feeds...&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built: Changenotes
&lt;/h2&gt;

&lt;p&gt;We were building this exact pipeline internally and decided to package it as a service: &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect your GitHub repo (OAuth, takes 10 seconds)&lt;/li&gt;
&lt;li&gt;Push a release as you normally would&lt;/li&gt;
&lt;li&gt;Changenotes generates a categorized, AI-written changelog entry automatically&lt;/li&gt;
&lt;li&gt;Review the draft, edit if needed, publish to your hosted changelog page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated output handles the hard parts automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dependency bumps are collapsed or omitted&lt;/li&gt;
&lt;li&gt;Bot commits (Dependabot, Renovate) are filtered&lt;/li&gt;
&lt;li&gt;Conventional commits are parsed, but non-conventional repos work too&lt;/li&gt;
&lt;li&gt;Everything is editable before publishing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each project gets a public changelog page at changenotes.app/your-org/your-repo with an RSS feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventional commits help a lot
&lt;/h2&gt;

&lt;p&gt;If your team uses &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;Conventional Commits&lt;/a&gt;, changelog generation becomes much more reliable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;feat(auth): add OAuth2 login with GitHub
fix(api): handle rate limit errors gracefully
chore(deps): update dependencies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type prefix gives the LLM strong signal for categorization. Even if you're not using conventional commits today, it's worth adopting incrementally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;After setting this up, changelogs stop being a chore. Your users get clear, consistent release notes. Your team never has to argue about whether something is worth documenting.&lt;/p&gt;

&lt;p&gt;If you found this useful, &lt;a href="https://changenotes.app" rel="noopener noreferrer"&gt;Changenotes&lt;/a&gt; is what we built to solve this. $29/mo per workspace, 14-day free trial, no credit card required. Happy to answer questions in the comments.&lt;/p&gt;

</description>
      <category>changelog</category>
    </item>
  </channel>
</rss>
