ci(auth-contract): Layer-2 PR-gate Playwright on docker-compose stack#207
Merged
Conversation
Spins up postgres + redis + api (built from the PR's source) via
api/docker-compose.ci.yml, waits for /healthz, then runs the three-test
auth-contract spec in api/e2e/browser/tests/auth-contract-local.spec.ts
against localhost:8080.
Mirrors the Layer-1 prod-target assertions
(instanode-web/e2e/auth-contract.spec.ts):
1. preflight OPTIONS /auth/exchange returns ACAO=<web_origin> + ACAC=true
2. real Chromium cross-origin POST /auth/exchange completes the CORS
traversal (no "Failed to fetch")
3. POST /auth/email/start returns 202 {ok:true}
Layer-1 catches the AUTH-004 regression class POST-deploy against prod;
Layer-2 catches it PRE-merge against the PR's actual binary. Two new
dimensions of coverage: (a) the real api router/CORS/middleware stack as
it would ship, not just unit-test mocks; (b) a real Chromium fetch
inside the PR window, not after kubectl set image.
Anti-goals: no worker / provisioner / NATS / mongo / object-store — the
auth surface doesn't need them, and keeping the compose surface to
postgres+redis+api holds the per-PR wall clock under ~5 min (compose
build dominates).
Trickiest decision: Chromium's Local / Private Network Access checks
block even loopback→loopback fetches as "Permission denied for loopback
address space" when there's no Access-Control-Allow-Private-Network
header. PROD does not hit this case (instanode.dev → api.instanode.dev
are both public addresses, not loopback). Workaround: a dedicated
chromium-compose-pna Playwright project that launches Chromium with
--disable-features=LocalNetworkAccessChecks,PrivateNetworkAccess*. The
disable is strictly a localhost shim — it does NOT weaken the CORS
contract under test, which is the api's ACAO + ACAC response. The other
projects in playwright.config.ts are untouched.
CORS allow-origin for the test: the api's router (router.go ~L237)
unlocks http://localhost:5173,3000,5174 when ENVIRONMENT=development.
The compose stack sets ENVIRONMENT=development, the spec stubs the test
origin at http://localhost:5173 via page.route(), so no new prod allow-
list entries land on the api side.
Per rule 25 observability: the workflow uploads a 1-line JSONL artifact
("gate":"auth-contract-compose-pw", pr, sha, status, ts) on every run
so "did the gate fire on the last N PRs?" is answerable without log
scraping. A compose-only CI signal is not a prod metric, so no NR
alert/dashboard is required by rule 25's literal text.
Local verification:
docker compose -f api/docker-compose.ci.yml up -d --build → healthy
/healthz commit_id=ci-local, migration_count=63, status=ok
npx playwright test tests/auth-contract-local.spec.ts \
--project=chromium-compose-pna --reporter=list → 3 passed (435ms)
Coverage block:
Symptom: AUTH-004 CORS regression class — preflight loses ACAC
or web origin slips out of the api allowlist, browser
fetch reports "Failed to fetch", login wedges
Enumeration: rg -F 'AllowCredentials|AllowOrigins|corsAllowOrigins'
internal/router/ internal/middleware/
Sites found: 1 router CORS block (router.go ~L237-283), 1
PreflightAllowlist middleware
Sites touched: 0 in this PR — this is the GATE, not the fix. The
gate fires when EITHER site regresses.
Coverage test: the three new asserts in auth-contract-local.spec.ts
(preflight headers / browser fetch / 202 envelope) plus
the existing Layer-1 spec — both must stay green for
the contract to be ungrokable from underneath.
Live verified: not applicable — this PR ships the test, not a code
change to the system under test. Verification is that
the test itself passes against an unmodified api built
from current master.
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
api/docker-compose.ci.yml) and runs three Playwright contract assertions againstlocalhost:8080on every PR.instanode-web/e2e/auth-contract.spec.ts) so the AUTH-004 CORS-credentials regression class is caught PRE-merge, not 5 minutes POST-deploy..github/workflows/auth-contract-compose-pw.yml. Wall clock budget: ~5 min/PR (compose build dominates).Trickiest decision
Chromium's Local / Private Network Access checks block even loopback→loopback
fetch()("Permission denied for loopback address space") regardless of correct CORS headers. Prod never hits this —instanode.devandapi.instanode.devare both public. Fix: a dedicatedchromium-compose-pnaPlaywright project that launches Chromium with--disable-features=LocalNetworkAccessChecks,PrivateNetworkAccess*. Strictly a localhost shim; the CORS contract under test (ACAO + ACAC + 202 envelope) is untouched.CORS allowlist exposure
The api's router unlocks
http://localhost:5173,3000,5174whenENVIRONMENT=development(router.go ~L237). The compose stack setsENVIRONMENT=developmentand the spec stubshttp://localhost:5173as the test document origin. No new prod-allowlist entries.Rule 25 observability
Workflow uploads a 1-line JSONL artifact
auth-contract-gate-signalon every run (gate,pr,sha,status,ts) so "did the gate fire on the last N PRs?" is answerable without log scraping. Compose-only CI signal is not a prod metric, so no NR alert/dashboard required by rule 25's literal text.Coverage block (rule 17)
Test plan
docker compose -f api/docker-compose.ci.yml up -d --build/healthzreturns{"ok":true,...}(got it on 1st probe attempt)npx playwright test tests/auth-contract-local.spec.ts --project=chromium-compose-pna→ 3 passed (435ms)Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com