Files
2026-09-13 13:30:21 +08:00

54 lines
1.9 KiB
Python

#!/usr/bin/env python3
"""Print the CHANGELOG.md section for a version, for use as release notes.
VERSION accepts a tag name ("v2.7.0") or a plain version ("2.7.0"). Older
sections are titled with two components and a name, such as
"## Version 2.5: Help Formatter", so "2.5.0" also matches "## Version 2.5".
Each section keeps its own link definitions, so the output needs no fixup.
With --title, print the section heading instead of the body; this is the name
of the GitHub release.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
def extract(text: str, version: str) -> tuple[str, str] | None:
"""Return the heading and the body of the section for a version."""
heads = list(re.finditer(r"^## (Version (\S+?):?(?:[ \t].*)?)$", text, re.M))
for candidate in (version, version.rsplit(".", 1)[0]):
for i, head in enumerate(heads):
if head.group(2) != candidate:
continue
end = heads[i + 1].start() if i + 1 < len(heads) else len(text)
return head.group(1), text[head.end() : end].strip()
return None
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("version", help='tag ("v2.7.0") or version ("2.7.0")')
parser.add_argument("changelog", nargs="?", type=Path, default=Path("CHANGELOG.md"))
parser.add_argument(
"--title", action="store_true", help="print the heading, not the body"
)
args = parser.parse_args()
version = args.version.lstrip("v")
section = extract(args.changelog.read_text(encoding="utf-8"), version)
if section is None:
raise SystemExit(f"No '## Version {version}' section in {args.changelog}")
heading, notes = section
print(heading if args.title else notes)
if __name__ == "__main__":
main()