ci(docs): fix screenshot capture against the #1335 C2 install guard#1427
Merged
Conversation
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.
7 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
docs-screenshots-capture.ymlworkflow has been silently failingto refresh installer screenshots (and
panel-01-login) since thescreenshot pipeline first landed (#1333). The last run on PR #1426
(run 26143228051)
made the failure visible:
Two distinct root causes, both fixed here:
refuses to start over an installed panel: when
web/config.phpexists it short-circuits every
/install/?step=NURL to a static409 "already installed" page with no
<form>. The dev stack'sdocker/php/web-entrypoint.shalways createsconfig.phponfirst boot, so the
waitFor: 'form'selector reliably times out.panel-01-loginaborted by post-login redirect.page.login.phpemits<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 inlineredirect aborted Playwright's
page.goto()withnet::ERR_ABORTED.Changes
docs/scripts/capture.mjsaccepts a newCAPTURE_ROUTESenv var(
all/panel/install, defaultall). The login routemoves into
PANEL_PUBLIC_ROUTESand is captured from a fresh,cookie-less browser context BEFORE the authenticated panel pass.
.github/workflows/docs-screenshots-capture.ymlruns the capturein two passes: panel first, then host-side stash of
web/config.php→/tmp(the bind-mount means the containersees the rename), then install routes, then
if: always()restoreso a failed install pass still leaves the panel in a bootable
state for
docker compose down.docs/README.mddocuments the new env var and the manualstash/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.mjscovers that follow-up — out ofscope here.
Test plan
python3 -c "import yaml; yaml.safe_load(open(...))").node --check docs/scripts/capture.mjspasses locally.safe-to-screenshotlabel on a PRthat touches the install wizard / panel chrome and confirms
that
docs-screenshots-capture.ymlproduces non-stale PNGsfor both
docs/src/assets/auto/install/*.pnganddocs/src/assets/auto/panel/*.png(includingpanel-01-login).cd docs && npm run captureproduces the panelset with
web/config.phpin place, and rerunning with thedocs/README.md-documented stash dance fills in the install
set.