Ctrl K
ComunidadGuías…

scripts/verify_pages_site.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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())