MeDiVa
DocsRecipes

Enforce CHANGELOG entries

Require release notes to contain real Added, Changed, and Fixed entries before a release lands.

The jump log can be brief, but it cannot say "TBD" where the coordinates should be.

Put the contract beside the file it guards: CHANGELOG.mdv.md next to CHANGELOG.md. With that convention, local checks and CI can use the zero-config scan or validate the changelog explicitly.

The contract

CHANGELOG.mdv.md
<!-- mdv: block required heading.level=1 heading.pattern=/^Changelog$/ -->
# Changelog
<!-- mdv: endblock -->

<!-- mdv: block required heading.level=2 heading.pattern=/^[0-9]+\.[0-9]+\.[0-9]+\s-\s[0-9]{4}-[0-9]{2}-[0-9]{2}$/ -->
## 1.2.3 - 2026-06-28
<!-- mdv: endblock -->

<!-- mdv: block required minWords=3 noPlaceholder -->
### Added

- New user-visible capability.
<!-- mdv: endblock -->

<!-- mdv: block required minWords=3 noPlaceholder -->
### Changed

- Changed user-visible behavior.
<!-- mdv: endblock -->

<!-- mdv: block required minWords=3 noPlaceholder -->
### Fixed

- Fixed user-visible bug.
<!-- mdv: endblock -->

This requires a # Changelog title, one release heading shaped like 1.2.3 - 2026-06-28, then real Added, Changed, and Fixed sections. It is the heading.pattern regex that's enforced, not the literal sample date, so any conforming release heading passes. noPlaceholder rejects entries like TBD, TODO, and untouched template text.

The GitHub Action

.github/workflows/changelog.yml
on:
  pull_request:
    paths:
      - CHANGELOG.md
      - CHANGELOG.mdv.md
jobs:
  changelog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install -D mediva
      - run: npx mediva check CHANGELOG.md --schema CHANGELOG.mdv.md

You can also run the sibling scan from the repo root:

npx mediva check

With no file arguments, mediva validates every X.md that has a sibling X.mdv.md schema.

Variations

  • If some releases legitimately have no fixed bugs, keep the required ### Fixed heading and write an honest line like No bug fixes this release. rather than deleting it (the body must still clear minWords=3).
  • For a single release note file, keep this same contract beside release.md as release.mdv.md.
  • This contract is written for a single-release file. If you keep a full history in one file, the repeated ### Added / ### Changed / ### Fixed headings emit duplicate-heading warnings, and validation attaches to the last occurrence of each — so in a newest-at-top changelog the rules land on the oldest release, not the newest. To gate the release you actually care about, generate a one-release fragment in CI (e.g. the section above the second ## x.y.z heading) and check that fragment against this contract.

On this page