Merge pull request #4382 from pipecat-ai/aleix/unfill-changelog-script
chore(changelog): add release-changelog.py and fix (PR line indentation in towncrier template
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% for text, values in sections[section][category].items() %}
|
{% for text, values in sections[section][category].items() %}
|
||||||
{{ text }}
|
{{ text }}
|
||||||
(PR {{ values|join(', ') }})
|
(PR {{ values|join(', ') }})
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
96
scripts/release-changelog.py
Executable file
96
scripts/release-changelog.py
Executable file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate release notes from CHANGELOG.md for use in GitHub Releases (or anywhere).
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
python scripts/release-changelog.py VERSION
|
||||||
|
python scripts/release-changelog.py 1.1.0
|
||||||
|
python scripts/release-changelog.py --file path/to/CHANGELOG.md 1.1.0
|
||||||
|
|
||||||
|
Extracts the requested ``## [VERSION]`` section (heading and ``### …``
|
||||||
|
subheadings included) and prints it to stdout. The only transformation
|
||||||
|
applied is collapsing each entry paragraph onto a single line, so it is
|
||||||
|
suitable for pasting into release notes that don't need 80-column wrapping.
|
||||||
|
``(PR [...])`` references stay on their own two-space-indented continuation
|
||||||
|
line. The input file is never modified.
|
||||||
|
|
||||||
|
Every paragraph is unfilled. Indentation is preserved — each logical line
|
||||||
|
(bullets, sub-bullets, and the ``(PR [...])`` continuation) keeps its
|
||||||
|
leading whitespace; only the wrapped continuation lines that follow them
|
||||||
|
get joined back. Code-block paragraphs (triple-backtick fences, or deeply
|
||||||
|
indented blocks with no list markers around) are passed through untouched.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_CHANGELOG = Path(__file__).resolve().parent.parent / "CHANGELOG.md"
|
||||||
|
|
||||||
|
PARAGRAPH_SPLIT_RE = re.compile(r"(\n[ \t]*\n+)")
|
||||||
|
CONTINUATION_RE = re.compile(r"\n(?![ \t]*[-*+] )[ \t]+")
|
||||||
|
PR_RE = re.compile(r" \(PR \[")
|
||||||
|
CODE_FENCE_RE = re.compile(r"^\s*```")
|
||||||
|
DEEP_INDENT_RE = re.compile(r"^ {4,}")
|
||||||
|
LIST_MARKER_RE = re.compile(r"^[ \t]*[-*+] ")
|
||||||
|
SECTION_HEADING_RE = re.compile(r"^## \[", re.MULTILINE)
|
||||||
|
|
||||||
|
|
||||||
|
def is_code_block_paragraph(paragraph: str) -> bool:
|
||||||
|
lines = paragraph.splitlines()
|
||||||
|
if any(CODE_FENCE_RE.match(line) for line in lines):
|
||||||
|
return True
|
||||||
|
# In a list paragraph, 4-space indent is a sub-bullet continuation, not
|
||||||
|
# code. Only treat deep-indented lines as code when no bullets are around
|
||||||
|
# to claim them.
|
||||||
|
if any(LIST_MARKER_RE.match(line) for line in lines):
|
||||||
|
return False
|
||||||
|
return any(DEEP_INDENT_RE.match(line) for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def unfill_entry(paragraph: str) -> str:
|
||||||
|
joined = CONTINUATION_RE.sub(" ", paragraph)
|
||||||
|
return PR_RE.sub("\n (PR [", joined, count=1)
|
||||||
|
|
||||||
|
|
||||||
|
def process(text: str) -> str:
|
||||||
|
parts = PARAGRAPH_SPLIT_RE.split(text)
|
||||||
|
for i in range(0, len(parts), 2):
|
||||||
|
para = parts[i]
|
||||||
|
if para and not is_code_block_paragraph(para):
|
||||||
|
parts[i] = unfill_entry(para)
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_section(text: str, version: str) -> str:
|
||||||
|
head_re = re.compile(rf"^## \[{re.escape(version)}\]", re.MULTILINE)
|
||||||
|
m = head_re.search(text)
|
||||||
|
if not m:
|
||||||
|
sys.exit(f"error: version {version!r} not found")
|
||||||
|
nxt = SECTION_HEADING_RE.search(text, m.end())
|
||||||
|
end = nxt.start() if nxt else len(text)
|
||||||
|
return text[m.start() : end]
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
summary = (__doc__ or "").splitlines()[0]
|
||||||
|
parser = argparse.ArgumentParser(description=summary)
|
||||||
|
parser.add_argument("version", help="Version to extract (e.g. 1.1.0).")
|
||||||
|
parser.add_argument(
|
||||||
|
"--file",
|
||||||
|
type=Path,
|
||||||
|
default=DEFAULT_CHANGELOG,
|
||||||
|
help=f"Path to the changelog (default: {DEFAULT_CHANGELOG}).",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
text = args.file.read_text()
|
||||||
|
section = extract_section(text, args.version)
|
||||||
|
sys.stdout.write(process(section))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user