Fork changelogs: release notes for a project that used to be someone else’s

Every fork begins with an argument: the project you depend on has gone somewhere you can’t follow — a license change, an abandoned repo, a direction you disagree with — and someone decides the code deserves a different future. What happens next is mostly a communication problem, and the fork’s changelog is where it plays out. A fork’s release notes carry a burden no ordinary project’s do: they must prove the fork is alive, prove it’s trustworthy, and account — release after release — for its relationship to the project it left. OpenTofu, Valkey, MariaDB, and io.js all faced the same three readers: the potential switcher asking “can I move to this without breaking anything, and will it still exist next year?”, the current user tracking how far the fork has drifted from upstream, and the skeptic checking the commit dates before recommending it. Write for those three and the rest of this guide follows.

The fork point is an entry, not a rebrand

The single most important entry a fork will ever publish is its first one. It has two jobs: mark the boundary, and keep the inherited history honest. Everything before the fork point is someone else’s work — your users depend on that history (they’ll search it for “when was this fixed?”), so keep it, dated and intact, with upstream’s name on it. The fork-point entry itself sits on top and states plainly: what this project forked from and at what version, why (link the fuller announcement — this permalink will be cited in every discussion thread for years), what the compatibility promise is on day one, and what happens next. Then new history accumulates above the line, yours.

The failure mode is the find-and-replace changelog: upstream’s entire history with the old name swapped for the new one, as if the fork had written a decade of releases it didn’t write. It claims credit the project hasn’t earned and destroys the one thing inherited history is for — provenance. Anyone who diffs the two changelogs (and in a contested fork, someone will) now has evidence you’ll rewrite the record when it flatters you. That is a fatal first impression for a project whose core sales pitch is trustworthiness. If you’re also renaming things users see, the rename guide covers that half of the problem; this one is about the ledger.

Version numbers: pick a lineage strategy and say it out loud

A fork’s version number answers a question ordinary semver never faces: relative to whose history? There are three workable answers, and real forks have used each:

  • Continue the line. OpenTofu’s first release was 1.6.0, picking up where the Terraform 1.5.x series it forked from left off — the number itself says “drop-in successor.” MariaDB did the same, shipping 5.1 through 5.5 in step with MySQL. The benefit is instant legibility; the cost is that your numbering is forever entangled with a project you no longer control.
  • Match, then diverge deliberately. Valkey’s first release was 7.2.4 — the exact Redis version it forked from, a compatibility statement in four characters — and its first real divergence shipped as 8.0. MariaDB eventually jumped to 10.0 when tracking MySQL’s numbers had become more misleading than helpful. The jump is fine; the rule is that it’s an announced decision, not drift.
  • Restart and mean it. io.js forked from Node.js 0.x and shipped as 1.0.0 — the new number was the message: this fork exists to do semver and ship faster. Restarting is honest when the fork’s whole point is a different release philosophy. It’s a trap when it just erases the map back to upstream.

Whichever you choose, two obligations follow. Publish the mapping — a table or a sentence per release that says which upstream version this release corresponds to or last synced with — and keep stating it for as long as migrating users exist. And when the strategy changes (Valkey’s 8.0 moment, MariaDB’s 10.0 moment), the changelog entry for that release explains the change of scheme itself, or you’ve spent your numbering credibility on a surprise.

Three kinds of entries only a fork writes

On top of the ordinary stream of features and fixes, a fork’s changelog carries three entry types with no equivalent upstream — and the third is the one most forks miss:

  • Divergence entries — the changes that are the reason the fork exists. New features upstream refused, removals of the thing you forked to escape, license or governance changes. These are your loudest entries; write them with the care of a launch announcement, because each one is a small argument for switching.
  • Ported entries — upstream shipped something good and you brought it over. Credit it plainly (“ported from upstream 2.14, thanks to the original authors”) and link upstream’s notes rather than pasting them. Crediting upstream costs nothing and signals confidence; forks that scrub upstream’s name from ported work look insecure, and maintainers notice.
  • Declined entries — upstream shipped something and you are deliberately not taking it. This is the highest-trust entry a fork can print, because it answers the question every fork-watcher silently asks: is the gap between you and upstream a decision or a backlog? “Not porting the new telemetry subsystem; it conflicts with why this fork exists” is a position. Silence about the same gap reads as falling behind, even when it isn’t.

The upstream-sync entry

Most forks track a living upstream, so the routine entry of fork life is the sync: you merged upstream’s latest and now owe your readers an accounting. A good sync entry answers three questions in a few lines each — what came in (headline items, link to upstream’s notes for the rest), what conflicted with your divergences and how you resolved it, and what you skipped, with the one-clause reason that turns a gap into a decision. A skeleton:

## Synced with upstream 2.15 — 2026-07-20
Merged upstream’s 2.15 release (their notes: <link>).

Included: the new bulk-import API, ~40 fixes.
Adapted: their config loader rewrite — reworked to keep our
  TOML support, behavior otherwise identical.
Skipped: the new license-check phone-home (see our fork
  charter); the experimental UI (may revisit).

Compatibility: tracks upstream 2.15 APIs.

Two cautions. Don’t paste upstream’s full notes — that’s their work, and your readers need your delta, not a mirror (the same pointer-not-archive rule as chat announcements). And date the sync by when you shipped it, not when upstream did — the lag between those dates is real information, and hiding it just moves the discovery to someone’s CVE scanner.

Security: time-to-port is your reputation

The sharpest question anyone asks about a fork: when upstream ships a security fix, how long until the fork has it? Your changelog is where that reputation is built, one dated entry at a time. When upstream publishes an advisory, you owe an entry in one of exactly three shapes: “affected — fixed in this release” (port the fix, say which upstream advisory it corresponds to, follow the advisory-vs-changelog discipline for how much detail to publish when); “not affected — here’s why” (you removed or replaced the vulnerable subsystem — say so, because your users’ scanners flag the upstream CVE against you either way and this entry is what support links to); or “assessing” published quickly and edited when resolved, per the two-pass hotfix pattern. The one unacceptable shape is silence — a fork that ports CVE fixes late and quietly is accumulating the exact evidence that kills fork adoption, and unlike most reputation problems, this one has a public timestamped record.

The changelog is the proof of life

Before anyone migrates to a fork, they perform the same ritual: open the repo, check the last release date, skim the recent changelog. A fork is a promise that maintenance will continue under new management, and the dated release history is the only evidence that ages well — every entry is a timestamped, uneditable-in-spirit “still here.” SQLite’s unbroken two-decade changelog works as a buying signal; for a fork the effect is doubled, because doubt about survival is the default. This cuts both ways: a cadence that slips reads as dying even when development is healthy. If work is happening but releases aren’t, say so in the stream — a short “what’s cooking” entry during a long gap costs a paragraph and answers the question every watcher is actually asking.

Write for the migrator

For a fork, migrating users are the growth channel, and they need two documents with a clean division of labor. The migration guide — “switching from upstream” — lives in your docs: the one-line swap if you’re a drop-in replacement, the config differences table, the compatibility statement, kept current as a snapshot (docs, not changelog — it describes now, not history). The changelog links to it from the fork-point entry and from any release that changes the answer. A “differences from upstream” page follows the same rule: it’s a living snapshot, refreshed each sync, with the changelog as the dated record of how each difference arose. Migrators will also read your inherited history to find the last version where fork and upstream were identical — one more reason the imported history keeps its real dates (the backfill rules apply: never invent dates you don’t have).

Anti-patterns

  • The find-and-replace history. Upstream’s decade of releases with your name swapped in. Provenance destroyed, credit claimed, trust forfeited on day one.
  • The unexplained gap. Upstream has shipped four releases; your changelog doesn’t mention them. Readers can’t tell a decision from a backlog, so they assume backlog.
  • The manifesto stream. Every entry is about the rival — “unlike upstream, we respect users.” One fork-point entry gets to be about why you left; after that, the changelog is about your software or it isn’t worth reading.
  • Numbering without a map. Restarting at 1.0 with no statement of which upstream version you froze from. Every migrator now does archaeology you could have done once.
  • The silent CVE port. Slipping upstream’s security fix into a routine release unlabeled. You did the work and kept the doubt.
  • Divergence by accident. Skipping upstream changes because syncing got hard, while the changelog keeps implying you track upstream closely. When the gap surfaces — and a user’s failing workload will surface it — the record shows you knew.

Running a fork’s changelog on Wakelog

The fork lifecycle maps onto Wakelog directly. Start by importing the inherited history — paste upstream’s CHANGELOG.md or point the importer at the old repo’s GitHub Releases — so the pre-fork record survives with its real dates, then post the fork-point entry with the announcement tag and pin its permalink in your README and announcement threads. Sync entries post from the same CI job that lands the merge (one curl), ported CVE fixes carry the security tag so “?tag=security” is your public time-to-port record, and the RSS feed plus the README badge — “shipped 3d ago” — answer the is-it-alive ritual before anyone has to ask. The honest caveat: no tool decides what to port, adapt, or decline — the divergence bookkeeping that makes a fork’s changelog worth trusting is maintainer judgment, and it stays a writing job.

Give your fork a changelog — free   Next: CHANGELOG.md vs GitHub Releases →

Related guides

Last updated 2026-07-29 · All guides