Skip to content

Accepted changelog formats

Parse is a line scanner, not a markdown parser. It recognises three kinds of line — release headings, section headings and bullets — and ignores everything else. This is the exact set it recognises.

Release headings

Recognised at heading level 1 or 2 only. Two shapes:

Shape Example Version recorded
Link-wrapped ## [v1.2.3](https://…/v1.2.3) (2026-01-02) v1.2.3 — trailing text is dropped
Bare # v1.2.3 v1.2.3
Bare with trailing text # v1.2.3 (2026-01-02) v1.2.3 (2026-01-02)the whole line

The version must look like 1.2.3 or v1.2.3 — three dot-separated numbers, with an optional v — or be the literal word Unreleased. A two-part version (# v1.2) is not recognised as a release heading, and a version at heading level 3 (### v1.2.3) is read as a section heading instead, which silently resets the category to Other.

A document title such as # Changelog matches nothing and is ignored.

Section headings and the categories they map to

Recognised at heading level 2 or 3. Matching is case-insensitive, and leading non-alphanumeric characters are stripped first, so release-please's ### ⚠ BREAKING CHANGES maps correctly.

Heading text Category
Breaking Changes CategoryBreaking
Features or feat CategoryFeature
Bug Fixes or fix CategoryFix
Performance Improvements or perf CategoryPerformance
anything else CategoryOther

There is no partial matching. Performance on its own is not Performance Improvements, and the keep-a-changelog headings — Added, Changed, Deprecated, Removed, Fixed, Security — are all unrecognised and land in Other. Note that Fixed is not fix.

Entries appearing before any section heading in a release take CategoryOther.

Entry lines

A bullet is a line beginning with * or - followed by whitespace. + bullets and numbered lists are not entries. Lines are trimmed before matching, so an indented sub-bullet is read as a top-level entry of the current release.

Written Scope Description Category
* plain text empty plain text current section's
* **cli:** add a flag cli add a flag current section's
- **cli**: add a flag cli add a flag current section's
- **BREAKING**: **api**: drop it api drop it CategoryBreaking
- BREAKING CHANGE: drop it empty drop it CategoryBreaking
- BREAKING-CHANGE: drop it empty drop it CategoryBreaking

The inline **BREAKING**: marker is matched literally — capitals, colon, one space — and overrides whatever the surrounding section said. A scope may not contain : or *, so **a:b:** is left in the description rather than becoming a scope.

Entry.Raw holds the whole original line, bullet character included, before any of this was stripped.

What is not interpreted

Everything else is skipped: prose, tables, HTML, link-reference definitions, and the [Compare to previous version](…) lines releaser-pleaser writes between releases.

Fenced code blocks are not treated specially. A line inside a fence that starts with * becomes an entry, and a # v1.0.0 inside a fence starts a release. A changelog whose entries quote shell output will parse into entries you did not intend.

Trailing commit links are not stripped. Parsing this repository's own changelog leaves the link in the text:

Description: "detect release tags on merge commits via ancestry-based bucketing
              ([b064cee](https://gitlab.com/…/commit/b064cee…))"

Ordering, FromVersion and ToVersion

Parse reverses the releases it finds, so Changelog.Releases runs oldest to newest on the assumption that the document runs newest first. FromVersion is then the first release of the reversed slice and ToVersion the last.

Feed it an oldest-first document and both come out backwards — the parser has no way to know, because it does not sort by version. Unreleased is treated as an ordinary version string, so a newest-first document with an unreleased section yields ToVersion == "Unreleased".

Reading from a release archive

ParseFromArchive(r io.Reader) expects a gzip-compressed tar stream. Any other container — a plain tar, a zip, a bare markdown file — fails at the gzip header.

  • The first regular file whose base name is CHANGELOG.md is used, matched case-insensitively (changelog.md counts) at any depth inside the archive.
  • Directories, symlinks and other non-regular entries are skipped.
  • The file is read up to 10 MB and then truncated silently. A larger changelog parses as though the first 10 MB were the whole file — no error, no warning, and the releases past the cut are simply absent.
  • If no changelog is found, the return is (nil, nil) — see errors and return values.