Changelog linting: automated checks that catch bad release notes

Everything else you ship goes through checks. Code has tests, formatting has a formatter, commit messages have hooks, the OpenAPI spec has a diff gate. The changelog — the one artifact your users actually read — usually ships with none of that. Whoever cut the release pasted something in at 5:50pm, nobody reviewed it, and the result is how you get entries like “Various fixes and improvements” sitting above a breaking change nobody flagged.

You can’t unit-test prose. But most changelog failures aren’t prose failures — they’re structural, and structure is checkable. A missing date, an entry dumped straight from git log, a breaking change mentioned mid-paragraph with no flag: a script can catch every one of those before a human ever needs to exercise judgment. This guide covers what’s worth automating, where the check belongs in your pipeline, and — just as important — what a linter can never tell you.

What a linter can actually catch

The mechanically checkable failure modes, and why each one matters:

  • Missing or ambiguous dates. An undated entry can’t answer “was this before or after the bug appeared?” — the single most common reason anyone opens an old changelog. Slash dates (03/04/2025) are worse than none: half your readers will parse them backwards. ISO 8601 or spelled-out months are the only unambiguous options.
  • Wrong order. Oldest-first means every visitor scrolls past your 2019 launch post to find last week’s fix. Scattered order means your publishing process is manual and error-prone. Both are one comparison per pair of dates to detect.
  • Future dates. Almost always a typo — and a typo that makes readers doubt every other date on the page.
  • Empty version-only entries. A heading like 1.4.2 with no body tells readers a release happened and nothing else. It’s the changelog equivalent of a test that asserts true.
  • Commit dumps. Bullets full of SHAs and fix(auth): refactor token TTL mean the changelog was generated and never edited — the from-git guide covers why generated output is a draft, not a release note. Heuristics (share of bullets matching SHA or conventional-commit shapes) catch this reliably.
  • Vague titles. “Bug fixes and improvements”, “Minor updates”, “Misc changes” — a banlist finds them in milliseconds, and every hit is an entry that tells readers nothing. The craft guide explains what should be there instead: the symptom, named from the reader’s side.
  • Breaking changes mentioned but not flagged. Text that says “no longer”, “removed”, or “renamed” inside an entry that carries no breaking marker is the most expensive lint hit there is — it’s the difference between a planned migration and a 2am page. See announcing breaking changes.
  • Vague security entries. “Security improvements” with no affected versions and no severity is indistinguishable from a cover-up — and attackers diff releases anyway. The security advisory guide covers what a ship-day entry can safely say; a linter can at least catch that the word “security” appeared with nothing actionable around it.
  • “See commits” pointers. An entry whose body is a link to the compare view delegates the writing to the reader.
  • Duplicate titles. Two entries named “Bug fixes” aren’t duplicates by accident — they’re evidence the titles carry no information.

None of these require understanding your product. That’s exactly why they belong in a script: they’re the checklist half of review, and checklists are what machines are for.

What a linter can never catch

A linter reads the changelog; it can’t read your product, your reader, or your intentions. It cannot tell whether:

  • the title names the reader’s symptom or your internal cause (“Fixed session handling” vs. “Fixed: you got signed out at random”);
  • the important change leads the entry, or is buried under four cosmetic bullets;
  • the entry is honest — an undocumented nerf, a quietly removed feature, or a “performance improvements” line hiding a regression will all lint clean;
  • the tone fits your product, or reads like a press release.

So treat the grade as a floor, not a ceiling. A failing grade reliably means the changelog is bad; a passing grade means only that it isn’t bad in the ways a machine can see. The point of automating the floor is to spend human review on the ceiling — the judgment calls above are exactly what your one round of human reading should look for, now that it doesn’t have to count dates.

Three places to run it (they check different things)

1. The pull-request gate — “does this change have an entry?” Presence checks (changelog-enforcer is the standard GitHub Action) fail a PR that doesn’t touch the changelog. This is a different question from quality — it catches the change that ships silently, which no after-the-fact linter can. Two rules make it livable: an explicit skip label (dependency bumps, typo fixes, CI changes don’t warrant entries — force-writing them produces noise, not coverage), and honesty that the check is crude: it verifies a file was edited, not that the edit says anything. A one-word entry sails through. Fragment-based tools — towncrier, Changesets — solve presence by construction: each PR adds its own fragment file, and the release step assembles them, which also kills the merge conflicts a single shared file breeds.

2. The release gate — “is the assembled changelog sound?” This is where the structural checks from the list above belong: run the linter against the full file (or your Releases page) as part of cutting the release, and fail the pipeline below a threshold. Because it runs on the whole artifact, it catches what the PR gate can’t — ordering, date coverage, the unflagged breaking change that was fine in isolation but wrong in context.

3. The scheduled audit — “did we drift?” A weekly cron that lints and posts the grade to your team channel catches the slow failures: the changelog that quietly stopped getting dates when a new tool was adopted, the format change that broke your own feed parser. It’s also the only gate that watches changelogs nobody releases from CI — the marketing-owned page, the vendor changelogs you depend on.

Failure semantics: when to block, when to warn

  • Block on structural breakage: unparseable file, future date, oldest-first order, empty entries. These are objectively wrong and trivially fixable — exactly what CI blocking is for.
  • Warn on style: vague-title hits, commit-dump heuristics, missing markers. Heuristics have false positives, and a warning a human glances at costs nothing; a false-positive block at release time costs trust in the whole gate.
  • Never make an emergency wait on prose. A hotfix with a rough two-line entry beats a polished one that ships an hour later. Give the release gate the same escape hatch you give every other CI check — and audit uses of it.
  • Ratchet, don’t demand perfection. If you’re adopting a changelog on an existing product — or inheriting years of imported history — set the threshold at your current grade and raise it as you fix entries. A gate that fails on day one for entries written in 2021 gets deleted, not obeyed. Old entries are history; lint pressure belongs on new ones.

The tool landscape

Four families, checking four different things:

  • Format validators — parsers for a specific convention, like the keep-a-changelog parser, which will object when your file breaks the format’s rules. Precise, but only about syntax, and only for that format.
  • Presence enforcers — changelog-enforcer and friends, covered above. Catch silence, not quality.
  • Fragment assemblers — towncrier, Changesets: make presence structural and assembly mechanical. The assembled output still deserves a content pass; fragments written by fifty PR authors don’t converge on one voice by themselves.
  • Content linters — checks like the list at the top of this guide: format-agnostic, opinionated about what entries say rather than how they’re delimited. This is the family our own free linter belongs to.

They compose. A presence check on PRs, a content lint at release, and a weekly audit overlap barely at all — each catches failures the others are blind to.

Anti-patterns

  • Linting theater. Renaming “Various improvements” to “Assorted refinements” to dodge the banlist. The check exists to prompt a real title; gaming it means the team sees lint as an obstacle — which usually means the rules were imposed without the why. Every rule you enforce should link to a reason a human can read.
  • Blocking releases on prose style. The strictness that’s right for structure is wrong for wording. Blocked releases breed resentment; resentment breeds escape-hatch abuse; and then the gate catches nothing.
  • Green-means-good. Passing lint gets waved through with no human read at all. The linter freed reviewers to judge content; if nobody judges content, you’ve automated the wrong half.
  • Presence without content. The enforcer proves the file was touched, the team learns “add any line to pass”, and the changelog fills with entries like “Update”. Pair presence checks with content checks or don’t bother.
  • Lint-driven archaeology. Spending a sprint rewriting 2019 entries to reach grade A. Old entries earn their keep by existing; the archive matters, but polish belongs on what ships next.

Where Wakelog fits

Wakelog runs a free changelog linter — paste a CHANGELOG.md or point it at a GitHub, GitLab or Codeberg repo’s Releases and get a graded report. It’s a content linter in the taxonomy above: twelve checks, each finding linked to the guide that explains the fix, no signup, nothing saved. The same engine is a no-auth JSON API, so the CI story is one line — wakelog lint --min-grade B exits non-zero when the grade slips (that’s the release gate and the weekly audit from this guide; details in the docs). Lint by URL and you can put a live grade badge in your README, re-checked daily. Honest caveats: it grades the changelog as a whole, so it won’t tell you whether this PR deserved an entry — pair it with a presence check for that — and like every linter in this guide, it can’t tell whether your entries are true. That part stays yours.

Lint your changelog — free, no signup   Next: wiring the changelog into CI →

Related guides

Last updated 2026-07-31 · All guides