Skip to content

ci(docs): fix screenshot capture against the #1335 C2 install guard#1427

Merged
rumblefrog merged 1 commit into
mainfrom
fix/screenshot-capture-install-c2-guard
May 20, 2026
Merged

ci(docs): fix screenshot capture against the #1335 C2 install guard#1427
rumblefrog merged 1 commit into
mainfrom
fix/screenshot-capture-install-c2-guard

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Summary

The docs-screenshots-capture.yml workflow has been silently failing
to refresh installer screenshots (and panel-01-login) since the
screenshot pipeline first landed (#1333). The last run on PR #1426
(run 26143228051)
made the failure visible:

[capture] FAILED .../install/?step=1: locator('form') timeout
[capture] FAILED .../install/?step=2: locator('form') timeout
[capture] FAILED .../install/?step=5: locator('form') timeout
[capture] FAILED .../index.php?p=login: net::ERR_ABORTED

Two distinct root causes, both fixed here:

  1. Install routes blocked by the Post-#1332 install-wizard human-flow audit: panel-takeover paths via localhost host-check bypass + /install/ replayable on installed panels, plus 8 UX gaps #1335 C2 guard. The wizard
    refuses to start over an installed panel: when web/config.php
    exists it short-circuits every /install/?step=N URL to a static
    409 "already installed" page with no <form>. The dev stack's
    docker/php/web-entrypoint.sh always creates config.php on
    first boot, so the waitFor: 'form' selector reliably times out.
  2. panel-01-login aborted by post-login redirect.
    page.login.php emits <script>window.location.href='index.php'</script>
    whenever a session already exists. The single capture pass logged
    in first and then visited /index.php?p=login, where the inline
    redirect aborted Playwright's page.goto() with net::ERR_ABORTED.

Changes

  • docs/scripts/capture.mjs accepts a new CAPTURE_ROUTES env var
    (all / panel / install, default all). The login route
    moves into PANEL_PUBLIC_ROUTES and is captured from a fresh,
    cookie-less browser context BEFORE the authenticated panel pass.
  • .github/workflows/docs-screenshots-capture.yml runs the capture
    in two passes: panel first, then host-side stash of
    web/config.php/tmp (the bind-mount means the container
    sees the rename), then install routes, then if: always() restore
    so a failed install pass still leaves the panel in a bootable
    state for docker compose down.
  • docs/README.md documents the new env var and the manual
    stash/restore for local-dev install captures.

Step 5 of the wizard (admin form) still redirects to step 2 on a
cold deep-link because steps 3-6 are POST-handoff-gated; the
existing TODO note in capture.mjs covers that follow-up — out of
scope here.

Test plan

  • Workflow YAML parses cleanly (verified locally via
    python3 -c "import yaml; yaml.safe_load(open(...))").
  • node --check docs/scripts/capture.mjs passes locally.
  • A maintainer applies the safe-to-screenshot label on a PR
    that touches the install wizard / panel chrome and confirms
    that docs-screenshots-capture.yml produces non-stale PNGs
    for both docs/src/assets/auto/install/*.png and
    docs/src/assets/auto/panel/*.png (including panel-01-login).
  • Local repro: cd docs && npm run capture produces the panel
    set with web/config.php in place, and rerunning with the
    docs/README.md-documented stash dance fills in the install
    set.

The capture workflow on every PR labelled `safe-to-screenshot`
silently failed to refresh the install-wizard screenshots and the
panel-01-login screenshot since the screenshot pipeline first
landed (#1333). Symptom in the latest run (PR #1426 — actions run
26143228051):

    [capture] FAILED .../install/?step=1: locator('form') timeout
    [capture] FAILED .../install/?step=2: locator('form') timeout
    [capture] FAILED .../install/?step=5: locator('form') timeout
    [capture] FAILED .../index.php?p=login: net::ERR_ABORTED

Two distinct root causes.

1. Install wizard surfaces. The wizard's #1335 C2 guard refuses to
   start over an installed panel: when `web/config.php` exists it
   short-circuits every install URL to a static 409 "already
   installed" page that has no `<form>`. The dev stack's entrypoint
   (`docker/php/web-entrypoint.sh`) always creates `config.php` on
   first boot, so every `/install/?step=N` capture lands on the 409
   page and the `waitFor: 'form'` selector times out.

2. `panel-01-login`. `page.login.php` emits
   `<script>window.location.href='index.php'</script>` whenever a
   session already exists. The single panel-capture pass logged in
   first and then visited `/index.php?p=login`, where the inline
   redirect aborted Playwright's `page.goto()` with
   `net::ERR_ABORTED`.

Fix shape:

- `docs/scripts/capture.mjs` honours a new `CAPTURE_ROUTES`
  env var (`all` / `panel` / `install`, default `all`) so CI can
  invoke the script twice with `web/config.php` stashed around the
  install pass. The login route is split into `PANEL_PUBLIC_ROUTES`
  and captured from a fresh, cookie-less context BEFORE the
  authenticated pass — no session means no inline redirect.

- `.github/workflows/docs-screenshots-capture.yml` runs three new
  steps in place of the single `Capture screenshots` step: capture
  panel routes, host-side `mv web/config.php → /tmp` (bind-mount
  means the container sees the rename), capture install routes,
  then `if: always()` restore so a failed install pass still leaves
  the panel in a bootable state for tear-down.

- `docs/README.md` documents the new env var + the manual
  stash/restore dance for local-dev install captures.

Step 5 of the wizard (admin form) still redirects to step 2 on a
cold deep-link because steps 3-6 are POST-handoff-gated; the
existing TODO note in `capture.mjs` covers that follow-up, out of
scope here.
@rumblefrog rumblefrog added this pull request to the merge queue May 20, 2026
Merged via the queue into main with commit 25e6d9b May 20, 2026
2 checks passed
@rumblefrog rumblefrog deleted the fix/screenshot-capture-install-c2-guard branch May 20, 2026 05:48
rumblefrog added a commit to iBoonie/sourcebans-pp that referenced this pull request May 23, 2026
… trap) (sbpp#1432)

sbpp#1427 introduced the stash/restore dance around the install-pass
capture so the wizard's sbpp#1335 C2 guard (refuses to start over an
installed panel) actually exposes step 1-5 instead of the static 409
"already installed" page. PR 1430 was the first PR to actually
exercise that pair after sbpp#1427 landed and hit:

    mv: cannot move '/tmp/sbpp-config-stash.php' to 'web/config.php':
        Operation not permitted

Root cause: the dev container's entrypoint
(`docker/php/web-entrypoint.sh`) runs as root, so its `cat > config.php`
creates `web/config.php` owned `root:root` (UID 0:0) on the bind-
mounted host filesystem. On GHA ubuntu-24.04 runners `/tmp` and
`/home/runner/work/` are on the same filesystem, so the stash's
`mv web/config.php /tmp/...` is a same-fs atomic `rename(2)` that
*preserves* ownership — the file lands in `/tmp` still owned by root.
`/tmp` carries the sticky bit, and per `rename(2)`'s EPERM rule the
runner (UID 1001) can't move a non-owned file out of a sticky
directory. The restore fails before it ever touches `web/`.

Fix: stash to `pr-head/.sbpp-config-stash.php` — a sibling of `web/`
on the same filesystem, in a runner-owned, sticky-bit-free directory.
Same-fs atomic rename works both ways regardless of which UID the
container created the file as. The auto-commit step's
`file_pattern: 'docs/src/assets/auto/**'` already constrains commits
to the screenshot diff, so a stale stash never leaks into the PR.

`docs/README.md`'s local-dev dance carried the same `/tmp` shape and
is updated in lockstep — Docker-as-root on a developer's machine
hits the same trap. `.gitignore` adds `/.sbpp-config-stash.php` so
an interrupted local run can't accidentally commit the panel's DB
credentials.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant