Automate bilingual changelog generation before publication

This commit is contained in:
Alexandre NOEL 2026-10-08 18:46:39 +02:00
commit aeea5ab595
3 changed files with 154 additions and 0 deletions

76
tools/render-changelog.py Normal file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Generate the public FR/EN changelogs from changelog.json (standard library only)."""
import argparse
import json
import os
from pathlib import Path
import re
import tempfile
ROOT = Path(__file__).resolve().parents[1]
OUTPUTS = {"fr": "CHANGELOG.md", "en": "CHANGELOG.en.md"}
def render(source):
if source.get("schema") != 1 or not isinstance(source.get("entries"), list):
raise ValueError("Unsupported changelog schema")
ids = set()
for entry in source["entries"]:
identifier = entry["id"]
if not re.fullmatch(r"[A-Za-z0-9_.-]+", identifier) or identifier in ids:
raise ValueError("Invalid or duplicate entry id: " + identifier)
ids.add(identifier)
if not ids:
raise ValueError("The changelog must not be empty")
documents = {}
for language, filename in OUTPUTS.items():
preamble = source["preamble"][language].strip()
if not preamble.startswith("# "):
raise ValueError("Missing preamble for " + language)
sections = [preamble]
for entry in source["entries"]:
body = entry["text"][language].strip()
if not body.startswith("## "):
raise ValueError("Missing heading: " + entry["id"] + "/" + language)
sections.append('<a id="' + entry["id"] + '"></a>\n\n' + body)
documents[filename] = "\n\n".join(sections) + "\n"
return documents
def generate(root=ROOT, check=False):
documents = render(json.loads((root / "changelog.json").read_text(encoding="utf-8")))
changed = []
for filename, content in documents.items():
path = root / filename
if path.exists() and path.read_text(encoding="utf-8") == content:
continue
changed.append(filename)
if check:
continue
descriptor, temporary = tempfile.mkstemp(prefix=".changelog-", dir=root)
try:
with os.fdopen(descriptor, "w", encoding="utf-8", newline="\n") as stream:
stream.write(content)
os.chmod(temporary, 0o644)
os.replace(temporary, path)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
return changed
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Fail if generated files are out of date")
args = parser.parse_args()
try:
changed = generate(check=args.check)
except (ValueError, KeyError, TypeError, AttributeError) as error:
parser.exit(2, "Invalid changelog: " + str(error) + "\n")
if changed:
print(("Out of date: " if args.check else "Generated: ") + ", ".join(changed))
return 1 if args.check and changed else 0
if __name__ == "__main__":
raise SystemExit(main())