ci: auto-generate release notes from the invisible_firefox commits
The publish job used a fixed body that still read 'DRAFT - do not publish' on the live release and listed none of the actual changes. Now the body is built from the source commits that went into the binary: the build records which invisible_firefox commit it came from (source-commit.txt), and publish diffs that against the previous release's recorded commit via the GitHub compare API (no deep clone, no cross-repo token) to list the user-facing subjects. docs/chore/ci/test commits are filtered out, and the body ends with 'Built from invisible_firefox @<sha>' for traceability. It's still a draft - the realness gate and the un-draft flip stay manual (issue #14).
This commit is contained in:
@@ -104,6 +104,24 @@ jobs:
|
|||||||
ref: ${{ env.SOURCE_REF }}
|
ref: ${{ env.SOURCE_REF }}
|
||||||
fetch-depth: 1
|
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
|
- name: Set up Python
|
||||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
with: { python-version: '3.11' }
|
with: { python-version: '3.11' }
|
||||||
@@ -373,9 +391,18 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
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
|
- name: Download all build assets
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with: { pattern: asset-*, path: dl, merge-multiple: true }
|
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)
|
- name: Assert all 5 target archives present (no silent partial release)
|
||||||
run: |
|
run: |
|
||||||
cd dl
|
cd dl
|
||||||
@@ -402,9 +429,38 @@ jobs:
|
|||||||
TAG="${{ github.event.inputs.release_tag }}"
|
TAG="${{ github.event.inputs.release_tag }}"
|
||||||
[ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}"
|
[ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}"
|
||||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
# bare revision number for the release title: firefox-9 -> 9
|
# bare revision number for the release title: firefox-10 -> 10
|
||||||
echo "num=${TAG#firefox-}" >> "$GITHUB_OUTPUT"
|
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"
|
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:-<none>}"
|
||||||
|
# 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
|
- name: Create DRAFT release with all assets
|
||||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
||||||
with:
|
with:
|
||||||
@@ -417,13 +473,7 @@ jobs:
|
|||||||
dl/*.tar.gz
|
dl/*.tar.gz
|
||||||
dl/*.zip
|
dl/*.zip
|
||||||
dl/checksums.txt
|
dl/checksums.txt
|
||||||
body: |
|
dl/source-commit.txt
|
||||||
Patched Firefox 150.0.1 — built on GitHub Actions ($0, no mold).
|
body_path: body.md
|
||||||
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
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -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 <sha> \
|
||||||
|
[--prev-sha <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())
|
||||||
Reference in New Issue
Block a user