Skip to content

fix(changelog): clearer error when changelog format cannot be inferred#1973

Open
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:fix/894-better-changelog-format-error
Open

fix(changelog): clearer error when changelog format cannot be inferred#1973
bearomorphism wants to merge 2 commits intocommitizen-tools:masterfrom
bearomorphism:fix/894-better-changelog-format-error

Conversation

@bearomorphism
Copy link
Copy Markdown
Collaborator

@bearomorphism bearomorphism commented May 9, 2026

Description

Closes #894.

Why

Since commitizen 3.12, running cz changelog with a non-standard --file-name such as docs/source/changelog.md.sections (a Sphinx-style sectioned changelog file) fails with the message Unknown changelog format 'None'. The error is cryptic: it exposes an internal None Python value rather than explaining what went wrong or what the user should do.

The failure path is get_changelog_format (commitizen/changelog_formats/__init__.py:64–80). When changelog_format is not set in the config, name is None. The function then calls _guess_changelog_format(filename) (__init__.py:83–98), which matches the filename against known extensions. A filename like changelog.md.sections does not end with .mdendswith requires the final suffix — so inference returns None. At line 78, the single raise path always formats the message as f"Unknown changelog format '{name}'", which prints Unknown changelog format 'None' regardless of whether the problem was a bad explicit setting or a failed inference. The original reporter (@gsemet) filed the issue in October 2023; a second user (@anthonyfinch) hit the same problem shortly after, and triage in #1964 confirmed the bug is still present against master (v4.15.1).

The fix splits the error into two distinct, actionable messages — one for each failure mode — and both messages list the registered format names so users know exactly what to write in changelog_format.

What changed

File Change
commitizen/changelog_formats/__init__.py Split the single raise ChangelogFormatUnknown in get_changelog_format (line 78) into two branches: one for an explicitly-set-but-unknown name, one for inference failure; both append Known formats: <sorted list>
tests/test_changelog_formats.py Strengthened test_get_format_empty_filename_no_setting and test_get_format_unknown to assert the new message content; added test_get_format_unknown_name_lists_known_formats for the explicit-bad-name path

How it works

  • KNOWN_CHANGELOG_FORMATS (commitizen/changelog_formats/__init__.py:58–61) is a dict populated from commitizen.changelog_format entry-points at module import time. Third-party plugins that register their own format are automatically included in the list — the message is always accurate.
  • The new logic builds known = ", ".join(sorted(KNOWN_CHANGELOG_FORMATS)) once, then branches:
    • If name is truthy (the user explicitly set changelog_format to an unrecognised value): "Unknown changelog format '{name}'. Known formats: {known}." — echoes the bad value so it's easy to spot a typo.
    • Otherwise (inference failed): "Cannot infer changelog format from filename '{filename}'. Set the \changelog_format` setting explicitly. Known formats: {known}."` — names the offending filename and points directly to the fix.
  • Why not silently default to Markdown? A silent fallback would mask real misconfiguration: a user who runs a Sphinx .rst workflow and forgets to set changelog_format = "restructuredtext" would get a Markdown-parsed changelog written into their .rst file, corrupting it without any warning. An explicit error that explains what to do is safer.
  • Why list sorted(KNOWN_CHANGELOG_FORMATS) rather than a hard-coded string? The list is derived from entry-points, so it includes third-party plugins and stays accurate if a format is added or removed in future releases.
  • The docstring on get_changelog_format is updated to reference ChangelogFormatUnknown (not the stale FormatUnknown name) and to describe both failure modes.

Backward compatibility

  • The exception type raised is unchanged — ChangelogFormatUnknown in both new branches.
  • Success paths are entirely unmodified; users with valid changelog_format settings or standard file extensions see no change.
  • The error message text changes — intentionally — but nothing in the test suite or documented public API depended on the exact string "Unknown changelog format 'None'".
  • All 19 existing tests in test_changelog_formats.py pass; the two parametrised tests that previously only checked pytest.raises(ChangelogFormatUnknown) now additionally assert message content.

Checklist

Was generative AI tooling used to co-author this PR?

  • Yes (please specify the tool below)

Generated-by: Claude following the guidelines

Code Changes

  • Add test cases to all the changes you introduce
  • Run uv run poe all locally to ensure this change passes linter check and tests
  • Manually test the changes (see "Steps to Test" below)
  • Update the documentation for the changes

Expected Behavior

Scenario Outcome
cz changelog --file-name CHANGELOG.weird (no changelog_format set) Error: Cannot infer changelog format from filename 'CHANGELOG.weird'. Set the \changelog_format` setting explicitly. Known formats: asciidoc, markdown, restructuredtext, textile.`
changelog_format = "definitely-not-a-format" in config Error: Unknown changelog format 'definitely-not-a-format'. Known formats: asciidoc, markdown, restructuredtext, textile.
cz changelog --file-name docs/source/changelog.md.sections (exact reproducer from #894) Same inference-failure message naming changelog.md.sections
changelog_format = "markdown" with any filename Success — no change to happy path

Steps to Test This Pull Request

git fetch fork fix/894-better-changelog-format-error
git checkout fork/fix/894-better-changelog-format-error

# 1. Targeted regression test.
uv run pytest tests/test_changelog_formats.py::test_get_format_unknown_name_lists_known_formats \
              tests/test_changelog_formats.py::test_get_format_unknown \
              tests/test_changelog_formats.py::test_get_format_empty_filename_no_setting -v

# 2. Reproduce the bug (exact command from the issue), then verify the fix.

mkdir /tmp/cz894 && cd /tmp/cz894
git init -b main
git config user.name test && git config user.email test@test.com

cat > cz.toml << 'EOF'
[tool.commitizen]
name = "cz_conventional_commits"
version = "0.1.0"
EOF

git add cz.toml
git commit -m "fix: initial commit"
cz bump --yes

# Before the fix: prints "Unknown changelog format 'None'" (exit 29).
# After the fix: prints a message naming changelog.md.sections and listing known formats.
cz changelog \
  --file-name docs/source/changelog.md.sections \
  --unreleased-version UNRELEASED_VERSION \
  --dry-run 2>&1 || true

# Also exercise the explicit-bad-name path:
CZ_CHANGELOG_FORMAT=definitely-not-a-format \
  cz changelog --dry-run 2>&1 || true
# Should print "Unknown changelog format 'definitely-not-a-format'. Known formats: ..."

Additional Context

This is one of three bugs surfaced by the triage audit in #1964. The fix is intentionally minimal — error messages only, zero behaviour change on success paths. A follow-up could consider defaulting to markdown when inference fails (as suggested in the triage comment), but that is a separate decision with broader implications and belongs in its own PR.

When `changelog_format` was unset and the changelog filename had an
unknown extension (e.g. `CHANGELOG.md.sections`), commitizen raised
`Unknown changelog format 'None'` -- which is confusing because the
user never set anything to `None`.

Improve the two failure paths:

* If the user set `changelog_format` to an unknown value, the error
  echoes that value and lists the registered formats.
* If the format had to be inferred from the filename and the inference
  failed, the error names the offending filename, points to the
  `changelog_format` setting and lists the registered formats.

No behaviour change for the success paths.

Closes commitizen-tools#894

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.24%. Comparing base (4b93a50) to head (931877e).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1973   +/-   ##
=======================================
  Coverage   98.23%   98.24%           
=======================================
  Files          61       61           
  Lines        2779     2785    +6     
=======================================
+ Hits         2730     2736    +6     
  Misses         49       49           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the UX of cz changelog failures by making ChangelogFormatUnknown errors actionable when the changelog format can’t be determined (either because inference from filename fails, or because an explicit changelog_format is invalid), and updates tests to assert the new messaging.

Changes:

  • Split get_changelog_format’s single error into two distinct messages (explicit invalid format vs. inference failure) and include a “Known formats” list.
  • Update/extend tests/test_changelog_formats.py to assert the new message content for inference failures and explicit invalid names.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
commitizen/changelog_formats/__init__.py Adds clearer error branches and appends known format names to the raised error message.
tests/test_changelog_formats.py Strengthens assertions on error messages and adds a regression test for the explicit-invalid-name path.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread commitizen/changelog_formats/__init__.py Outdated
Comment thread tests/test_changelog_formats.py
…lid filename

Refactor get_changelog_format with explicit branching: when changelog_format is set, look it up directly and raise ChangelogFormatUnknown if missing, regardless of whether the filename extension is recognized. Previously the 'or _guess_changelog_format' fallback could silently ignore an invalid explicit configuration when the filename had a known extension.

Add a regression test exercising changelog_format='invalidformat' with filename='CHANGELOG.md'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Encounter "Unknown changelog format 'None'" when generating changelog

2 participants