feat(release): lint and normalise the changelog message interactively
The release flow now sanitises the release message into the changelog house style and validates it before writing: a leading '#' heading becomes a bold line, inline `code` becomes *italic*, and the resulting entry is checked with markdownlint-cli2 (using the target repo's own config, i.e. the same rules it enforces) or a built-in fallback. In an interactive run the editor re-opens with the findings until the entry is clean; with --message it transforms then aborts on any finding. The editor's ignore-marker moved from '#' to ';' so heading lines survive as content. debian/changelog and the RPM %changelog mirror the transformed body, keeping all three changelogs consistent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
44
tests/unit/pkgmgr/actions/release/test_changelog_lint.py
Normal file
44
tests/unit/pkgmgr/actions/release/test_changelog_lint.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from pkgmgr.actions.release.files.changelog_lint import (
|
||||
lint_changelog_entry,
|
||||
transform_changelog_message,
|
||||
)
|
||||
|
||||
|
||||
class TestTransformChangelogMessage(unittest.TestCase):
|
||||
def test_heading_becomes_bold(self) -> None:
|
||||
out = transform_changelog_message("# Title\n\nbody")
|
||||
self.assertIn("**Title**", out)
|
||||
self.assertNotIn("# Title", out)
|
||||
|
||||
def test_multi_hash_heading_becomes_bold(self) -> None:
|
||||
self.assertEqual(
|
||||
transform_changelog_message("### Sub heading"), "**Sub heading**"
|
||||
)
|
||||
|
||||
def test_inline_code_becomes_italic(self) -> None:
|
||||
out = transform_changelog_message("use `pkgmgr release` now")
|
||||
self.assertEqual(out, "use *pkgmgr release* now")
|
||||
self.assertNotIn("`", out)
|
||||
|
||||
def test_plain_message_is_unchanged(self) -> None:
|
||||
self.assertEqual(transform_changelog_message("just text"), "just text")
|
||||
|
||||
def test_no_heading_or_backtick_survives(self) -> None:
|
||||
out = transform_changelog_message("# Heading\n\n* item with `code`")
|
||||
self.assertNotIn("`", out)
|
||||
for line in out.split("\n"):
|
||||
self.assertFalse(line.lstrip().startswith("#"))
|
||||
|
||||
|
||||
class TestLintChangelogEntry(unittest.TestCase):
|
||||
def test_clean_entry_has_no_findings(self) -> None:
|
||||
entry = "## [1.0.0] - 2026-01-01\n\n* a clean bullet\n\n"
|
||||
self.assertEqual(lint_changelog_entry("CHANGELOG.md", entry), [])
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
@@ -321,6 +321,24 @@ class TestUpdateChangelog(unittest.TestCase):
|
||||
self.assertIn("\n\n* Provided bullet\n", content)
|
||||
self.assertNotIn("* * Provided bullet", content)
|
||||
|
||||
def test_update_changelog_transforms_heading_and_inline_code(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = os.path.join(tmpdir, "CHANGELOG.md")
|
||||
update_changelog(
|
||||
path,
|
||||
"1.2.3",
|
||||
message="# Summary\n\n* uses `foo` tool",
|
||||
preview=False,
|
||||
)
|
||||
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
self.assertIn("**Summary**", content)
|
||||
self.assertIn("*foo*", content)
|
||||
self.assertNotIn("# Summary", content)
|
||||
self.assertNotIn("`foo`", content)
|
||||
|
||||
def test_update_changelog_preview_does_not_write(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
path = os.path.join(tmpdir, "CHANGELOG.md")
|
||||
|
||||
Reference in New Issue
Block a user