Skip to content

ci(auth-contract): Layer-2 PR-gate Playwright on docker-compose stack#207

Merged
mastermanas805 merged 2 commits into
masterfrom
ci/auth-contract-compose-pw
May 30, 2026
Merged

ci(auth-contract): Layer-2 PR-gate Playwright on docker-compose stack#207
mastermanas805 merged 2 commits into
masterfrom
ci/auth-contract-compose-pw

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Spins up a minimal postgres + redis + api docker-compose stack from THIS PR's source (api/docker-compose.ci.yml) and runs three Playwright contract assertions against localhost:8080 on every PR.
  • Mirrors the Layer-1 prod-target spec (instanode-web/e2e/auth-contract.spec.ts) so the AUTH-004 CORS-credentials regression class is caught PRE-merge, not 5 minutes POST-deploy.
  • Workflow: .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.dev and api.instanode.dev are both public. Fix: a dedicated chromium-compose-pna Playwright 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,5174 when ENVIRONMENT=development (router.go ~L237). The compose stack sets ENVIRONMENT=development and the spec stubs http://localhost:5173 as the test document origin. No new prod-allowlist entries.

Rule 25 observability

Workflow uploads a 1-line JSONL artifact auth-contract-gate-signal on 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)

Symptom:        AUTH-004 CORS class — preflight loses ACAC or web origin
                slips out of 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 / cross-origin browser POST / 202
                envelope) plus the existing Layer-1 spec
Live verified:  N/A — this PR ships the test, not a code change. Local
                verification: docker compose up against current master,
                /healthz returns commit_id=ci-local + migration_count=63,
                3 Playwright tests pass in 435ms.

Test plan

  • Build the compose stack locally: docker compose -f api/docker-compose.ci.yml up -d --build
  • Confirm /healthz returns {"ok":true,...} (got it on 1st probe attempt)
  • npx playwright test tests/auth-contract-local.spec.ts --project=chromium-compose-pna3 passed (435ms)
  • CI: this workflow runs on this PR's first push — verify it goes green
  • CI: a follow-up "deliberate regression" PR (drop AllowCredentials) reds this gate before merge

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

mastermanas805 and others added 2 commits May 30, 2026 21:39
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.
@mastermanas805 mastermanas805 merged commit 3a4eb79 into master May 30, 2026
16 checks passed
@mastermanas805 mastermanas805 deleted the ci/auth-contract-compose-pw branch May 30, 2026 16:41
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