fix(release): preserve multi-line changelog bodies in package mirrors

update_debian_changelog and update_spec_changelog wrapped the whole release message in a single prefixed line, so only the first line of a multi-line (already-bulleted) message was indented/dashed while the rest stayed at column 0. That made dpkg-parsechangelog emit 'badly formatted heading line' and rpmspec report 'bad date', failing the package build. Each non-empty body line is now indented (Debian) or dash-prefixed (RPM) individually.

update_changelog no longer auto-prepends a '* ' bullet to the CHANGELOG.md entry: the message is inserted verbatim with blank-line separation, so a caller-provided markdown list stays markdown-lint-clean instead of gaining a stray leading bullet.

Adds regression tests for the multi-line debian/rpm mirroring and the no-injected-bullet behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-Birkenbach
2026-06-28 00:51:37 +02:00
parent f6228988e1
commit 87ee6c66c8
4 changed files with 120 additions and 6 deletions

View File

@@ -73,7 +73,8 @@ def update_changelog(
else: else:
message = editor_message message = editor_message
entry = f"## [{new_version}] - {today}\n\n* {message}\n\n" body = message.strip() if message and message.strip() else f"Release {new_version}."
entry = f"## [{new_version}] - {today}\n\n{body}\n\n"
if os.path.exists(changelog_path): if os.path.exists(changelog_path):
try: try:

View File

@@ -47,10 +47,17 @@ def update_debian_changelog(
author_name, author_email = _get_debian_author() author_name, author_email = _get_debian_author()
first_line = f"{package_name} ({debian_version}) unstable; urgency=medium" first_line = f"{package_name} ({debian_version}) unstable; urgency=medium"
body_line = message.strip() if message else f"Automated release {new_version}." body = (
message.strip()
if message and message.strip()
else f"Automated release {new_version}."
)
indented_body = "\n".join(
f" {line}" if line.strip() else line for line in body.split("\n")
)
stanza = ( stanza = (
f"{first_line}\n\n" f"{first_line}\n\n"
f" * {body_line}\n\n" f"{indented_body}\n\n"
f" -- {author_name} <{author_email}> {date_str}\n\n" f" -- {author_name} <{author_email}> {date_str}\n\n"
) )

View File

@@ -30,11 +30,18 @@ def update_spec_changelog(
date_str = now.strftime("%a %b %d %Y") date_str = now.strftime("%a %b %d %Y")
author_name, author_email = _get_debian_author() author_name, author_email = _get_debian_author()
body_line = message.strip() if message else f"Automated release {new_version}." body = (
message.strip()
if message and message.strip()
else f"Automated release {new_version}."
)
dashed_body = "\n".join(
f"- {line}" if line.strip() else line for line in body.split("\n")
)
stanza = ( stanza = (
f"* {date_str} {author_name} <{author_email}> - {debian_version}\n" f"* {date_str} {author_name} <{author_email}> - {debian_version}\n"
f"- {body_line}\n\n" f"{dashed_body}\n\n"
) )
marker = "%changelog" marker = "%changelog"

View File

@@ -310,6 +310,17 @@ class TestUpdateChangelog(unittest.TestCase):
self.assertTrue(content.startswith("# Changelog\n\n## [1.0.0]")) self.assertTrue(content.startswith("# Changelog\n\n## [1.0.0]"))
self.assertIn("## [0.1.0] - 2024-01-01", content) self.assertIn("## [0.1.0] - 2024-01-01", content)
def test_update_changelog_does_not_inject_a_leading_bullet(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "CHANGELOG.md")
update_changelog(path, "1.2.3", message="* Provided bullet", preview=False)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
self.assertIn("\n\n* Provided bullet\n", content)
self.assertNotIn("* * Provided bullet", content)
def test_update_changelog_preview_does_not_write(self) -> None: def test_update_changelog_preview_does_not_write(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "CHANGELOG.md") path = os.path.join(tmpdir, "CHANGELOG.md")
@@ -349,7 +360,7 @@ class TestUpdateDebianChangelog(unittest.TestCase):
content = f.read() content = f.read()
self.assertIn("package-manager (1.2.3-1) unstable; urgency=medium", content) self.assertIn("package-manager (1.2.3-1) unstable; urgency=medium", content)
self.assertIn(" * Test debian entry", content) self.assertIn(" Test debian entry", content)
self.assertIn("Test Maintainer <test@example.com>", content) self.assertIn("Test Maintainer <test@example.com>", content)
self.assertIn("existing content", content) self.assertIn("existing content", content)
@@ -378,6 +389,44 @@ class TestUpdateDebianChangelog(unittest.TestCase):
self.assertEqual(content, original) self.assertEqual(content, original)
def test_update_debian_changelog_indents_every_line_of_multiline_message(
self,
) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "changelog")
with open(path, "w", encoding="utf-8") as f:
f.write("existing content\n")
with patch.dict(
os.environ,
{"DEBFULLNAME": "Test Maintainer", "DEBEMAIL": "test@example.com"},
clear=False,
):
update_debian_changelog(
debian_changelog_path=path,
package_name="package-manager",
new_version="1.2.3",
message="* First bullet\n\n* Second bullet",
preview=False,
)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
self.assertIn(" * First bullet", content)
self.assertIn(" * Second bullet", content)
stanza = content.split(" -- ", 1)[0]
body_lines = [
line
for line in stanza.splitlines()
if line.strip() and not line.startswith("package-manager (")
]
for line in body_lines:
self.assertTrue(
line.startswith(" "),
msg=f"un-indented debian change line: {line!r}",
)
class TestUpdateSpecChangelog(unittest.TestCase): class TestUpdateSpecChangelog(unittest.TestCase):
def test_update_spec_changelog_inserts_stanza_after_changelog_marker(self) -> None: def test_update_spec_changelog_inserts_stanza_after_changelog_marker(self) -> None:
@@ -464,6 +513,56 @@ class TestUpdateSpecChangelog(unittest.TestCase):
self.assertEqual(content, original) self.assertEqual(content, original)
def test_update_spec_changelog_dashes_every_line_of_multiline_message(
self,
) -> None:
original = (
textwrap.dedent(
"""
Name: package-manager
Version: 0.1.0
Release: 5%{?dist}
%changelog
* Mon Jan 01 2024 Old Maintainer <old@example.com> - 0.1.0-1
- Old entry
"""
).strip()
+ "\n"
)
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "package-manager.spec")
with open(path, "w", encoding="utf-8") as f:
f.write(original)
with patch.dict(
os.environ,
{"DEBFULLNAME": "Test Maintainer", "DEBEMAIL": "test@example.com"},
clear=False,
):
update_spec_changelog(
spec_path=path,
package_name="package-manager",
new_version="1.2.3",
message="* First bullet\n\n* Second bullet",
preview=False,
)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
self.assertIn("- * First bullet", content)
self.assertIn("- * Second bullet", content)
new_section = content.split("- Old entry", 1)[0].split("%changelog", 1)[1]
for line in new_section.splitlines():
if not line.strip() or line.startswith("* "):
continue
self.assertTrue(
line.startswith("- "),
msg=f"rpm body line would be read as a new entry: {line!r}",
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()