Release Notes and Changelogs from Git
A product's changelog is one of the most-read pages on its documentation site, and one of the least loved by the people who write it. Someone copies merged pull request titles into a Markdown file the day before a release, half of them are meaningless to users ("refactor auth middleware", "bump deps"), the links to new documentation are missing, and the page ends up as a wall of text that nobody can subscribe to or search by version.
This guide builds a changelog for a static documentation site from Git: structured input from changesets written in pull requests, a draft generated at release time and edited by a writer, one page per release with a stable URL, links checked against the docs build, and a feed. It is part of Content Workflows for Documentation Teams. The product ships monthly releases with 40–80 merged pull requests each, and the docs site is built with Astro.
Prerequisites
- A product repository with pull requests, and a documentation site built by a static generator.
- Agreement on release cadence and version numbering.
- A place where release notes will live: in the docs repository, or generated into it from the product repository.
Step 1: Structured Input at Pull Request Time
The best time to describe a change for users is when it is made, by the person making it. Changesets are small Markdown files added in the pull request:
<!-- .changeset/quiet-owls-sing.md -->
---
type: feature
area: search
docs: /guide/search/filters/
---
Search results can now be filtered by product version. The filter remembers your last choice.
A pull request template prompts for one, and a CI check requires a changeset for pull requests labelled user-facing — refactors, tests and dependency updates are exempt with an internal label. The body is written for users, in one or two sentences, and docs points at the page that explains the change.
Teams that already use conventional commits (feat(search): filter by version) can generate the same structure from history with tools such as git-cliff; the text is usually weaker, because commit messages are written for reviewers, but it is a reasonable start.
Step 2: Generate a Draft at Release Time
When the release branch is cut, a script collects the changesets merged since the last release tag, groups them, and writes a draft release page into the docs repository as a pull request:
// scripts/draft-release.mjs
const since = execSync('git describe --tags --abbrev=0').toString().trim();
const files = execSync(`git diff --name-only ${since}..HEAD -- .changeset/`).toString().split('\n').filter(Boolean);
const entries = files.map((f) => matter(readFileSync(f, 'utf8'))).map(({ data, content }) => ({ ...data, text: content.trim() }));
const order = ['breaking', 'feature', 'improvement', 'fix'];
const groups = order.map((type) => ({ type, items: entries.filter((e) => e.type === type) })).filter((g) => g.items.length);
const body = groups.map((g) => `## ${titles[g.type]}\n\n` + g.items
.sort((a, b) => a.area.localeCompare(b.area))
.map((e) => `- **${areaNames[e.area]}:** ${e.text}${e.docs ? ` See [the guide](${e.docs}).` : ''}`).join('\n')).join('\n\n');
writeFileSync(`src/content/releases/${version}.md`, `---\nversion: ${version}\ndate: ${today}\nsummary: ''\n---\n\n${body}\n`);
The draft for a typical release had 34 entries in four groups. A writer then edits it: adds a two-sentence summary at the top, merges related entries, rewrites anything unclear, and highlights the two or three changes most readers care about. Editing a structured draft took about 40 minutes per release, against roughly three hours of assembling notes from pull request titles.
Step 3: One Page per Release
Each release is a content collection entry with frontmatter (version, date, summary, optional breaking: true), rendered at /changelog/4-12/. An index page lists releases newest first with their summaries, and a filter by area uses the area values in each entry. Stable per-release URLs matter: support replies, forum posts and upgrade guides link to "the 4.12 release notes", and a single long page with anchors breaks those links when it is split or trimmed.
Breaking changes get extra treatment: a callout at the top of the release page, a link to the upgrade guide, and inclusion in a separate "breaking changes by version" page that people upgrading across several versions read first.
Step 4: Check Links and Publish a Feed
Every docs link in a release entry must point to a page that exists in the build. The site's link checker covers this, but a specific check with a clear message helps writers: "4.12: link /guide/search/filters/ does not exist — has the guide been merged?" Release notes and their documentation are often merged the same day, and the check catches the order-of-merge mistakes.
An Atom feed with one entry per release, full content and stable IDs lets readers subscribe, and lets teams pipe releases into Slack or Discord with standard RSS integrations. The feed follows the practices in RSS and Atom Feeds for Static Blogs. Within two months, the feed had 1,400 subscribers, and the support team's internal channel received releases automatically.
Step 5: Publish on Release Day
The release page must go live when the product release does, not when someone remembers. The docs pull request containing the release page carries a publish-on-release label; a workflow in the product repository, triggered by publishing the GitHub release, merges it via the API (after its checks pass) and triggers a production docs build. For releases scheduled at a set time, the same approach as Scheduling Content Publication with Cron-Triggered Builds works: the entry has a future date and the build excludes it until then.
Writing Notes Readers Use
The structure only helps if the entries are written for the people reading them. The style guide for changesets fits on one screen: lead with what the reader can now do or what changed for them, not with what the code does ("Search results can now be filtered by version", not "Add version facet to search index"); name the feature as it appears in the product; mention breaking changes explicitly with what the reader must do; and keep each entry to one or two sentences, linking to the guide for detail. Vale enforces a few of these mechanically — banned words like "refactor" and "bump" in changesets, sentence length — as in Editorial Checks with Vale in CI. Reader behaviour confirmed the approach: on release pages, 38% of sessions clicked through to a linked guide, which told the team which changes readers cared about most.
Measured Impact
| Measure | Hand-assembled notes | Changesets + generated draft |
|---|---|---|
| Writer time per release | ~3 h | ~40 min |
| Entries per release linking to docs | 9 | 31 |
| Release notes published on release day | 4 of 6 releases | 6 of 6 |
| Internal-only entries in public notes | 11 per release | 0 |
| Changelog feed subscribers after two months | — | 1,400 |
| Support tickets asking "what changed in X" per month | 38 | 17 |
Pitfalls & Rollback
- Publishing commit messages as release notes. Keep a writer in the loop; drafts are a starting point.
- One giant changelog page. Per-release pages give stable links and make the index scannable.
- Changesets that are optional. Without a CI check, they are forgotten exactly on the busiest pull requests.
- Docs links to pages that are not merged yet. Check links in the release entry against the build.
- Rollback: release pages are ordinary content; reverting the merge unpublishes one, and the draft script can be dropped without affecting existing pages.
Conclusion
Good release notes come from capturing the user-facing description when a change is made, not reconstructing it from history on release day. Changesets in pull requests, a generated draft grouped by type and area, a writer's edit, per-release pages with checked docs links, and a feed turned a dreaded monthly chore into a forty-minute task — and gave readers something they actually subscribe to.
FAQ
Should a changelog be generated from commit messages?
Partly. Commit messages written for developers are rarely good release notes for users. A useful pattern generates a draft from structured commits or changesets, grouped by type and area, and lets a writer edit it into user-facing notes before publishing.
What is the difference between conventional commits and changesets?
Conventional commits encode the change type in each commit message, such as feat or fix, and tools derive the changelog from history. Changesets are small Markdown files added in pull requests that describe the change for users, which tools collect at release time. Changesets usually produce better user-facing text.
How should a changelog be structured on a documentation site?
One page per release, with a stable URL, grouped into new features, improvements, fixes and breaking changes, linking to the relevant documentation pages, plus an index page and an RSS or Atom feed that readers and integrations can subscribe to.
How do I link release notes to documentation changes?
Include the documentation URL in the changeset or the pull request template, and have the generator check that each linked page exists in the build. Readers then go directly from a release note to the updated guide.
Related
- Parent: Content Workflows for Documentation Teams — the docs-as-code workflow.
- Docs-as-Code Review Workflow for Writers — reviewing the release pull request.
- RSS and Atom Feeds for Static Blogs — building the changelog feed.
- Versioned Documentation with Docusaurus — docs per version alongside notes per version.
- Tracking Deploy Markers in Analytics — connecting releases to metrics.
Filed underHow-tos