Some checks failed
changelog-draft / changelog-draft (push) Failing after 1s
Drafts CHANGELOG_PENDING.md entries from Conventional Commit subjects on every push to main -- feat/fix/refactor/perf/security bucketed into Added/Fixed/Changed, chore/ci/test/docs/style/merge skipped, anything mentioning internal infra hostnames dropped. No LLM call, no network access. Never touches CHANGELOG.md -- still needs a human to fold an entry in. Rolled out fleet-wide from the PeerCortex pilot (2026-07-18).
106 lines
3.3 KiB
Python
106 lines
3.3 KiB
Python
#!/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"
|
|
"<!-- PENDING_ENTRIES -->\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 = "<!-- PENDING_ENTRIES -->\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()
|