#!/usr/bin/env python3 """Draft CHANGELOG_PENDING.md entries from Conventional Commit messages pushed since the last run. Deterministic -- no LLM call, no network access. Human review still required before folding an entry into CHANGELOG.md. """ import re import subprocess import sys from datetime import datetime, timezone INCLUDE_TYPES = { "feat": "Added", "fix": "Fixed", "refactor": "Changed", "perf": "Changed", "security": "Fixed", } SKIP_TYPES = {"chore", "ci", "test", "docs", "style", "merge"} # Anything mentioning internal infra by name/address is dropped rather than # drafted -- better a missing entry (caught in human review) than a leaked one. INTERNAL_PATTERN = re.compile( r"\b(erik|gitea\.context-x|192\.168\.|10\.10\.0\.|opnsense|ssh |systemd|pm2 |crontab)\b", re.IGNORECASE, ) PENDING_HEADER = ( "# Pending Changelog Entries\n\n" "Drafted automatically from commit messages since the last processed " "push. Review, edit as needed, and fold into CHANGELOG.md -- then clear " "this file.\n\n" "\n" ) def run(cmd): return subprocess.check_output(cmd, shell=True, text=True).strip() def main(): before = sys.argv[1] if len(sys.argv) > 1 else "" after = sys.argv[2] if len(sys.argv) > 2 else "HEAD" if not before or set(before) == {"0"}: print("No usable commit range (first push on branch) -- skipping.") return try: log = run("git log {}..{} --format='%s'".format(before, after)) except subprocess.CalledProcessError: print("Could not diff commit range -- skipping.") return if not log: print("No new commits.") return buckets = {"Added": [], "Fixed": [], "Changed": []} for subject in log.splitlines(): m = re.match(r"^(\w+)(\([\w./-]+\))?!?:\s*(.+)$", subject) if not m: continue ctype, _, desc = m.groups() ctype = ctype.lower() if ctype in SKIP_TYPES or ctype not in INCLUDE_TYPES: continue if INTERNAL_PATTERN.search(desc): continue buckets[INCLUDE_TYPES[ctype]].append(desc.strip()) if not any(buckets.values()): print("Nothing changelog-worthy in this push.") return date = datetime.now(timezone.utc).strftime("%Y-%m-%d") lines = ["## Pending -- {}\n".format(date)] for section in ("Added", "Fixed", "Changed"): if buckets[section]: lines.append("### {}".format(section)) for item in buckets[section]: lines.append("- {}".format(item)) lines.append("") entry = "\n".join(lines).rstrip() + "\n\n" try: with open("CHANGELOG_PENDING.md", "r") as f: existing = f.read() if not existing.strip(): existing = PENDING_HEADER # pre-existing but empty placeholder file except FileNotFoundError: existing = PENDING_HEADER marker = "\n" if marker in existing: head, tail = existing.split(marker, 1) updated = head + marker + entry + tail.lstrip("\n") else: updated = existing.rstrip("\n") + "\n\n" + entry with open("CHANGELOG_PENDING.md", "w") as f: f.write(updated) print("Wrote entry:\n" + entry) if __name__ == "__main__": main()