diff --git a/src/pkgmgr/actions/release/files/changelog_md.py b/src/pkgmgr/actions/release/files/changelog_md.py index 142f9d8..0cc4e61 100644 --- a/src/pkgmgr/actions/release/files/changelog_md.py +++ b/src/pkgmgr/actions/release/files/changelog_md.py @@ -16,6 +16,11 @@ H1_RE = re.compile(r"^#\s+\S", re.MULTILINE) H2_RE = re.compile(r"^##\s+\S", re.MULTILINE) +def _at_end_of_file(entry: str) -> str: + """Return *entry* without the blank line that separates it from a next one.""" + return entry.rstrip("\n") + "\n" + + def _insert_after_h1(existing: str, entry: str) -> str: """Place *entry* after the H1 (and any intro prose), above the first H2. @@ -26,7 +31,7 @@ def _insert_after_h1(existing: str, entry: str) -> str: ``## ``) is preserved: *entry* is prepended unchanged. """ if not existing.strip(): - return f"# Changelog\n\n{entry}" + return f"# Changelog\n\n{_at_end_of_file(entry)}" if not H1_RE.search(existing): # Legacy layout: file starts with `## [version]` and has no H1. @@ -43,7 +48,7 @@ def _insert_after_h1(existing: str, entry: str) -> str: if existing.endswith("\n\n") else ("\n" if existing.endswith("\n") else "\n\n") ) - return f"{existing}{suffix}{entry}" + return f"{existing}{suffix}{_at_end_of_file(entry)}" # Insert new entry just before the first H2. head = existing[: h2_match.start()].rstrip("\n") + "\n\n" diff --git a/tests/unit/pkgmgr/actions/release/test_changelog_md.py b/tests/unit/pkgmgr/actions/release/test_changelog_md.py new file mode 100644 index 0000000..f22b341 --- /dev/null +++ b/tests/unit/pkgmgr/actions/release/test_changelog_md.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import unittest + +from pkgmgr.actions.release.files.changelog_md import _insert_after_h1 + +ENTRY = "## [1.0.0] - 2026-09-18\n\nOfficial Release\n\n" + + +class TestInsertAfterH1(unittest.TestCase): + def _assert_lint_clean(self, document: str) -> None: + self.assertNotIn( + "\n\n\n", + document, + f"MD012: multiple consecutive blank lines in\n{document!r}", + ) + self.assertTrue( + document.endswith("\n") and not document.endswith("\n\n"), + f"MD012: blank line at end of file in\n{document!r}", + ) + + def test_an_empty_changelog_gets_one_trailing_newline(self) -> None: + self._assert_lint_clean(_insert_after_h1("", ENTRY)) + + def test_a_first_entry_under_a_bare_h1_gets_one_trailing_newline(self) -> None: + self._assert_lint_clean(_insert_after_h1("# Changelog\n", ENTRY)) + + def test_a_second_entry_stays_separated_from_the_first(self) -> None: + existing = "# Changelog\n\n## [0.9.0] - 2026-09-01\n\nOlder\n" + document = _insert_after_h1(existing, ENTRY) + self._assert_lint_clean(document) + self.assertIn("Official Release\n\n## [0.9.0]", document) + + def test_a_legacy_headerless_changelog_gains_an_h1(self) -> None: + document = _insert_after_h1("## [0.9.0] - 2026-09-01\n\nOlder\n", ENTRY) + self._assert_lint_clean(document) + self.assertTrue(document.startswith("# Changelog\n\n## [1.0.0]")) + + +if __name__ == "__main__": + unittest.main()