A changelog format handles parsing and rendering a CHANGELOG.<ext> file in a specific markup language. End-user documentation: Changelog command. Built-ins are markdown (default), asciidoc, textile, restructuredtext.
Architectural context: Architecture § Extension points.
- "Support
<markup>changelogs." - A user wants
cz changelogto emit something other than Markdown. - An incremental-changelog use case fails because the user's existing
CHANGELOGfile is not Markdown.
commitizen/changelog_formats/__init__.py—ChangelogFormatProtocol, entry-point groupcommitizen.changelog_format,KNOWN_CHANGELOG_FORMATSregistry,_guess_changelog_formatextension-based fallback.commitizen/changelog_formats/base.py:BaseFormat— abstract implementation; you only need to overrideparse_version_from_titleandparse_title_level.- A close-match existing format:
- Heading-prefix-based:
commitizen/changelog_formats/markdown.py(uses#,##prefixes). - Underline-based:
commitizen/changelog_formats/restructuredtext.py(uses===,---lines).
- Heading-prefix-based:
commitizen/templates/— Jinja2 templates namedCHANGELOG.<ext>.j2control rendering.tests/test_changelog_format_<name>.py— every format has parity tests; copy the closest one.
-
Create the format module at
commitizen/changelog_formats/<name>.py. SubclassBaseFormat. Set the class attributes:extension: ClassVar[str]— primary file extension (no dot).alternative_extensions: ClassVar[set[str]]— other accepted extensions for the same format.
-
Implement two methods:
parse_version_from_title(line: str) -> VersionTag | None— given one line, return aVersionTagif the line is a release heading.parse_title_level(line: str) -> int | None— return the heading level (1, 2, 3, ...) if the line is a heading. The base classBaseFormat.get_metadata_from_filewalks the file once and calls both methods per line.
-
Add the Jinja2 template at
commitizen/templates/CHANGELOG.<ext>.j2. Mirror the structure ofCHANGELOG.md.j2— same blocks, different markup. Make sure the loops overtree,entries, andchange_typematch. -
Register the built-in in
pyproject.tomlunder[project.entry-points."commitizen.changelog_format"]:<name> = "commitizen.changelog_formats.<name>:<Name>" -
Add tests at
tests/test_changelog_format_<name>.py. Copy the closest existing test file and adapt the fixtures. -
Update the cross-format suite
tests/test_changelog_formats.pyif it parametrizes over all formats — add the new one to its lists. -
Update user docs at
docs/commands/changelog.mdanddocs/customization/changelog_template.md— list the new format and show how to opt in viachangelog_format. -
Re-run the install so the entry point registers:
uv sync --frozen --group base --group test --group linters
uv run pytest tests/test_changelog_format_<name>.py tests/test_changelog_formats.py tests/test_changelog.py tests/test_incremental_build.py -n auto
uv run poe lint
uv run poe doc:build # if docs changed
uv run poe all # final pre-pushKNOWN_CHANGELOG_FORMATSis populated at import time from entry points, so you must re-runuv syncafter editingpyproject.tomlbefore tests can see your new format.- Forgetting
alternative_extensions—_guess_changelog_formatuses bothextensionandalternative_extensionswhen the user does not setchangelog_formatexplicitly. If a user hasCHANGELOG.<alt-ext>, your format will not auto-detect without it. - Template encoding — Jinja2 reads templates with the active encoding; keep them ASCII-safe or test with non-UTF-8
encodingsettings. - Heading regex anchoring — match the whole line (
^...$) when the markup is line-anchored (Markdown headings); a substring match will pick up non-heading lines that mentionunreleased. - Snapshot updates — many changelog tests use
pytest-regressions. See the update-snapshots playbook when output intentionally changes.
- The target format requires structured metadata that does not fit the
parse_title_*Protocol (e.g., front-matter in YAML). - The format implies a fundamentally different rendering tree (e.g., one file per release) — that is a bigger change than a format addition.