Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/developer/releaser-tool.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,6 @@ process:
7. Finally, it creates a draft release on GitHub with the changelog notes.
8. If releasing a bugfix, it offers to port the changelog to the ``main``
or ``master`` branch.

GitHub release descriptions are normalized without paragraph wrapping
before submission. This does not modify the changelog file.
2 changes: 2 additions & 0 deletions openwisp_utils/releaser/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
demote_markdown_headings,
format_file_with_docstrfmt,
get_current_branch,
normalize_markdown,
rst_to_markdown,
)
from openwisp_utils.releaser.version import (
Expand Down Expand Up @@ -360,6 +361,7 @@ def main():
release_body_md = rst_to_markdown(release_body_rst)

try:
release_body_md = normalize_markdown(release_body_md)
release_url = gh.create_release(tag_name, release_title, release_body_md)
print(f"📦 Draft release created on GitHub: {release_url}")
except SkipSignal:
Expand Down
47 changes: 47 additions & 0 deletions openwisp_utils/releaser/tests/test_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,53 @@ def test_feature_release_flow_markdown(mock_all, mocker):
assert "## Markdown Changelog" in release_call_args[2]


@pytest.mark.parametrize(
("changelog_format", "latest_block", "converted_body"),
[
(
"md",
"## Version 1.2.1 [2025-08-11]\n\n### Features\n\n- A wrapped\n" " item.",
None,
),
(
"rst",
"Version 1.2.1 [2025-08-11]\n"
"--------------------------\n\n"
"Features\n~~~~~~~~\n\n"
"- A wrapped\n item.",
"# Features\n\n- A wrapped item.",
),
],
)
def test_release_body_is_normalized(
mock_all, mocker, changelog_format, latest_block, converted_body
):
"""Tests that both changelog formats pass a normalized body to GitHub."""
mock_config, mock_gh = mock_all["check_prerequisites"].return_value
mock_config["changelog_format"] = changelog_format
mock_config["changelog_path"] = f"CHANGES.{changelog_format}"
mock_all["get_release_block_from_file"].return_value = latest_block
if converted_body:
mocker.patch(
"openwisp_utils.releaser.release.rst_to_markdown",
return_value=converted_body,
)
normalizer = mocker.patch(
"openwisp_utils.releaser.release.normalize_markdown",
side_effect=lambda value: f"normalized: {value}",
)
run_release()
expected_body = "# Features\n\n- A wrapped\n item."
if converted_body:
expected_body = converted_body
normalizer.assert_called_once_with(expected_body)
assert mock_gh.create_release.call_args.args == (
"1.3.0",
"1.3.0 [2025-08-11]",
f"normalized: {expected_body}",
)


def test_release_flow_manual_bump(mock_all):
"""Tests the flow where automatic version bumping fails and the user is prompted to do it manually."""
mock_all["bump_version"].return_value = False
Expand Down
25 changes: 25 additions & 0 deletions openwisp_utils/releaser/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
SkipSignal,
branch_exists,
format_file_with_docstrfmt,
normalize_markdown,
retryable_request,
)

Expand Down Expand Up @@ -104,6 +105,30 @@ def test_demote_markdown_headings():
assert result.strip() == expected_md.strip()


def test_normalize_markdown():
"""Test that Markdown paragraphs are unwrapped without losing structure."""
markdown = """## Features

- Configured nginx to
[serve precompressed files](https://example.com), improving delivery.
- Nested item.

> A wrapped
> quote.

```python
long_code_line = "must stay intact"
```"""
normalized = normalize_markdown(markdown)
assert (
"- Configured nginx to [serve precompressed files](https://example.com), "
"improving delivery." in normalized
)
assert " - Nested item." in normalized
assert "> A wrapped quote." in normalized
assert '``` python\nlong_code_line = "must stay intact"\n```' in normalized


@patch("openwisp_utils.releaser.utils.subprocess.run")
@patch("builtins.print")
def test_format_file_with_docstrfmt_success(mock_print, mock_subprocess):
Expand Down
7 changes: 7 additions & 0 deletions openwisp_utils/releaser/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ def rst_to_markdown(text):
).strip()


def normalize_markdown(text):
"""Normalize GitHub-Flavored Markdown without wrapping paragraphs."""
return pypandoc.convert_text(
text, "gfm", format="gfm", extra_args=["--wrap=none"]
).strip()


def _call_docstrfmt(file_path):
return subprocess.run(
[
Expand Down
Loading