--- phase: 08-gitea-ci plan: 03 type: execute wave: 3 depends_on: ['08-02'] files_modified: - .gitea/workflows/ci.yml autonomous: false requirements: [CI-01] must_haves: truths: - "On a PR to main, a harness job brings up the full dev stack inside the runner: MariaDB + API dev server (DEV_AUTH_BYPASS=true, :3000) + PWA Vite dev server (:5173, started by Playwright's own webServer)" - 'The harness step waits for BOTH the API (:3000/health) and the PWA Vite server (:5173) to accept connections before Playwright launches, so it does not flake on startup races' - 'The Phase 7 Playwright specs run UNCHANGED across both device profiles (iPhone 14/WebKit + Pixel 7/Chromium) and a failure blocks merge' - 'On harness failure, test-results/ (traces/screenshots/videos) upload as a CI artifact via the gitea-upload-artifact fork' artifacts: - path: '.gitea/workflows/ci.yml' provides: 'PR-triggered harness job running the Phase 7 mobile harness' contains: 'test:e2e' key_links: - from: '.gitea/workflows/ci.yml (harness job)' to: 'apps/pwa/e2e/global-setup.ts' via: 'DEV_AUTH_BYPASS + PLAYWRIGHT_BASE_URL + DB_* env → pnpm test:e2e' pattern: 'DEV_AUTH_BYPASS' - from: 'harness job' to: 'API :3000' via: 'background node dist/index.js + curl /health readiness loop' pattern: 'localhost:3000/health' --- Add the harness job to `.gitea/workflows/ci.yml`: bring up the dev stack inside the runner (MariaDB → migrate → API background process with DEV_AUTH_BYPASS=true on :3000 → Playwright starts Vite on :5173 itself) and run the Phase 7 mobile Playwright harness UNCHANGED across both device profiles, uploading traces on failure. This is the v1.1 extension of CI-01 (ROADMAP criteria 3 + 4) and Pitfall "dev-stack readiness races". Purpose: Catch mobile-only regressions on every PR with no developer's host stack required (Phase 7 success criterion 4). CI owns ONLY stack bring-up + readiness waits — never spec content (D-01/D-02; 08-CONTEXT phase boundary). Output: a `harness` job in ci.yml gated on `pull_request → main`. The orchestration order in 08-RESEARCH §Dev-Stack Bring-Up is mandatory and SEQUENTIAL within the job. Read 08-01-SUMMARY for the runner-mode DB path and the WebKit-deps / upload-artifact answers. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-gitea-ci/08-RESEARCH.md @.planning/research/PITFALLS.md @.planning/phases/08-gitea-ci/08-01-SUMMARY.md @apps/pwa/playwright.config.ts @apps/pwa/e2e/global-setup.ts @apps/pwa/vite.config.ts - `.gitea/workflows/ci.yml` (EXTENDED — adds the harness job; created in Plan 02) Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.config.ts — do NOT modify these files): - `playwright.config.ts`: `reuseExistingServer: !process.env.CI` → with CI=true, Playwright STARTS Vite itself (`pnpm --filter @familysync/pwa dev`, :5173). `retries: 2`, `workers: 1`, `reporter: 'github'` are all gated on `process.env.CI`. Two projects: `iphone` (WebKit) + `pixel` (Chromium), both `serviceWorkers: 'block'`. - `reporter: 'github'` likely emits invisible output in Gitea (08-RESEARCH Pitfall 5 / D-06). Override the reporter at the CI invocation: pass `--reporter=list,html` (e.g. `pnpm test:e2e -- --reporter=list,html`) OR confirm from 08-01-SUMMARY whether Gitea rendered annotations; if it did, the override is harmless. Do NOT edit playwright.config.ts. - `global-setup.ts`: FAILS CLOSED — throws if `NODE_ENV=production` OR if `DEV_AUTH_BYPASS !== 'true'`. It polls `${PLAYWRIGHT_BASE_URL}/health` (via the Vite proxy → :3000), then gates `/api/me` (must be 200 → proves DEV_AUTH_BYPASS reached the API), then mysql2-seeds calendar id=10 + lists for user 1. It reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (DB_HOST default 127.0.0.1). - `vite.config.ts`: dev proxy `/health`, `/api`, `/callback` → http://localhost:3000. So PLAYWRIGHT_BASE_URL=http://localhost:5173 reaches the API health endpoint through the proxy. - Root `test:e2e` = `pnpm --filter @familysync/pwa test:e2e` = `playwright test`. - API start: `dev` script is `node --watch dist/index.js` and needs a prior build. In CI run `pnpm --filter @familysync/api build` (tsc → dist/index.js) then `node apps/api/dist/index.js &` (no --watch; the watcher is irrelevant in CI — Claude's Discretion in D + 08-RESEARCH Pattern 3). Pass DEV_AUTH_BYPASS=true INLINE on the node line (Pitfall 8 — env inheritance across `&` steps is not guaranteed). - `npx playwright install --with-deps webkit chromium` must run from `apps/pwa` (where @playwright/test lives). Playwright explicitly says do NOT cache browser binaries (08-RESEARCH). - DEV_AUTH_BYPASS user 1 has no CalDAV credential → harness verifies layout/flows, not live event-create (project memory). Specs already account for this; no change. Task 1: Add the harness job — DB + migrate + API background process + readiness .gitea/workflows/ci.yml - .planning/phases/08-gitea-ci/08-RESEARCH.md (§Dev-Stack Bring-Up — the numbered 1..8 sequence is canonical; §Pattern 3 API background process; Pitfall 8 DEV_AUTH_BYPASS inline) - .planning/phases/08-gitea-ci/08-01-SUMMARY.md (runner-mode DB path; WebKit deps y/n) - apps/pwa/e2e/global-setup.ts (fail-closed guards; readiness order) Add a `harness` job to ci.yml: `runs-on: self-hosted`, `if: github.event_name == 'pull_request'` (parallel with fast-checks + api — D-03; no `needs:`). DB bring-up: SAME runner-mode branch as Plan 02's api job (services: mariadb: for Docker mode with DB_HOST=mariadb, or `docker run -d` + readiness loop for host mode with DB_HOST=127.0.0.1). Set job env: DB_PORT 3306, DB_USER familysync, DB_PASSWORD testpass, DB_NAME familysync (throwaway creds). Include the explicit healthcheck.sh readiness loop before migrate (Pitfall 11; never mysqladmin). Steps, in this exact order (08-RESEARCH §Dev-Stack Bring-Up 1..6): 1. `uses: actions/checkout@v4`; `uses: actions/setup-node@v4` (node 22); `corepack enable pnpm`. 2. `run: pnpm install --frozen-lockfile`. 3. (after DB ready) `run: pnpm --filter @familysync/api db:migrate` with DB_* env (drizzle-kit migrate; never push). 4. `run: pnpm --filter @familysync/api build` (produces dist/index.js — Pitfall 4). 5. Start API as a background process with DEV_AUTH_BYPASS INLINE: `NODE_ENV=development DEV_AUTH_BYPASS=true DB_HOST=$DB_HOST DB_PORT=3306 DB_USER=familysync DB_PASSWORD=testpass DB_NAME=familysync node apps/api/dist/index.js & echo $! > /tmp/api.pid` (Pitfall 8). NODE_ENV must be `development` (not production — global-setup refuses; not test — dev-bypass activation checks development per 08-RESEARCH note). 6. Wait for API :3000: a curl retry loop `until curl -sf http://localhost:3000/health` with a ~60s deadline; on timeout, `kill $(cat /tmp/api.pid)` and `exit 1`. This step-level wait (D-02) ensures the API is up BEFORE Playwright starts Vite — separate from and earlier than global-setup's own poll. grep -q "harness" .gitea/workflows/ci.yml && grep -q "node apps/api/dist/index.js" .gitea/workflows/ci.yml && grep -q "DEV_AUTH_BYPASS=true node" .gitea/workflows/ci.yml && grep -q "localhost:3000/health" .gitea/workflows/ci.yml && grep -q "db:migrate" .gitea/workflows/ci.yml && ! grep -q "db:push" .gitea/workflows/ci.yml && echo HARNESS_STACK_OK The harness job brings up MariaDB (per runner mode), migrates, builds the API, starts it as a background process with DEV_AUTH_BYPASS=true passed inline on the node line, and waits for :3000/health before continuing. Task 2: Add Playwright install + run (both profiles) + artifact upload on failure .gitea/workflows/ci.yml - apps/pwa/playwright.config.ts (CI gating: reuseExistingServer, reporter:'github', both projects) - .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 5 Playwright harness; Pitfall 5 reporter override; Pitfall 6 upload-artifact fork) - .planning/phases/08-gitea-ci/08-01-SUMMARY.md (WebKit deps y/n; upload-artifact fork y/n; did Gitea render 'github' reporter annotations?) Continue the `harness` job (08-RESEARCH §Dev-Stack Bring-Up 7..8): 7. Install browsers: `run: npx playwright install --with-deps webkit chromium` with `working-directory: apps/pwa`. (If 08-01-SUMMARY showed WebKit deps cannot install on this runner, record that as a phase blocker in the SUMMARY — do NOT silently drop the iphone profile; D-05 requires BOTH profiles. WebKit feasibility is a hard CI-01 input.) 8. Run the harness: `run: pnpm test:e2e -- --reporter=list,html` (the `--reporter=list,html` overrides the config's CI `'github'` reporter which renders invisibly in Gitea — Pitfall 5; skip the override only if 08-01-SUMMARY confirmed Gitea renders 'github' annotations, in which case it is harmless to keep). env on this step: `CI: 'true'`, `PLAYWRIGHT_BASE_URL: http://localhost:5173`, `DEV_AUTH_BYPASS: 'true'`, `NODE_ENV: development`, plus DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (global-setup seeds the DB directly via mysql2). CI=true makes Playwright start Vite itself (:5173) and use retries:2/workers:1; the run covers both `iphone` and `pixel` projects by default (no --project filter). 9. Upload artifacts on failure: a final step `if: failure()` `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` (NEVER actions/upload-artifact@v4 — GHES-blocked on Gitea, Pitfall 6) with `name: playwright-traces-${{ github.run_id }}`, `path: apps/pwa/test-results/`, `retention-days: 14` (D-06). Add a final `if: always()` step to `kill $(cat /tmp/api.pid) 2>/dev/null || true` to clean up the API background process. Do NOT modify playwright.config.ts, global-setup.ts, vite.config.ts, or any spec — CI owns bring-up only (D-01/D-02; phase boundary). grep -q "playwright install --with-deps webkit chromium" .gitea/workflows/ci.yml && grep -q "test:e2e" .gitea/workflows/ci.yml && grep -q "PLAYWRIGHT_BASE_URL: http://localhost:5173" .gitea/workflows/ci.yml && grep -q "ChristopherHX/gitea-upload-artifact@v4" .gitea/workflows/ci.yml && ! grep -q "actions/upload-artifact@v4" .gitea/workflows/ci.yml && git diff --quiet -- apps/pwa/playwright.config.ts apps/pwa/e2e/global-setup.ts apps/pwa/vite.config.ts && echo HARNESS_RUN_OK The harness job installs webkit+chromium with deps, runs pnpm test:e2e (CI=true, DEV_AUTH_BYPASS=true, base URL :5173, DB env) across both profiles with a list,html reporter override, and uploads test-results/ on failure via the gitea fork. No Phase 7 harness file is modified. Task 3: Verify the harness job on the PR The harness job (Tasks 1–2) added to ci.yml, exercised on the open PR to main. 1. Push the branch; on the PR, confirm the `harness` job runs alongside fast-checks + api. 2. Confirm it brings up MariaDB → migrate → API (:3000) → Playwright starts Vite (:5173) → both `iphone` and `pixel` projects execute and pass (ROADMAP criteria 3 + 4). 3. Confirm the readiness waits prevented a startup race (no "/api/me did not return 200" or ECONNREFUSED from global-setup on a cold run). If global-setup throws the DEV_AUTH_BYPASS error, the API was started without the inline flag (Pitfall 8) — fix the node invocation, do not re-run. 4. Deliberately break a spec or seed once (or inspect a prior failure) to confirm test-results/ uploads as a downloadable artifact in the Gitea UI (D-06). Revert the break. 5. Confirm test output is readable in the Gitea log (list reporter), not invisible 'github' annotations. Type "harness green" once both device profiles pass against the CI-brought-up stack and artifact upload is confirmed, or paste the failing log. ## Trust Boundaries | Boundary | Description | | --------------------- | --------------------------------------------------------------------------------- | | DEV_AUTH_BYPASS in CI | Bypass auth flag active in the harness job only; must never reach the publish job | | CI test DB → seed | global-setup TRUNCATEs tables; fail-closed guards protect against prod DB | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | | --------- | ----------------- | ----------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | T-08-06 | Spoofing | DEV_AUTH_BYPASS=true in harness job | mitigate | Bypass is set ONLY in the harness job env, against throwaway DB creds; it never appears in the publish job (Plan 04). global-setup.ts fails closed on NODE_ENV=production and on missing DEV_AUTH_BYPASS, so it cannot wipe/seed an unconfirmed DB (08-RESEARCH Security Domain). | | T-08-07 | Tampering | drizzle migrate against CI DB | mitigate | db:migrate only; db:push forbidden (grep gate). Throwaway creds, ephemeral container. | | T-08-08 | Denial of Service | dev-server startup race | mitigate | Explicit :3000/health curl loop before Playwright (D-02) on top of global-setup's :5173/health + /api/me gates; MariaDB healthcheck.sh readiness loop before migrate (Pitfall 11). | - ci.yml passes both Task grep gates (background API + inline bypass + :3000 readiness; both browsers + base URL + gitea upload fork; no harness-file edits). - PR run shows the harness job green across iphone + pixel on a cold run. - Artifact upload confirmed on a forced failure; output legible via list reporter. - CI-01 (harness half): PR to main brings up API + PWA dev servers + MariaDB with DEV_AUTH_BYPASS and runs the Phase 7 specs headlessly; failure gates merge (ROADMAP criterion 3). - Readiness: waits for both :3000 and :5173 before Playwright (ROADMAP criterion 4; Pitfall dev-stack races). - Phase 7 specs reused UNCHANGED (phase boundary); both device profiles run (D-05); traces upload on failure (D-06). Create `.planning/phases/08-gitea-ci/08-03-SUMMARY.md` when done. Record: final API readiness timeout, whether the reporter override was needed, WebKit-deps install outcome on the runner, and confirmation no Phase 7 harness file was modified.