scripts/verify_pages_site.py
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env python3
"""Fail the run when the *published* Pages site is not the Archiver app.
A green ``actions/deploy-pages`` step only means GitHub accepted the artifact;
it says nothing about what the host serves afterwards. On 2026-10-02 the
repository's Pages source was on *Deploy from a branch* (``main`` / ``docs``),
so GitHub's built-in builder ran on the same push, finished two minutes after
the workflow, and replaced the app with its own build of ``docs/`` — five
rendered markdown files and no ``index.html``. ``/Archiver/`` answered the
"File not found … you must provide an index.html" page while every workflow run
and deployment status was green.
This check closes that gap: fetch the URL ``actions/deploy-pages`` reported,
require the app shell (the page that names ``static/archiver-engine.js`` — the
docs-only build and GitHub's 404 page do not), then require a 200 for the files
the app cannot start without. It retries, because a fresh Pages deployment can
take a minute to reach every edge.
verify_pages_site.py https://2archiver.github.io/Archiver/
Exit status 0 means the app is live; 1 names what was served instead and points
at the Pages source setting. Stdlib only.
"""
from __future__ import annotations
import argparse
import sys
import time
import urllib.error
import urllib.request
from urllib.parse import urljoin
USER_AGENT = "archiver-pages-verify/1 (+https://github.com/2archiver/Archiver)"
# The app shell must name the engine script; nothing else that can be published
# to this project path (GitHub's 404 page, the docs-only Jekyll build) contains it.
PAGE_MARKER = "archiver-engine.js"
# Files the boot path needs. Kept in step with scripts/build_pages.py's required
# list by tests/test_pages_deploy.py, which builds the site and checks these exist.
REQUIRED_PATHS = (
"manifest.json",
"favicon.svg",
"archiver-coi-sw.js",
"static/archiver-engine.js",
"static/archiver-prep.js",
"static/archiver-worker.js",
"static/vendor/wllama-3.6.1.wasm",
"static/vendor/wllama-compat-3.6.1.wasm",
)
def _get(url: str, timeout: float, limit: "int | None" = None) -> "tuple[int, bytes]":
"""GET ``url``; return (status, body). Failures come back as status 0."""
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status, (resp.read(limit) if limit else resp.read())
except urllib.error.HTTPError as exc:
return exc.code, b""
except (urllib.error.URLError, OSError, ValueError):
return 0, b""
def check_site(base: str, timeout: float) -> "tuple[list[str], list[str]]":
"""One pass over the site: (lines that were fine, problems found)."""
ok: "list[str]" = []
status, body = _get(base, timeout)
if status != 200:
return ok, [f"{base} answered {status or 'nothing'}, not 200"]
text = body.decode("utf-8", "replace")
if PAGE_MARKER not in text:
return ok, [f"{base} answered 200 but is not the app (no {PAGE_MARKER!r} in the page; "
"the docs-only build and GitHub's 404 page both lack it)"]
ok.append(f"200 {base} (the app shell)")
problems = []
for path in REQUIRED_PATHS:
url = urljoin(base, path)
status, _ = _get(url, timeout, limit=1) # one byte: the vendored wasm is ~20 MB
if status != 200:
problems.append(f"{url} answered {status or 'nothing'}, not 200")
else:
ok.append(f"200 {url}")
return ok, problems
def main(argv: "list[str] | None" = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("base_url", help="the page_url actions/deploy-pages reported")
ap.add_argument("--attempts", type=int, default=20, help="tries before failing (default 20)")
ap.add_argument("--delay", type=float, default=5.0, help="seconds between tries (default 5)")
ap.add_argument("--timeout", type=float, default=15.0, help="per-request timeout in seconds")
args = ap.parse_args(argv)
base = args.base_url if args.base_url.endswith("/") else args.base_url + "/"
attempts = max(1, args.attempts)
problems: "list[str]" = []
for attempt in range(1, attempts + 1):
ok, problems = check_site(base, args.timeout)
if not problems:
print(f"verify_pages_site: {base} serves the app")
for line in ok:
print(f" {line}")
return 0
if attempt < attempts:
print(f"verify_pages_site: attempt {attempt}/{attempts}: {'; '.join(problems)} "
f"— retrying in {args.delay:g}s")
time.sleep(args.delay)
print(f"::error::{base} is not the Archiver app after {attempts} attempt(s): "
f"{'; '.join(problems)}", file=sys.stderr)
print("verify_pages_site: this workflow deployed _site, but the host is serving something "
"else. The usual cause is the Pages source sitting on 'Deploy from a branch', where "
"GitHub's own builder publishes that folder over the workflow artifact. Check "
"Settings → Pages → Build and deployment → Source → GitHub Actions, then re-run the "
"Pages workflow. See docs/PAGES.md.", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())