#!/usr/bin/env bash # Drive the real editor in a real browser: build the app, run it on a # scratch database, sign in for real, and put a headless Chrome through # fixtures/browser-drive.mjs. Exits non-zero if any check fails. # # Not in .amadan/ci.d/10-gate yet, deliberately, and the reason is # bigger than "the runner needs a Chrome installed". # # Chromium does not start inside the agent sandbox as it is configured # by default — it fails early on something it cannot write or bind. # The exact symptom moves: it has been "socket() failed: Operation not # permitted" and it has been "Failed to create a unique user data # directory for headless". Neither is quoted here as the diagnosis, # because quoting one symptom as though it were the reason is part of # what made the old version of this paragraph read as final. # # BUT IT RUNS FINE WITH THE SANDBOX OFF, and this header said otherwise # for long enough to do damage. It read "cannot start at all", full # stop — and a session that believed it merged a branch describing # itself as impossible to browser-verify, having never tried. A real # WCAG 2.2 SC 1.4.10 failure and four blinded checks went to main behind # that sentence. The sandbox is a setting, not a property of the # machine; a session that needs a browser turns it off for this one # command and gets one. # # So the open question for the gate is smaller than this file used to # claim. Not "can this run here" — it can — but "may a CI step run # unsandboxed", which is a real decision about what CI is allowed to do # and belongs to whoever owns the runner rather than to the branch that # wrote the script. # # Until that is answered it is run by hand, which is worth far more than # not having it: its first run caught an editor that silently discarded # every edit, and the run after this file's own header was corrected # caught five regressions a green gate had passed. # # $DOCS_CHROME overrides which browser to use. set -euo pipefail cd "$(dirname "$0")/.." chrome="${DOCS_CHROME:-}" if [ -z "$chrome" ]; then for candidate in chromium chromium-browser google-chrome google-chrome-stable; do if command -v "$candidate" >/dev/null 2>&1; then chrome="$candidate"; break; fi done fi [ -n "$chrome" ] || { echo "browser-drive: no Chrome found; set \$DOCS_CHROME" >&2; exit 2; } command -v node >/dev/null || { echo "browser-drive: node not on PATH" >&2; exit 2; } port="${DOCS_DRIVE_PORT:-8188}" debug_port="${DOCS_DRIVE_DEBUG_PORT:-9288}" addr="127.0.0.1:${port}" scratch="$(mktemp -d "${TMPDIR:-/tmp}/docs-drive.XXXXXX")" server_pid="" chrome_pid="" cleanup() { [ -n "$chrome_pid" ] && kill "$chrome_pid" 2>/dev/null || true [ -n "$server_pid" ] && kill "$server_pid" 2>/dev/null || true rm -rf "$scratch" 2>/dev/null || true } trap cleanup EXIT export CGO_ENABLED=0 echo "browser-drive: building" go build -o "${scratch}/docs" ./cmd/docs # -seed so there is a document to open; the same development-only path # scripts/run-dev.sh uses, against a database that exists for this run # only. "${scratch}/docs" -env development -seed -addr "$addr" -db "${scratch}/docs.db" > "${scratch}/server.log" 2>&1 & server_pid=$! for _ in $(seq 1 40); do if curl -fsS -o /dev/null "http://${addr}/signin" 2>/dev/null; then break; fi sleep 0.25 done curl -fsS -o /dev/null "http://${addr}/signin" || { echo "browser-drive: server did not start" >&2; cat "${scratch}/server.log" >&2; exit 1; } # Sign in the way a person does. In development the magic link is # logged instead of mailed (internal/docs/auth.go), so the link is read # back out of this run's own log — no stand-in identity, no test-only # door into the app. curl -fsS -X POST -H 'Sec-Fetch-Site: same-origin' \ --data-urlencode 'address=dev@example.com' -o /dev/null "http://${addr}/signin" link="$(grep -o "http://${addr}/auth/verify?token=[A-Za-z0-9_-]*" "${scratch}/server.log" | tail -1)" [ -n "$link" ] || { echo "browser-drive: no sign-in link in the log" >&2; exit 1; } # The link opens a confirm door that spends nothing; the drive presses # its Sign in button (browser-drive.mjs), which is what leaves the # session cookie in the profile it then uses. "$chrome" --headless=new --no-sandbox --disable-gpu --disable-crash-reporter \ --remote-debugging-port="${debug_port}" --user-data-dir="${scratch}/profile" \ --window-size=1280,900 \ about:blank > "${scratch}/chrome.log" 2>&1 & chrome_pid=$! target="" for _ in $(seq 1 40); do target="$(curl -fsS "http://127.0.0.1:${debug_port}/json/list" 2>/dev/null \ | tr ',' '\n' | grep -o 'ws://[^"]*devtools/page/[A-Za-z0-9]*' | head -1 || true)" [ -n "$target" ] && break sleep 0.25 done [ -n "$target" ] || { echo "browser-drive: Chrome did not expose a debugging target" >&2; exit 1; } echo "browser-drive: driving http://${addr}" node fixtures/browser-drive.mjs "$target" "http://${addr}" "$link"