mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-16 02:42:09 +00:00
138 lines
4.5 KiB
Python
138 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Render a publish-safe GitHub release body from the current RC packet."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
def _normalize_newlines(text: str) -> str:
|
|
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
|
|
def _replace_draft_heading(text: str, version: str) -> str:
|
|
lines = text.splitlines()
|
|
for index, line in enumerate(lines):
|
|
if line.startswith("# ") and "Draft Release Notes" in line:
|
|
lines[index] = f"# Pulse v{version} Release Notes"
|
|
return "\n".join(lines)
|
|
return text
|
|
|
|
|
|
def _drop_draft_disclaimer(text: str) -> str:
|
|
pattern = re.compile(
|
|
r"(?:^|\n)_?Draft only\. Do not treat this as published.*?(?:\n{2,}|$)",
|
|
re.DOTALL,
|
|
)
|
|
return pattern.sub("\n\n", text, count=1)
|
|
|
|
|
|
def _drop_level_two_sections(text: str, headings: set[str]) -> str:
|
|
lines = text.splitlines()
|
|
kept: list[str] = []
|
|
skip = False
|
|
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if stripped in headings:
|
|
skip = True
|
|
continue
|
|
if skip and stripped.startswith("## "):
|
|
skip = False
|
|
if not skip:
|
|
kept.append(line)
|
|
|
|
return "\n".join(kept)
|
|
|
|
|
|
def _drop_draft_packet_links(text: str) -> str:
|
|
return "\n".join(line for line in text.splitlines() if "_DRAFT.md" not in line)
|
|
|
|
|
|
def _collapse_blank_lines(text: str) -> str:
|
|
text = _normalize_newlines(text).strip()
|
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
return text + "\n"
|
|
|
|
|
|
def sanitize_release_notes(raw_text: str, version: str) -> str:
|
|
text = _normalize_newlines(raw_text)
|
|
text = _replace_draft_heading(text, version)
|
|
text = _drop_draft_disclaimer(text)
|
|
text = _drop_level_two_sections(text, {"## Installation", "## Promotion Metadata"})
|
|
text = _drop_draft_packet_links(text)
|
|
return _collapse_blank_lines(text)
|
|
|
|
|
|
def build_installation_section(version: str) -> str:
|
|
return "\n".join(
|
|
[
|
|
"## Installation",
|
|
"",
|
|
"**Docker (recommended):**",
|
|
"```bash",
|
|
f"docker pull rcourtman/pulse:{version}",
|
|
"```",
|
|
"",
|
|
"**Docker Compose:**",
|
|
f"Update your `docker-compose.yml` to use `rcourtman/pulse:{version}`",
|
|
"",
|
|
"See the [Installation Guide](https://github.com/rcourtman/Pulse#installation) for complete setup instructions.",
|
|
]
|
|
)
|
|
|
|
|
|
def build_promotion_metadata_section(args: argparse.Namespace) -> str:
|
|
lines = [
|
|
"## Promotion Metadata",
|
|
"",
|
|
f"- Promotion channel: {args.promotion_channel}",
|
|
f"- Candidate stable tag: {args.candidate_tag}",
|
|
f"- Promoted prerelease tag: {args.promoted_prerelease_tag or 'n/a'}",
|
|
f"- Rollback target: {args.rollback_target}",
|
|
f"- Rollback command: `{args.rollback_command}`",
|
|
]
|
|
if args.planned_ga_date:
|
|
lines.append(f"- Planned GA date: {args.planned_ga_date}")
|
|
if args.planned_v5_eos_date:
|
|
lines.append(f"- Planned v5 end-of-support date: {args.planned_v5_eos_date}")
|
|
lines.append(f"- Hotfix exception: {args.hotfix_exception}")
|
|
if args.hotfix_reason:
|
|
lines.append(f"- Hotfix reason: {args.hotfix_reason}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--version", required=True)
|
|
parser.add_argument("--release-notes-file", required=True)
|
|
parser.add_argument("--output", required=True)
|
|
parser.add_argument("--promotion-channel", required=True)
|
|
parser.add_argument("--candidate-tag", required=True)
|
|
parser.add_argument("--promoted-prerelease-tag", default="")
|
|
parser.add_argument("--rollback-target", required=True)
|
|
parser.add_argument("--rollback-command", required=True)
|
|
parser.add_argument("--planned-ga-date", default="")
|
|
parser.add_argument("--planned-v5-eos-date", default="")
|
|
parser.add_argument("--hotfix-exception", required=True)
|
|
parser.add_argument("--hotfix-reason", default="")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
raw_text = Path(args.release_notes_file).read_text(encoding="utf-8")
|
|
sanitized = sanitize_release_notes(raw_text, args.version).rstrip("\n")
|
|
sections = [
|
|
sanitized,
|
|
build_installation_section(args.version),
|
|
build_promotion_metadata_section(args),
|
|
]
|
|
Path(args.output).write_text("\n\n".join(sections) + "\n", encoding="utf-8")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|