diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26ce75f..a8e0d14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,6 +104,24 @@ jobs: ref: ${{ env.SOURCE_REF }} fetch-depth: 1 + # Record which invisible_firefox commit this build came from. The publish + # job turns the range previous-release..this commit into the release notes + # (scripts/gen_release_notes.py), and re-publishes it as a source-commit.txt + # asset so the NEXT release knows where to start the changelog. One leg is + # enough — all legs check out the same SOURCE_REF. + - name: Record source commit (for auto release notes) + if: matrix.leg == 'linux-x86_64' + shell: bash + run: git rev-parse HEAD > source-commit.txt && cat source-commit.txt + - name: Upload source-commit artifact + if: matrix.leg == 'linux-x86_64' + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: source-commit + path: source-commit.txt + if-no-files-found: error + retention-days: 7 + - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: { python-version: '3.11' } @@ -373,9 +391,18 @@ jobs: permissions: contents: write steps: + - name: Checkout wrapper (for scripts/gen_release_notes.py) + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: { fetch-depth: 1 } + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: { python-version: '3.11' } - name: Download all build assets uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: { pattern: asset-*, path: dl, merge-multiple: true } + - name: Download source-commit metadata + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: { name: source-commit, path: src-meta } - name: Assert all 5 target archives present (no silent partial release) run: | cd dl @@ -402,9 +429,38 @@ jobs: TAG="${{ github.event.inputs.release_tag }}" [ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}" echo "tag=$TAG" >> "$GITHUB_OUTPUT" - # bare revision number for the release title: firefox-9 -> 9 - echo "num=${TAG#firefox-}" >> "$GITHUB_OUTPUT" + # bare revision number for the release title: firefox-10 -> 10 + N="${TAG#firefox-}" + echo "num=$N" >> "$GITHUB_OUTPUT" + # previous release tag, for the changelog range (firefox-10 -> firefox-9) + case "$N" in (*[!0-9]*|'') echo "prevtag=" >> "$GITHUB_OUTPUT";; + (*) echo "prevtag=firefox-$((N-1))" >> "$GITHUB_OUTPUT";; esac echo "publishing DRAFT release for tag: $TAG" + - name: Build release notes from the source commits + id: notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + CUR="$(cat src-meta/source-commit.txt 2>/dev/null | tr -d '[:space:]')" + echo "this build's source commit: ${CUR:-}" + # previous release's recorded source commit — gives the changelog range. + # Missing (first automated notes / firefox-0) -> notes omit the changelog. + PREV="" + PREVTAG="${{ steps.tag.outputs.prevtag }}" + if [ -n "$PREVTAG" ] && gh release download "$PREVTAG" -R "${{ github.repository }}" \ + --pattern source-commit.txt --dir prev 2>/dev/null; then + PREV="$(cat prev/source-commit.txt | tr -d '[:space:]')" + echo "previous ($PREVTAG) source commit: $PREV" + else + echo "no previous source-commit.txt — changelog section omitted this time" + fi + python scripts/gen_release_notes.py --tag "${{ steps.tag.outputs.tag }}" \ + --current "$CUR" --prev-sha "$PREV" --source-repo "${{ env.SOURCE_REPO }}" > body.md + echo "----- generated body.md -----"; cat body.md + # publish THIS build's source commit so the next release can diff from it + cp src-meta/source-commit.txt dl/source-commit.txt - name: Create DRAFT release with all assets uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: @@ -417,13 +473,7 @@ jobs: dl/*.tar.gz dl/*.zip dl/checksums.txt - body: | - Patched Firefox 150.0.1 — built on GitHub Actions ($0, no mold). - Targets: linux-x86_64, linux-arm64, win-x86_64, macos-arm64, macos-x86_64. - - DRAFT — do not publish until validate_release.py + realness gate pass on all archives. - - macOS: ad-hoc signed (not notarized). After download run: - xattr -dr com.apple.quarantine Firefox.app + dl/source-commit.txt + body_path: body.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/scripts/gen_release_notes.py b/scripts/gen_release_notes.py new file mode 100644 index 0000000..7ea4296 --- /dev/null +++ b/scripts/gen_release_notes.py @@ -0,0 +1,114 @@ +#!/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())