#!/usr/bin/env python3 """Draft the next Kami release notes from git log. Pulls commit subjects in a rev range and pours them into the V1.4.0-style template (centered logo + bilingual changelog). The output is a starting point: regroup the commits into 5-8 product-themed bullets and translate each to Chinese before publishing. Usage: python3 scripts/draft-release-notes.py python3 scripts/draft-release-notes.py V1.4.0..HEAD python3 scripts/draft-release-notes.py \\ --version V1.5.0 \\ --title "Steadier Hand" \\ --subtitle-en "Plugin install fix and audit cleanup." \\ --subtitle-cn "插件安装修复,审计清理沉淀。" The default rev range is `..HEAD`. Output goes to stdout; pipe to a file or `pbcopy` to feed `gh release create --notes-file`. """ from __future__ import annotations import argparse import subprocess import sys from textwrap import dedent _HEADER = dedent("""\
Kami Logo

Kami {version}

{subtitle_en}

""") _FOOTER = dedent("""\ > Kami is a quiet design system for professional documents, one constraint set that any agent can trust. https://github.com/tw93/Kami """) # Conventional-commit prefix to a short product label, used as a hint when # the user reorganizes the auto-listed commits into themed bullets. _PREFIX_HINT = { "build": "build", "ci": "ci", "feat": "feature", "fix": "fix", "docs": "docs", "chore": "chore", "refactor": "refactor", "test": "test", "perf": "perf", "revert": "revert", } def _run(cmd: list[str]) -> str: result = subprocess.run(cmd, capture_output=True, text=True, check=False) if result.returncode != 0: raise RuntimeError(f"{' '.join(cmd)}: {result.stderr.strip() or 'failed'}") return result.stdout def latest_tag() -> str | None: try: out = _run(["git", "describe", "--tags", "--abbrev=0"]).strip() except RuntimeError: return None return out or None def commits_in(rev_range: str) -> list[tuple[str, str]]: """Return [(short_sha, subject), ...] in chronological order.""" out = _run(["git", "log", rev_range, "--format=%h\t%s", "--reverse"]) rows: list[tuple[str, str]] = [] for line in out.splitlines(): if "\t" in line: sha, subject = line.split("\t", 1) rows.append((sha.strip(), subject.strip())) return rows def classify(subject: str) -> str: """Return a one-word commit category derived from the conventional-commit prefix.""" head = subject.split(":", 1)[0].split("(", 1)[0].strip().lower() return _PREFIX_HINT.get(head, "other") def render( version: str, title: str, subtitle_en: str, subtitle_cn: str, rev_range: str, commits: list[tuple[str, str]], ) -> str: out: list[str] = [] out.append(_HEADER.format(version=version, subtitle_en=subtitle_en)) out.append(f"") out.append(f"") out.append("") out.append("") out.append("### Changelog") out.append("") for i, (sha, subject) in enumerate(commits, 1): out.append(f"{i}. **TODO**: {subject} ") out.append("") out.append("### 更新日志") out.append("") out.append(f"") out.append("") out.append("") for i in range(1, len(commits) + 1): out.append(f"{i}. **TODO**:(对应英文第 {i} 条)") out.append("") out.append(_FOOTER) return "\n".join(out) def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument( "rev_range", nargs="?", default=None, help="Git rev range (default: ..HEAD)", ) parser.add_argument("--version", default="VX.Y.Z", help="Version label, e.g. V1.5.0") parser.add_argument("--title", default="", help="Release title, e.g. 'Steadier Hand'") parser.add_argument( "--subtitle-en", default="<one-line subtitle>", help="Short English subtitle for the centered hero block", ) parser.add_argument( "--subtitle-cn", default="<一句话中文副标题>", help="Short Chinese subtitle, kept as a comment hint for translators", ) return parser.parse_args(argv[1:]) def main(argv: list[str]) -> int: args = parse_args(argv) rev_range = args.rev_range if rev_range is None: tag = latest_tag() if not tag: print("ERROR: no tag found; pass an explicit rev range like V1.0.0..HEAD", file=sys.stderr) return 2 rev_range = f"{tag}..HEAD" try: commits = commits_in(rev_range) except RuntimeError as exc: print(f"ERROR: {exc}", file=sys.stderr) return 2 if not commits: print(f"ERROR: no commits in {rev_range}", file=sys.stderr) return 1 out = render( version=args.version, title=args.title, subtitle_en=args.subtitle_en, subtitle_cn=args.subtitle_cn, rev_range=rev_range, commits=commits, ) print(out) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))