A GitHub Action that validates a CHANGELOG.md file conforms to the Keep a Changelog format.
- The title line is exactly
# Changelog. - No manual table of contents (anchor links) before the first
## [version]section. ###subsections use only the approved Keep a Changelog types (Added,Changed,Deprecated,Removed,Fixed,Security), with no duplicates within a single section.- Every
## [version]section has a matching[version]: https://...reference link at the bottom of the file. - Every released
## [version]section has a trailing release date in the format- YYYY-MM-DD(e.g.## [1.2.0] - 2026-04-02);[Unreleased]must not have one. - The
[Unreleased]reference link compares the most recent version toHEAD(e.g..../compare/v1.2.0...HEAD).
name: Validate Changelog
on:
pull_request:
paths:
- CHANGELOG.md
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ConfiguredThings/validate-changelog-action@v1No install required — the action is a single Node script that reads its input the same way GitHub Actions does (an INPUT_<NAME> environment variable per input). Node 24 is required to match action.yml.
Validate a CHANGELOG.md in the current directory:
node validate/index.jsValidate a changelog at a different path:
env "INPUT_CHANGELOG-PATH=path/to/CHANGELOG.md" node validate/index.jsThe script exits with a non-zero status and prints ::error ... lines for each violation, or prints a ✓ ... compliant message on success — matching what you'd see in a workflow run.
| Name | Description | Required | Default |
|---|---|---|---|
changelog-path |
Path to the changelog file | No | CHANGELOG.md |
- uses: ConfiguredThings/validate-changelog-action@v1
with:
changelog-path: docs/CHANGELOG.mdPushing a tag matching v* triggers .github/workflows/release.yml, which:
- Runs this action against
CHANGELOG.mdto make sure it's still compliant. - Installs a pinned release of
mdq(ajq-for-Markdown CLI) and uses it, viascripts/extract-changelog-section.js, to extract the## [<tag>]section body — matched against the heading's actual parsed text, not a line-scan regex (accepts the tag with or without a leadingv, e.g. tagv2matches a## [v2]or## [2]heading). - Fails the workflow if no matching, non-empty section is found — the release notes must come from the changelog, so a tag with no changelog entry can't ship.
- Creates the GitHub release for that tag using the extracted section as the release notes.
Running the extraction script locally requires mdq on your PATH (brew install mdq, or download a binary from its releases page):
node scripts/extract-changelog-section.js v2