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
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ jobs:
echo
echo "## Changes"
echo
# The CHANGELOG section for this version, when there is one.
python3 scripts/changelog_section.py "${GITHUB_REF_NAME#v}" || echo "See CHANGELOG.md."
# The CHANGELOG section for this version, when there is one: its
# intro, with the detailed lists collapsed.
python3 scripts/changelog_section.py --collapse "${GITHUB_REF_NAME#v}" || echo "See CHANGELOG.md."
} > notes.md
cat notes.md
- name: Create the GitHub release
Expand Down
26 changes: 21 additions & 5 deletions scripts/changelog_section.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#!/usr/bin/env python3
"""Print the CHANGELOG.md section for one version (release-notes helper).

scripts/changelog_section.py 0.1.0-rc1
scripts/changelog_section.py [--collapse] 0.1.0-rc1

Matches the `## [<version>]` heading and prints everything up to the next
`## ` heading. Exits 1 when the version has no section, so the release
workflow can fall back to a generic line.

With --collapse, the section's intro (everything before its first `### `
heading) stays visible and the rest goes in a collapsed `<details>` block, so
a release page leads with the summary.
"""

from __future__ import annotations
Expand All @@ -23,16 +27,28 @@ def section(text: str, version: str) -> str | None:
return m.group(1).strip() if m else None


def collapse(body: str) -> str:
m = re.search(r"^### ", body, re.M)
if m is None:
return body
intro, details = body[:m.start()].strip(), body[m.start():].strip()
# GitHub only renders Markdown inside <details> with blank lines around it.
folded = f"<details>\n<summary>Full changelog</summary>\n\n{details}\n\n</details>"
return f"{intro}\n\n{folded}" if intro else folded


def main(argv: list[str]) -> int:
if len(argv) != 1:
folded = "--collapse" in argv
args = [a for a in argv if a != "--collapse"]
if len(args) != 1:
print(__doc__, file=sys.stderr)
return 2
path = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
body = section(path.read_text(encoding="utf-8"), argv[0])
body = section(path.read_text(encoding="utf-8"), args[0])
if body is None:
print(f"no CHANGELOG section for {argv[0]}", file=sys.stderr)
print(f"no CHANGELOG section for {args[0]}", file=sys.stderr)
return 1
print(body)
print(collapse(body) if folded else body)
return 0


Expand Down
57 changes: 57 additions & 0 deletions tests/test_changelog_section.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Release notes from CHANGELOG.md: scripts/changelog_section.py."""

import importlib.util
import unittest
from pathlib import Path

_PATH = Path(__file__).resolve().parent.parent / "scripts" / "changelog_section.py"
_spec = importlib.util.spec_from_file_location("changelog_section", _PATH)
changelog_section = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(changelog_section)

CHANGELOG = """# Changelog

## [Unreleased]

## [2.0.0] - 2026-11-01

Copying replaces merging.

- Works on the Steam Frame

### Added

- A thing

### Fixed

- Another thing

## [1.0.0] - 2026-09-21

### Added

- First release
"""


class CollapseTests(unittest.TestCase):
def test_keeps_the_intro_and_folds_the_lists(self):
body = changelog_section.section(CHANGELOG, "2.0.0")
self.assertEqual(changelog_section.collapse(body), (
"Copying replaces merging.\n\n- Works on the Steam Frame\n\n"
"<details>\n<summary>Full changelog</summary>\n\n"
"### Added\n\n- A thing\n\n### Fixed\n\n- Another thing\n\n</details>"))

def test_a_section_without_an_intro_is_all_folded(self):
body = changelog_section.section(CHANGELOG, "1.0.0")
self.assertEqual(changelog_section.collapse(body), (
"<details>\n<summary>Full changelog</summary>\n\n"
"### Added\n\n- First release\n\n</details>"))

def test_a_section_without_subheadings_is_unchanged(self):
self.assertEqual(changelog_section.collapse("Just a note."), "Just a note.")


if __name__ == "__main__":
unittest.main()
Loading