Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions .github/scripts/preview_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Comment on a pull request with a preview link for each tour it changes.

Driven by the `check_run` event of a finished Cloudflare Workers build: reads
the event from $GITHUB_EVENT_PATH, works out which tour JSON the pull request
changes, and posts a single comment (updated in place on later builds) playing
each of them from the branch's preview deployment.
"""
import json
import os
import re
import sys
import urllib.error
import urllib.request
from typing import Any

API_BASE = "https://api.github.com"
LIFE_URL = "https://beta.onezoom.org/life?tour=/tour/remote.html"
# Identifies our own comment so that later builds edit it rather than add another.
MARKER = "<!-- tour-preview-links -->"
# Directories build.py leaves out of _site, so nothing in them gets served.
UNPUBLISHED = (".github/", "deployment/", "_site/")


def worker_from_summary(summary: str) -> str | None:
"""The Cloudflare worker URL this build is deployed to."""
for label in ("Preview Alias URL", "Preview URL"):
pattern = rf"{label}:\s*https://([a-z0-9-]+)\.onezoom\.workers\.dev"
match = re.search(pattern, summary, re.IGNORECASE)
if match:
return match.group(1)
return None


def comment_body(worker: str, changed_tours: list[str]) -> str | None:
if not changed_tours:
return None
lines = [f"- [`{t}`]({LIFE_URL}/{worker}/{t})" for t in changed_tours]
return "\n".join([MARKER, "### Tour previews", ""] + lines) + "\n"


def api(path: str, method: str = "GET", body: dict | None = None) -> Any:
"""Call the GitHub API, returning the parsed body."""
url = API_BASE + path
payload = json.dumps(body).encode("utf-8") if body is not None else None

request = urllib.request.Request(url, data=payload, method=method)
request.add_header("Authorization", f"Bearer {os.environ['GITHUB_TOKEN']}")
request.add_header("Accept", "application/vnd.github+json")
request.add_header("X-GitHub-Api-Version", "2026-03-10")
if payload is not None:
request.add_header("Content-Type", "application/json; charset=utf-8")

try:
with urllib.request.urlopen(request) as response:
raw = response.read()
return json.loads(raw) if raw else None
except urllib.error.HTTPError as e:
print(
f"{method} {url} failed: {e}\n"
f"{e.read().decode('utf-8', errors='replace')}",
file=sys.stderr,
)
raise


def changed_tour_paths(repo: str, number: int) -> list[str]:
"""Tour identifiers for the changed files the preview deployment will serve."""
return sorted({
changed["filename"][: -len(".json")]
# first page only so won't show more than 100 changed files
for changed in api(f"/repos/{repo}/pulls/{number}/files?per_page=100")
if changed["status"] != "removed"
and changed["filename"].endswith(".json")
and not changed["filename"].startswith(UNPUBLISHED)
})


def existing_comment(repo: str, number: int) -> dict | None:
for comment in api(f"/repos/{repo}/issues/{number}/comments?per_page=100"):
if MARKER in (comment.get("body") or ""):
return comment
return None


def main() -> None:
repo = os.environ["GITHUB_REPOSITORY"]
with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as f:
check_run = json.load(f)["check_run"]

worker = worker_from_summary((check_run.get("output") or {}).get("summary") or "")
if not worker:
print("No preview URL in the build summary, nothing to comment.")
return

for pull in check_run.get("pull_requests") or []:
number = pull["number"]
changed_tours = changed_tour_paths(repo, number)
body = comment_body(worker, changed_tours)
comment = existing_comment(repo, number)

if body:
if comment:
api(f"/repos/{repo}/issues/comments/{comment['id']}",
"PATCH", {"body": body})
print(f"Updated the comment on #{number}: {', '.join(changed_tours)}")
else:
api(f"/repos/{repo}/issues/{number}/comments",
"POST", {"body": body})
print(f"Commented on #{number}: {', '.join(changed_tours)}")
elif comment:
api(f"/repos/{repo}/issues/comments/{comment['id']}", "DELETE")
print(f"Deleted the comment on #{number}")
else:
print(f"No tour JSON changed in #{number}")


if __name__ == "__main__":
main()
27 changes: 27 additions & 0 deletions .github/workflows/preview-links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Comment tour preview links

on:
check_run:
types: [completed]

permissions:
contents: read
pull-requests: write

jobs:
comment:
if: >-
github.event.check_run.conclusion == 'success' &&
startsWith(github.event.check_run.name, 'Workers Builds')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Comment with a preview link per changed tour
env:
GITHUB_TOKEN: ${{ github.token }}
run: python .github/scripts/preview_links.py
Loading