From ce68224a480236bcbb10a4f4b1bbdb00988e2eb7 Mon Sep 17 00:00:00 2001 From: Matthew Demidoff Date: Sat, 5 Sep 2026 21:44:36 +0200 Subject: [PATCH] ci: boot the runner image and run playwright against it CI built the production runner image on every push and then threw it away, running the Playwright scenarios against the dev server instead. Nonce CSP, HSTS, the standalone server and migrate-on-boot exist only in the image, so the artifact that ships was the one thing the end-to-end suite never touched. Tag the runner build, boot it on the host network against the Postgres and Redis services with the five secrets validateConfig() requires (Cloudflare's always-pass Turnstile keypair, so form submissions can be exercised later), wait on /api/health/ready, and point Playwright at it via PLAYWRIGHT_BASE_URL. Container logs are dumped on failure and the container is always removed. Locally nothing changes: without PLAYWRIGHT_BASE_URL the config still starts the dev server on 3100. --- .github/workflows/security.yml | 32 +++++++++++++++++++++++++++++++- README.md | 10 ++++++++-- playwright.config.ts | 27 ++++++++++++++------------- 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 15b9be0..7eaab1a 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -56,10 +56,40 @@ jobs: - run: npm run test:run - run: npm run migrate:smoke - run: npm run build - - run: docker build --target runner . + - run: docker build --target runner -t auth-runner:ci . - run: docker build --target worker . + # The image that ships is the one under test: nonce CSP, HSTS, the + # standalone server and migrate-on-boot only exist in production mode. + # --network host lets the container reach the service containers on + # localhost and lets Playwright reach it on 127.0.0.1:3000. The + # Turnstile pair is Cloudflare's documented always-pass test keypair. + - name: Boot the runner image + run: | + docker run -d --name auth-runner --network host \ + -e DATABASE_URL -e REDIS_URL -e OIDC_PRIVATE_KEY_PEM -e OIDC_KEY_ID \ + -e AUTH_BASE_URL=http://127.0.0.1:3000 \ + -e OAUTH_CSRF_SECRET=ci-oauth-csrf-secret \ + -e TELEGRAM_BOT_WEBHOOK_SECRET=ci-telegram-webhook-secret \ + -e TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA \ + -e TURNSTILE_SITE_KEY=1x00000000000000000000AA \ + auth-runner:ci - run: npx playwright install --with-deps chromium + - name: Wait for the runner to be ready + run: | + for i in $(seq 1 45); do + if curl -fsS http://127.0.0.1:3000/api/health/ready; then exit 0; fi + sleep 2 + done + echo "runner image did not become ready" >&2 + docker logs auth-runner + exit 1 - run: npm run test:e2e + env: + PLAYWRIGHT_BASE_URL: http://127.0.0.1:3000 + - if: failure() + run: docker logs auth-runner + - if: always() + run: docker rm -f auth-runner sdk: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 7b98d54..cf15a47 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,14 @@ With `REDIS_URL` set, rate-limit counters live in Redis and are shared across test files within one run, so a test that exercises a rate-limited path must use keys unique to itself. -CI (`.github/workflows/security.yml`) runs the unit and integration suites, -the Playwright end-to-end scenarios against Postgres and Redis services, `npm audit`, +`npm run test:e2e` starts the dev server on port 3100 by itself. Set +`PLAYWRIGHT_BASE_URL` to point the suite at an already-running server +instead (CI sets it to the booted production image). + +CI (`.github/workflows/security.yml`) runs the unit and integration suites +against Postgres and Redis services, boots the production runner image +against the same services and runs the Playwright end-to-end scenarios +against it, and runs `npm audit`, the migration smoke test, the production build, and both Docker image builds on every push. The `sdk` job separately builds and type-checks the Node SDK. diff --git a/playwright.config.ts b/playwright.config.ts index a23641c..e4d3b23 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,22 +1,23 @@ import { defineConfig, devices } from "@playwright/test"; +// CI points PLAYWRIGHT_BASE_URL at the booted production image and manages +// that process itself; locally the dev server on 3100 is started (or reused). +const baseURL = process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:3100"; + export default defineConfig({ testDir: "./tests/e2e", timeout: 30_000, use: { - baseURL: "http://127.0.0.1:3100", + baseURL, trace: "on-first-retry", }, - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - ], - webServer: { - command: "npm run dev -- -H 127.0.0.1 -p 3100", - url: "http://127.0.0.1:3100/login", - reuseExistingServer: !process.env.CI, - timeout: 120_000, - }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], + webServer: process.env.PLAYWRIGHT_BASE_URL + ? undefined + : { + command: "npm run dev -- -H 127.0.0.1 -p 3100", + url: "http://127.0.0.1:3100/login", + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, });