#!/usr/bin/env python3 """Generate the GitHub release body for a firefox-N build from the actual invisible_firefox commits that went into it. The release tag (firefox-N) lives on the wrapper, but the binary's changes live on the SOURCE repo (feder-cr/invisible_firefox). We never deep-clone that history (it's a full Firefox fork); instead we use GitHub's compare API to list the commits between the PREVIOUS release's source commit and this one, and turn their subject lines into a short human-readable "What changed" list. - The previous release's source commit comes from its ``source-commit.txt`` asset (this script's own output uploads one for the next run to read). - If there's no previous source commit (first automated release) or the compare fails, we fall back to a body WITHOUT the changelog section — publishing must never break on note generation. This is NOT an LLM and NOT a raw ``git log`` dump: it filters out the non-user-facing commits (docs/chore/ci/test/style) and prints the remaining subjects as plain bullets. Quality rides on writing good commit subjects. Usage: python scripts/gen_release_notes.py --tag firefox-10 --current \ [--prev-sha ] [--source-repo feder-cr/invisible_firefox] # reads GITHUB_TOKEN from the env for the compare API (optional for public). """ from __future__ import annotations import argparse import json import os import re import sys import urllib.request import urllib.error # Conventional-commit prefixes that never belong in user-facing release notes. _SKIP = re.compile(r"^(docs|chore|ci|test|style|build)(\(|:)", re.I) def _api(url: str, token: str | None) -> dict: headers = {"Accept": "application/vnd.github+json", "User-Agent": "invisible-playwright-release-notes"} if token: headers["Authorization"] = f"Bearer {token}" req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=30) as r: return json.load(r) def changelog_bullets(source_repo: str, prev_sha: str, current_sha: str, token: str | None) -> list[str]: """Return the user-facing commit subjects in prev_sha..current_sha, or [].""" if not prev_sha or not current_sha or prev_sha == current_sha: return [] url = f"https://api.github.com/repos/{source_repo}/compare/{prev_sha}...{current_sha}" try: data = _api(url, token) except (urllib.error.URLError, urllib.error.HTTPError, ValueError) as e: print(f"[gen_release_notes] compare API failed ({e}); no changelog section", file=sys.stderr) return [] bullets: list[str] = [] for c in data.get("commits", []): subject = (c.get("commit", {}).get("message") or "").splitlines()[0].strip() if not subject or _SKIP.match(subject): continue bullets.append(subject.rstrip(".")) return bullets def build_body(tag: str, current_sha: str, bullets: list[str]) -> str: m = re.search(r"(\d+)", tag) n = int(m.group(1)) if m else None prev_label = f"firefox-{n - 1}" if n else "the previous build" short = (current_sha or "")[:8] parts = ["Patched Firefox 150.0.1, the stealth build invisible_playwright drives.", ""] if bullets: parts.append(f"What changed since {prev_label}:") parts += [f"- {b}" for b in bullets] parts.append("") parts += [ "Builds: Linux x86_64, Linux arm64, Windows x86_64, macOS arm64, macOS x86_64.", "", "Most people won't grab these by hand. The wrapper fetches the right one for " "your platform on first run:", "", " pip install git+https://github.com/feder-cr/invisible_playwright", "", "If you do download manually, `checksums.txt` has the SHA256s. The macOS builds " "are ad-hoc signed (not notarized), so clear the quarantine flag: " "`xattr -dr com.apple.quarantine Firefox.app`", ] if short: parts += ["", f"Built from invisible_firefox @{short}."] return "\n".join(parts) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--tag", required=True, help="release tag, e.g. firefox-10") ap.add_argument("--current", required=True, help="invisible_firefox SHA this build was built from") ap.add_argument("--prev-sha", default="", help="previous release's source SHA (omit for none)") ap.add_argument("--source-repo", default="feder-cr/invisible_firefox") args = ap.parse_args() token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") bullets = changelog_bullets(args.source_repo, args.prev_sha, args.current, token) sys.stdout.write(build_body(args.tag, args.current, bullets)) return 0 if __name__ == "__main__": sys.exit(main())