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:
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user