Open source changelogs: CHANGELOG.md, GitHub Releases, or both?
Open source changelogs have a stranger readership than product changelogs. Almost nobody visits your repo to browse them. Instead they get read at three high-stakes moments: when a dependabot PR bumps your version and someone has 30 seconds to decide whether merging it will break production; when an upgrade already went wrong and someone is hunting for what changed; and when a potential adopter checks whether the project is alive. This guide covers where the changelog should live, how to keep it from becoming a merge-conflict machine, and what those three readers actually need from it.
The dependabot reader is your main reader
Package managers changed who reads changelogs. Most of your users never chose a version β a bot proposed one, and a human glanced at the diff. Dependabot and Renovate embed release notes directly in the bump PR, which means your changelog is read inside someone else's code review, compressed into a scroll-past. Write for that:
- Breaking changes first, loud, every time. A
BREAKINGline at the top of the entry, with the one-line migration ("renameopts.retrytoopts.retries"). If the reader has to open a linked issue to learn what breaks, most won't β they'll either merge blind or pin forever. - Say when there's nothing to fear. "No breaking changes. Safe to bump." is the most merge-encouraging sentence in open source, and almost nobody writes it.
- Name the bugs you fixed by symptom, so the reader who hit one recognizes it: "fixed ETIMEDOUT on keep-alive connections idle > 60s" beats "fixed socket handling".
CHANGELOG.md vs GitHub Releases
Both are common; they have different physics.
- CHANGELOG.md travels with the code: it's in the tarball, in vendored copies, in mirrors, readable offline and greppable with the tools you already have. It survives a platform move. But it only updates when someone edits it, and nobody is notified when it changes.
- GitHub Releases come with distribution: watchers get notified, there's an Atom feed, and β decisive for the dependabot reader β release notes are what the bump PR embeds. But they're platform-locked, invisible in the tarball, and editable history (a release body can be quietly rewritten; a committed file can't without a trace).
The wrong answer is maintaining both by hand β they drift within three releases. The right answer is one canonical source, mechanically mirrored: write the entry once and let the release script paste it into the other surface. Which one is canonical matters less than that only one is.
The merge-conflict problem
A shared CHANGELOG.md with an [Unreleased] section has a famous
failure mode: every PR appends to the same lines, so every PR conflicts with every other PR.
Projects solve it one of three ways:
- Fragment files. Each PR adds its own small file (Changesets for JS monorepos, towncrier in the Python world); the release step compiles fragments into the changelog and deletes them. No shared lines, no conflicts, and the entry is written while the author still remembers what the change does.
- Label-driven drafts. Release Drafter accumulates merged PR titles into a draft release, sorted by label. Zero contributor friction β but PR titles are written to pass review, not to inform users, so budget maintainer editing time before publish.
- Maintainer writes at release time. Fine for small projects with an engaged maintainer; it produces the best prose and the least process. It fails when the release is big enough that nobody remembers everything β which is exactly when the changelog matters most.
Credit is part of the changelog
Contributors get paid in visibility. "Thanks to @name for their first contribution" costs one line and is, for many people, the artifact they link from their profile for years. If a release includes outside contributions, credit them in the entry β by handle, next to the change, not in a bulk list at the bottom. GitHub's auto-generated notes do this mechanically; keep that part even if you rewrite everything else.
Anti-patterns
- "See commit history." That's not a changelog, it's homework. The reader deciding on a dependency bump will not read 300 commits; they'll just not upgrade.
- Publishing the raw commit dump as release notes. Auto-generation is a fine draft (we wrote a whole guide on doing it well), but "chore(deps): bump lodash" Γ 40 tells users nothing.
- The stale file. A CHANGELOG.md whose last entry is two majors old is worse than none β it actively signals abandonment to the adopter doing due diligence. If you've moved to Releases, replace the file's contents with a link.
- Empty release bodies. A tag with no notes embeds as nothing in the bump PR. Even two lines β what changed, whether it breaks β beat the void.
Where Wakelog fits
Wakelog gives an open source project a hosted changelog page with feeds, without changing
where your canonical source lives. Paste a github.com/owner/repo
URL and it imports your published GitHub Releases (or paste your CHANGELOG.md β the
parser handles keep-a-changelog and most real-world variants); point a release webhook at it
and every future release auto-posts. You get a clean public page, RSS + JSON feeds for the
humans who want to follow along without watching the repo, a README badge showing when you
last shipped, and a changelog.md export so the exit is one GET
request.
Host your project's changelog free Preview yours: paste a repo URL β
Related guides
- Release notes for CLI tools and libraries: writing for people who upgrade on purpose
Your readers choose every upgrade, often jumping several versions at once. What counts as the real interface of a CLI or library, and how to write notes that survive the jump. - Migrating your changelog: switching tools without losing your history
The history is the asset; the tool is just the container. Getting entries out, keeping original dates and permalinks alive, moving feed subscribers without losing them β and the exit-door test to run before you commit to the next tool. - Monorepo changelogs: one changelog or many?
Per-package or one big file? The rule that decides it, what Changesets and release-please actually do, and how to keep a human-readable stream on top.
Last updated 2026-07-27 Β· All guides