---
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).