Compare commits
10
Commits
5322cfc2b0
...
e57b76ef59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e57b76ef59 | ||
|
|
b333d7b7ea | ||
|
|
4a510f1d79 | ||
|
|
41b0b60291 | ||
|
|
4e474cdd9c | ||
|
|
92acf02989 | ||
|
|
e8a9ce4ea9 | ||
|
|
c3cee0baae | ||
|
|
4303a1b680 | ||
|
|
9c38dd33ff |
@@ -0,0 +1,225 @@
|
||||
name: runner-probe
|
||||
|
||||
# Probe-only workflow — answers all runner unknowns before real CI is designed.
|
||||
# Runs ONLY on the feature branch; never on main.
|
||||
# All steps that may fail on this runner use continue-on-error: true or || true
|
||||
# so the probe REPORTS findings instead of red-failing on an expected unknown.
|
||||
# Do NOT reference any secret in this workflow (T-08-01).
|
||||
#
|
||||
# Checks performed: P-01 through P-11 + P-13
|
||||
# P-12 (docker login) is deferred to Plan 04 (publish) — DO NOT add it here.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- gsd/phase-08-gitea-ci
|
||||
|
||||
jobs:
|
||||
probe:
|
||||
name: runner-probe
|
||||
runs-on: self-hosted
|
||||
|
||||
# P-05: Service container — probe whether it spawns and on which hostname.
|
||||
# Uses healthcheck.sh --connect --innodb_initialized.
|
||||
# Note: healthcheck.sh is used because the binary from older clients was
|
||||
# removed from mariadb:11 (Pitfall 11).
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
MARIADB_DATABASE: familysync
|
||||
MARIADB_USER: familysync
|
||||
MARIADB_PASSWORD: testpass
|
||||
options: >-
|
||||
--health-cmd="healthcheck.sh --connect --innodb_initialized"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=10
|
||||
--health-start-period=30s
|
||||
|
||||
steps:
|
||||
# ─── P-07: actions/checkout ──────────────────────────────────────────────
|
||||
# Must be first real step. Reaching subsequent steps proves it resolves.
|
||||
- name: P-07 checkout (actions/checkout@v4)
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# ─── P-08: actions/setup-node ────────────────────────────────────────────
|
||||
- name: P-08 setup-node (actions/setup-node@v4 — pin Node 22)
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
# ─── P-01: Node.js version ───────────────────────────────────────────────
|
||||
- name: P-01 Node.js version
|
||||
run: |
|
||||
echo "=== P-01: Node.js version ==="
|
||||
node --version
|
||||
echo "Expected: v22.x"
|
||||
|
||||
# ─── P-02: pnpm availability ─────────────────────────────────────────────
|
||||
- name: P-02 pnpm availability
|
||||
run: |
|
||||
echo "=== P-02: pnpm availability ==="
|
||||
pnpm --version 2>/dev/null || (
|
||||
echo "pnpm not found — attempting corepack activation"
|
||||
corepack enable pnpm
|
||||
pnpm --version
|
||||
)
|
||||
echo "Expected: pnpm 11.x (from packageManager field in package.json)"
|
||||
|
||||
# ─── P-03: Runner mode — THE critical fork ───────────────────────────────
|
||||
# Docker-executor mode: job runs inside a Docker container
|
||||
# → services: works; DB_HOST=mariadb
|
||||
# Host-executor mode: job runs directly on the Unraid host
|
||||
# → services: not supported; must use docker run -d fallback
|
||||
- name: P-03 runner mode detection (Docker vs host — critical fork)
|
||||
run: |
|
||||
echo "=== P-03: Runner mode (critical fork) ==="
|
||||
echo "--- /proc/1/cgroup (first 5 lines) ---"
|
||||
cat /proc/1/cgroup 2>/dev/null | head -5 || echo "(not readable)"
|
||||
echo "--- hostname ---"
|
||||
hostname
|
||||
echo "--- /.dockerenv presence ---"
|
||||
ls -la /.dockerenv 2>&1
|
||||
echo "--- Conclusion ---"
|
||||
if [ -f /.dockerenv ]; then
|
||||
echo "RUNNER MODE: Docker-executor (job is running inside a Docker container)"
|
||||
echo " → services: will work; MariaDB hostname = service label name (mariadb)"
|
||||
echo " → DB_HOST=mariadb in downstream jobs"
|
||||
else
|
||||
echo "RUNNER MODE: host-executor (job is running directly on the host)"
|
||||
echo " → services: NOT supported (nektos/act#2711)"
|
||||
echo " → Downstream jobs must use: docker run -d mariadb:11 + explicit readiness loop"
|
||||
echo " → DB_HOST=127.0.0.1 with -p 3306:3306 in docker run"
|
||||
fi
|
||||
|
||||
# ─── P-04: Docker socket access ──────────────────────────────────────────
|
||||
- name: P-04 Docker socket access
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "=== P-04: Docker socket access ==="
|
||||
docker info 2>&1 | head -20
|
||||
echo "--- docker ps (first few lines) ---"
|
||||
docker ps 2>&1 | head
|
||||
|
||||
# ─── P-05: Service container spawn check ─────────────────────────────────
|
||||
- name: P-05 service container spawn (mariadb:11 visible in docker ps?)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "=== P-05: Service container spawn ==="
|
||||
docker ps 2>&1 | grep -i maria \
|
||||
&& echo "RESULT: MariaDB service container IS visible in docker ps (Docker-executor mode confirmed)" \
|
||||
|| echo "RESULT: no mariadb container visible in docker ps (likely host-executor mode — services: not supported)"
|
||||
|
||||
# ─── P-06: MariaDB reachability — try BOTH hostnames ─────────────────────
|
||||
# Does NOT fail the job: records which hostname resolves.
|
||||
- name: P-06 MariaDB reachability (hostname=mariadb — Docker mode)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "=== P-06a: MariaDB via hostname 'mariadb' (Docker-executor mode) ==="
|
||||
mysql -h mariadb -P 3306 -u familysync -ptestpass -e "SELECT 1" 2>&1 | head \
|
||||
&& echo "P-06a RESULT: mariadb hostname RESOLVES — Docker mode" \
|
||||
|| echo "P-06a RESULT: mariadb hostname DOES NOT resolve (expected if host-executor mode)"
|
||||
|
||||
- name: P-06 MariaDB reachability (hostname=127.0.0.1 — host mode)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "=== P-06b: MariaDB via 127.0.0.1 (host-executor mode fallback) ==="
|
||||
mysql -h 127.0.0.1 -P 3306 -u familysync -ptestpass -e "SELECT 1" 2>&1 | head \
|
||||
&& echo "P-06b RESULT: 127.0.0.1 RESOLVES — host mode" \
|
||||
|| echo "P-06b RESULT: 127.0.0.1 does not resolve (expected if Docker-executor mode uses 'mariadb' hostname)"
|
||||
|
||||
# ─── P-09: actions/cache ─────────────────────────────────────────────────
|
||||
# Known issue: cache server runs in runner container; job container on different
|
||||
# network may cause socket hang-up. continue-on-error so probe reports finding.
|
||||
- name: P-09 cache action (actions/cache@v4 — may time out in Docker mode)
|
||||
uses: actions/cache@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: /tmp/probe-cache-test
|
||||
key: runner-probe-cache-test-${{ github.sha }}
|
||||
|
||||
- name: P-09 cache result
|
||||
run: |
|
||||
echo "=== P-09: Cache action result ==="
|
||||
echo "If the previous 'cache' step completed without hanging, cache IS usable."
|
||||
echo "If it timed out or errored, fall back to no-cache in downstream jobs."
|
||||
|
||||
# ─── P-10: Playwright WebKit system deps ────────────────────────────────
|
||||
# Confirms WebKit system deps install without sudo/apt failure (Assumption A10).
|
||||
# Runs from apps/pwa where @playwright/test is installed.
|
||||
- name: P-10 Playwright WebKit + Chromium system deps
|
||||
continue-on-error: true
|
||||
working-directory: apps/pwa
|
||||
run: |
|
||||
echo "=== P-10: Playwright browser system deps (webkit + chromium) ==="
|
||||
npx playwright install --with-deps webkit chromium 2>&1 | tail -30
|
||||
echo "--- Exit code: $? ---"
|
||||
echo "If the above shows installed without 'sudo' or 'apt' errors, WebKit deps are OK."
|
||||
|
||||
# ─── P-11: gitea-upload-artifact fork ───────────────────────────────────
|
||||
# The official upload-artifact action is broken on Gitea (detected as GHES, aborts).
|
||||
# Use ChristopherHX/gitea-upload-artifact@v4 — the confirmed Gitea fix (RESEARCH T-08-SC).
|
||||
- name: P-11 write dummy artifact for upload test
|
||||
run: |
|
||||
echo "=== P-11: Upload artifact test ==="
|
||||
mkdir -p /tmp/probe-artifact
|
||||
echo "runner-probe artifact: sha=${GITHUB_SHA}" > /tmp/probe-artifact/probe.txt
|
||||
cat /tmp/probe-artifact/probe.txt
|
||||
|
||||
- name: P-11 artifact upload (ChristopherHX/gitea-upload-artifact@v4)
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: runner-probe-artifact
|
||||
path: /tmp/probe-artifact/
|
||||
retention-days: 3
|
||||
|
||||
# ─── P-13: GITHUB_SHA short SHA expression ───────────────────────────────
|
||||
- name: P-13 short SHA expression (D-04 tag validation)
|
||||
run: |
|
||||
echo "=== P-13: GITHUB_SHA short SHA ==="
|
||||
echo "GITHUB_SHA full = ${GITHUB_SHA}"
|
||||
echo "short sha = ${GITHUB_SHA:0:7}"
|
||||
echo "Expected: a 7-character hex string — confirms D-04 tag expression works"
|
||||
if [ "${#GITHUB_SHA}" -ge 7 ]; then
|
||||
echo "P-13 RESULT: OK — GITHUB_SHA is available and bash substring works"
|
||||
else
|
||||
echo "P-13 RESULT: WARN — GITHUB_SHA is shorter than expected or empty"
|
||||
fi
|
||||
|
||||
# ─── SUMMARY: one-line verdict per fork ──────────────────────────────────
|
||||
# Plans 02–04 consume these answers to pick the correct implementation path.
|
||||
- name: SUMMARY — fork verdicts (record answers for SUMMARY.md)
|
||||
run: |
|
||||
echo "========================================================"
|
||||
echo " RUNNER PROBE SUMMARY — FORK ANSWERS"
|
||||
echo "========================================================"
|
||||
echo ""
|
||||
echo "P-03 Runner mode:"
|
||||
if [ -f /.dockerenv ]; then
|
||||
echo " DOCKER-EXECUTOR — job runs in a container"
|
||||
echo " → services: works; DB_HOST=mariadb"
|
||||
else
|
||||
echo " HOST-EXECUTOR — job runs directly on host"
|
||||
echo " → services: NOT supported; use docker run -d + DB_HOST=127.0.0.1"
|
||||
fi
|
||||
echo ""
|
||||
echo "P-05/P-06 MariaDB service container:"
|
||||
MARIA_CONTAINER=$(docker ps 2>/dev/null | grep -i maria | head -1 || true)
|
||||
if [ -n "$MARIA_CONTAINER" ]; then
|
||||
echo " Service container IS visible — hostname 'mariadb' should resolve"
|
||||
else
|
||||
echo " Service container NOT visible — use docker run -d fallback"
|
||||
fi
|
||||
echo ""
|
||||
echo "P-09 Cache: see 'cache' step result above (continue-on-error — pass = usable)"
|
||||
echo "P-10 WebKit deps: see 'playwright install' step above (continue-on-error)"
|
||||
echo "P-11 Upload artifact: see 'gitea-upload-artifact' step above (continue-on-error)"
|
||||
echo ""
|
||||
echo "P-13 Short SHA: ${GITHUB_SHA:0:7}"
|
||||
echo ""
|
||||
echo " SECURITY CHECK: this probe references NO secrets (T-08-01 compliant)"
|
||||
echo " P-12 (docker login/push) is deferred to Plan 04 — NOT in this probe."
|
||||
echo "========================================================"
|
||||
+38
-1
@@ -94,7 +94,23 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem
|
||||
- **--password-stdin** (Pitfall 13): `docker login` via `--password-stdin` with the token piped from a registered Gitea secret (PAT with `write:package`); never `-p $TOKEN` on the command line.
|
||||
- Hard constraints: API integration tests need a real MariaDB and live in `apps/api/tests/` (never `src/`); cache the pnpm store; Drizzle generate+migrate to set up the CI DB schema; the harness step reuses the Phase 7 specs unchanged (CI owns only the stack bring-up + readiness wait, not the spec content).
|
||||
|
||||
**Plans**: TBD
|
||||
**Plans**: 4 plans (4 waves)Plans:
|
||||
**Wave 1**
|
||||
|
||||
- [ ] 08-01-PLAN.md — Runner probe + operator runner/PAT registration (W0; answers the Docker-vs-host fork)
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
|
||||
- [ ] 08-02-PLAN.md — ci.yml: fast-checks (lint/typecheck/PWA unit) + API job (MariaDB service + migrate + DB-backed tests)
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
|
||||
- [ ] 08-03-PLAN.md — ci.yml: harness job (dev-stack bring-up + readiness waits + Phase 7 Playwright specs, both profiles)
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 completion)*
|
||||
|
||||
- [ ] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1-<sha> via --password-stdin)
|
||||
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 9: Faster Write-Back
|
||||
@@ -373,3 +389,24 @@ Plans:
|
||||
Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
|
||||
### Phase 999.15: Desktop e2e coverage — add a Desktop Playwright profile + desktop-safe specs (BACKLOG)
|
||||
|
||||
**Goal:** [Captured for future planning] The Playwright harness (`apps/pwa/playwright.config.ts`) defines only **mobile** device profiles — `iphone` (iPhone 14 / WebKit) and `pixel` (Pixel 7 / Chromium), both with touch and a mobile viewport. The Phase 8 CI regression gate runs `pnpm test:e2e`, so it currently validates the **mobile experience only**. Add desktop coverage so the regression gate exercises the desktop layout/flows as well.
|
||||
|
||||
**Options / decisions to make when picking this up:**
|
||||
|
||||
- **Add a Desktop profile:** a new `desktop` project in `playwright.config.ts` (e.g. `devices['Desktop Chrome']`, no `hasTouch`, wide viewport). Optionally a Desktop WebKit/Safari profile too — but the family's Apple member is already covered on mobile Safari via `iphone`; Desktop Chrome is likely sufficient for a shared/wall browser.
|
||||
- **Spec-compat pass (the real work):** the existing e2e specs were authored for mobile — they may assume touch gestures, a mobile nav/drawer, or mobile-only layout. Each spec needs review/adjustment so it passes (or is appropriately skipped) on a no-touch, wide-viewport desktop. This is harness/spec work, not CI plumbing.
|
||||
- **Gating choice:** decide whether desktop runs block the merge immediately, or run advisory (non-blocking) until the specs are confirmed desktop-safe.
|
||||
|
||||
**Boundary:** Phase 8 deliberately reused the Phase 7 harness **unchanged** (CI owns only stack bring-up + readiness waits, not spec content), which is why this was deferred. Once a Desktop project is added to the config, Phase 8 CI picks it up automatically via `pnpm test:e2e` — no CI changes needed beyond whatever runtime/wait the desktop profile requires.
|
||||
|
||||
**Context:** Deferred from Phase 8 (Gitea CI) planning, 2026-06-11 — user wants both mobile and desktop validated, but desktop needs a config addition + spec review that is out of Phase 8's CI-plumbing scope. Tags: testing, playwright, e2e, desktop, harness, ci.
|
||||
|
||||
**Requirements:** TBD
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
|
||||
+14
-14
@@ -2,14 +2,14 @@
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.1
|
||||
milestone_name: Operability & Polish
|
||||
status: verifying
|
||||
stopped_at: Completed 07-04-PLAN.md
|
||||
last_updated: "2026-06-11T06:29:22.998Z"
|
||||
last_activity: 2026-06-11
|
||||
status: executing
|
||||
stopped_at: "08-01 Task 2 complete (runner-probe.yml committed b333d7b) — awaiting Task 3 human-verify (push + read probe log)"
|
||||
last_updated: "2026-06-11T14:00:00.000Z"
|
||||
last_activity: 2026-06-11 -- 08-01 Task 2 complete; runner-probe.yml authored and committed
|
||||
progress:
|
||||
total_phases: 14
|
||||
total_phases: 15
|
||||
completed_phases: 1
|
||||
total_plans: 4
|
||||
total_plans: 8
|
||||
completed_plans: 4
|
||||
percent: 7
|
||||
---
|
||||
@@ -21,14 +21,14 @@ progress:
|
||||
See: .planning/PROJECT.md (updated 2026-06-10)
|
||||
|
||||
**Core value:** One color-coded family calendar (shared + personal) and shared lists from a single low-friction PWA — cross-ecosystem, no app store
|
||||
**Current focus:** Phase 07 — mobile-test-harness
|
||||
**Current focus:** Phase 08 — gitea-ci
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 8
|
||||
Plan: Not started
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-06-11
|
||||
Phase: 08 (gitea-ci) — EXECUTING
|
||||
Plan: 1 of 4 (in progress — awaiting Task 3 checkpoint verification)
|
||||
Status: Paused at checkpoint:human-verify (08-01 Task 3)
|
||||
Last activity: 2026-06-11 -- runner-probe.yml committed (b333d7b); awaiting operator to push + observe probe run
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
@@ -191,9 +191,9 @@ Recent decisions affecting current work:
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-06-11T06:13:23.996Z
|
||||
Stopped at: Completed 07-04-PLAN.md
|
||||
Resume file: None
|
||||
Last session: 2026-06-11T12:50:02.402Z
|
||||
Stopped at: Phase 8 context gathered
|
||||
Resume file: .planning/phases/08-gitea-ci/08-CONTEXT.md
|
||||
|
||||
## Operator Next Steps
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
phase: 07-mobile-test-harness
|
||||
fixed_at: 2026-06-11T08:05:00Z
|
||||
review_path: .planning/phases/07-mobile-test-harness/07-REVIEW.md
|
||||
iteration: 1
|
||||
findings_in_scope: 12
|
||||
fixed: 6
|
||||
skipped: 6
|
||||
status: partial
|
||||
---
|
||||
|
||||
# Phase 7: Code Review Fix Report
|
||||
|
||||
**Fixed at:** 2026-06-11T08:05:00Z
|
||||
**Source review:** .planning/phases/07-mobile-test-harness/07-REVIEW.md
|
||||
**Iteration:** 1
|
||||
|
||||
**Summary:**
|
||||
- Findings in scope (fix_scope=all): 12 open/actionable + info; CR-01/BL-01/BL-02 already resolved (left intact)
|
||||
- Fixed: 6 (WR-01, WR-02, WR-05, WR-06, WR-07 — and WR-02/WR-01 share one commit)
|
||||
- Skipped: 6 (WR-03, WR-04, IN-01..IN-05) — by-design / positive notes, no net-positive edit available
|
||||
|
||||
**Verification evidence (all fixes):**
|
||||
- Full E2E suite (both profiles, iphone/WebKit + pixel/Chromium): **58 passed** (29.4s), suite exit 0.
|
||||
- `pnpm --filter @familysync/pwa typecheck` (both `tsconfig.json` and `tsconfig.e2e.json`): **exit 0**.
|
||||
- SW test (WR-07) confirmed passing on BOTH iphone(WebKit) and pixel(Chromium) — re-run in isolation: 2 passed.
|
||||
- Suite was run from the isolated worktree with `.env` sourced from the main repo (worktree `.env` is gitignored/absent) + `DEV_AUTH_BYPASS=true DB_HOST=127.0.0.1 DB_PORT=3306`.
|
||||
|
||||
## Fixed Issues
|
||||
|
||||
### WR-01: readiness gate accepts the SPA shell, not a working DEV_AUTH_BYPASS API
|
||||
|
||||
**Files modified:** `apps/pwa/e2e/global-setup.ts`
|
||||
**Commit:** 9c38dd3 (shared with WR-02)
|
||||
**Applied fix:** Added a Step 1b probe after the `/health` gate: `fetch(baseURL + '/api/me', { redirect: 'manual' })` and throw with a clear, actionable message unless it returns 200. If the API was started without `DEV_AUTH_BYPASS=true`, `/api/me` redirects (302) to Authelia; the gate now fails loudly in setup instead of producing ~40 confusing spec failures. Verified: the seed ran and all 58 specs passed, proving the new gate does not false-positive against the correctly-configured dev stack.
|
||||
|
||||
### WR-02: readiness-gate success misreported as timeout near the deadline
|
||||
|
||||
**Files modified:** `apps/pwa/e2e/global-setup.ts`
|
||||
**Commit:** 9c38dd3 (shared with WR-01)
|
||||
**Applied fix:** Replaced the post-loop `if (Date.now() >= deadline) throw` (which can misclassify a success that arrived in the final second as a timeout, because `await fetch` itself consumes time) with an explicit `let ready = false` flag set inside the loop on `res.ok`; throw only `if (!ready)`. Removes the clock-inference race. Verified by full green suite (globalSetup executes once at suite start).
|
||||
|
||||
> Note: WR-01 and WR-02 are committed together because both edits live in the same contiguous readiness-gate hunk in `global-setup.ts` (no `gsd-tools` / interactive hunk-split available to separate one hunk into two commits). Both are readiness-gate robustness changes.
|
||||
|
||||
### WR-05: `page.unroute` not in `finally` — misleading dead cleanup
|
||||
|
||||
**Files modified:** `apps/pwa/e2e/calendar.spec.ts`, `apps/pwa/e2e/lists.spec.ts`
|
||||
**Commit:** 2b745ad
|
||||
**Applied fix:** Removed the 5 trailing `page.unroute(...)` calls (calendar: error-heading, retry-44px, error-overflow tests; lists: empty-state, empty-overflow tests) and replaced each with a one-line comment explaining that Playwright gives each test a fresh page/context, so route handlers do not leak across tests — and that a trailing unroute never runs anyway if an `expect` above throws. Chose "drop redundant calls" over "wrap in try/finally" per the reviewer's stated options; it is the lower-noise option and matches real per-test isolation. Verified: all route-mocked error/empty-state tests still pass on both profiles.
|
||||
|
||||
### WR-06: self-validation "remove style by reload" comment is wrong
|
||||
|
||||
**Files modified:** `apps/pwa/e2e/layout.spec.ts`
|
||||
**Commit:** 5322cfc
|
||||
**Applied fix:** Corrected both misleading comments (Rule 1 proof ~L209, Rule 2 proof ~L250) that claimed the injected `<style>` is removed "by navigating / page.reload drops inline style tags". The code actually removes it via `styleHandle.evaluate((el) => el.remove())` with no reload. Comment-only change. Tier-2 typecheck + full suite green.
|
||||
|
||||
### WR-07: SW-controller assertion near-vacuous on WebKit (iPhone) profile
|
||||
|
||||
**Files modified:** `apps/pwa/e2e/calendar.spec.ts`
|
||||
**Commit:** c564fc6
|
||||
**Applied fix:** Rewrote the test from asserting `navigator.serviceWorker.controller === null` (which passes for unrelated reasons: SW absent on WebKit/http, or null controller on any first uncontrolled load) to: (1) probe `'serviceWorker' in navigator`; (2) `test.skip(!swAvailable, ...)` so an unavailable API does not masquerade as a passing block (does NOT throw on WebKit); (3) where available, assert `navigator.serviceWorker.getRegistration()` resolves to `undefined`, which actually proves `serviceWorkers: 'block'` prevented registration. Renamed the test to "no service-worker registration". **Verified on BOTH profiles** — re-ran in isolation: `2 passed` (iphone + pixel); WebKit does not throw.
|
||||
|
||||
## Skipped Issues
|
||||
|
||||
### WR-03: webServer manages Vite only; proxied API not managed
|
||||
|
||||
**File:** `apps/pwa/playwright.config.ts:59-64`
|
||||
**Reason:** skipped — by design (D-09/D-10: operator brings up the stack, harness waits via globalSetup `/health` gate). Reviewer itself states "No code defect; documentation-coupling risk." WR-01's `/api/me` gate already strengthens the deferred-failure path. No net-positive code change.
|
||||
|
||||
### WR-04: `page.route('/api/lists')` exact match
|
||||
|
||||
**File:** `apps/pwa/e2e/lists.spec.ts:74, 96`
|
||||
**Reason:** skipped — already DOWNGRADED to resolved-correct in the review. `fetchLists()` requests the bare `/api/lists` (no query string), and the exact matcher is intentionally narrow so it does not swallow `/api/lists/:id/items`. Converting to a glob would be a regression. No change needed.
|
||||
|
||||
### IN-01: `mysql2` as PWA devDependency
|
||||
|
||||
**File:** `apps/pwa/package.json:38`
|
||||
**Reason:** skipped — placement is correct and acceptable (dev/test-only, never bundled; vitest excludes `e2e/**`). The only caveat is keeping the version pin in lockstep with `apps/api`; both are currently `3.22.4`. Not a defect.
|
||||
|
||||
### IN-02: `tsconfig.e2e.json` `types: ["node"]` narrows ambient types
|
||||
|
||||
**File:** `apps/pwa/tsconfig.e2e.json:4-5`
|
||||
**Reason:** skipped — positive "this is sound, no action" note from the reviewer. DOM globals come from `lib`, `@playwright/test` types via direct import. Confirmed by typecheck exit 0.
|
||||
|
||||
### IN-03: vitest `exclude: ['e2e/**']` isolation
|
||||
|
||||
**File:** `apps/pwa/vitest.config.ts:17`
|
||||
**Reason:** skipped — positive "no action" note; the two runners are cleanly partitioned.
|
||||
|
||||
### IN-04: `typecheck` script covers the e2e tsconfig
|
||||
|
||||
**File:** `apps/pwa/package.json:10`
|
||||
**Reason:** skipped — positive "good, no action" note; confirmed `typecheck` runs both tsconfigs (exit 0).
|
||||
|
||||
### IN-05: CR-01 guard protects production, not "the wrong dev DB"
|
||||
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:34-44`
|
||||
**Reason:** skipped — by design (D-06 deterministic reseed; documented in README). The dev-DB-wipe is intended. Adding an `E2E_ALLOW_TRUNCATE`/`*_test`-name gate would contradict the locked deterministic-reseed design and add operator friction for no production-safety gain (production is already hard-blocked). Per scope guidance, not a net-positive change.
|
||||
|
||||
---
|
||||
|
||||
_Fixed: 2026-06-11T08:05:00Z_
|
||||
_Fixer: Claude (gsd-code-fixer)_
|
||||
_Iteration: 1_
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
phase: 07-mobile-test-harness
|
||||
reviewed: 2026-06-11T03:30:00Z
|
||||
reviewed: 2026-06-11T12:30:00Z
|
||||
depth: deep
|
||||
files_reviewed: 11
|
||||
files_reviewed: 9
|
||||
files_reviewed_list:
|
||||
- apps/pwa/playwright.config.ts
|
||||
- apps/pwa/e2e/global-setup.ts
|
||||
@@ -13,399 +13,220 @@ files_reviewed_list:
|
||||
- apps/pwa/tsconfig.e2e.json
|
||||
- apps/pwa/vitest.config.ts
|
||||
- apps/pwa/package.json
|
||||
- package.json
|
||||
- .gitignore
|
||||
findings:
|
||||
critical: 0
|
||||
critical_resolved: 1
|
||||
blocker: 0
|
||||
blocker_resolved: 2
|
||||
warning: 7
|
||||
info: 5
|
||||
total: 14
|
||||
status: blockers_resolved
|
||||
warning: 0
|
||||
warning_resolved: 7
|
||||
info: 0
|
||||
info_bydesign: 5
|
||||
total: 0
|
||||
status: clean
|
||||
---
|
||||
|
||||
# Phase 7: Code Review Report (DEEP)
|
||||
# Phase 7: Code Review Report (DEEP) — Iteration 2 (--auto re-review)
|
||||
|
||||
**Reviewed:** 2026-06-11
|
||||
**Depth:** deep (cross-file call-chain analysis)
|
||||
**Files Reviewed:** 11 (harness) + 12 cross-referenced app/API source files
|
||||
**Status:** issues_found
|
||||
**Depth:** deep (cross-file call-chain analysis + live-stack verification)
|
||||
**Files Reviewed:** 9 (harness)
|
||||
**Status:** clean — zero open actionable findings
|
||||
|
||||
## Summary
|
||||
|
||||
This is a deep re-review of the Phase 7 Playwright mobile E2E harness, tracing every spec
|
||||
assertion through to the PWA component / API route it claims to exercise. The standard-depth
|
||||
pass found CR-01 (data-loss guard — resolved in `fcc680e`) plus 7 warnings and 4 info items.
|
||||
This is the iteration-2 re-review after the fixer applied 5 changes (commits `c564fc6` WR-07,
|
||||
`9c38dd3` WR-01+WR-02, `5322cfc` WR-06, `2b745ad` WR-05). The prior pass had resolved CR-01,
|
||||
BL-01, and BL-02; those resolution records are preserved below.
|
||||
|
||||
The deep pass **confirms CR-01 is soundly fixed end-to-end** but escalates two findings to
|
||||
**BLOCKER** that only cross-file analysis surfaces:
|
||||
**Verification performed this pass:**
|
||||
- Ran the full suite against the live dev stack (MariaDB :3306, API :3000 `DEV_AUTH_BYPASS=true`,
|
||||
Vite auto-started by `webServer`): **58 passed (55s)**.
|
||||
- Typecheck (`tsc --noEmit` + `tsc --project tsconfig.e2e.json --noEmit`): **exit 0**.
|
||||
- Probed `navigator.serviceWorker` availability on **both** engines to confirm the WR-07 fix is
|
||||
non-vacuous (see WR-07 below).
|
||||
- Probed `redirect:'manual'` response semantics to confirm the WR-01 gate distinguishes a
|
||||
dev-bypass 200 from an Authelia redirect.
|
||||
|
||||
1. **BL-01** — Two of the three `calendar.spec.ts` "populated state" assertions are **vacuous**:
|
||||
they assert against a code path (`EmptyState` / "Nothing here") that `CalendarShell`
|
||||
**never renders**, and against a wrapper element (`.sx-react-calendar-wrapper`) that is
|
||||
**always rendered on success regardless of whether the seed produced any events**. Neither
|
||||
assertion would fail if the seed broke or the events query returned `[]`. The harness's
|
||||
central claim — "validates the populated calendar state" — is not met.
|
||||
|
||||
2. **BL-02** — The seeded calendar event is placed at **`Date.now() + 24h`** while the PWA's
|
||||
initial fetch window is **current-month −7d … +7d** (`calendarStore.initialCalendarRange`).
|
||||
When the suite runs in the last 7 days of a month, "tomorrow" falls in the *next* month,
|
||||
outside the initial window, so the seeded event is never fetched. This is latent today only
|
||||
because BL-01's assertions don't actually check for the event — but it means the seed↔window
|
||||
contract is broken and any future "seeded event is visible" assertion will be date-dependent
|
||||
and flaky.
|
||||
|
||||
The auth/security call-chain (DEV_AUTH_BYPASS) is sound. The route-mock URL patterns match the
|
||||
real request URLs. The vitest/e2e glob isolation is correct. The remaining issues are
|
||||
determinism and config robustness (warnings) carried forward with deeper evidence.
|
||||
**Result:** all 7 prior warnings are resolved by the fixes (5 actionable + WR-03/WR-04 by-design),
|
||||
no fix introduced a regression or new defect, and no new cross-file issue was exposed.
|
||||
Setting `status: clean`. The 5 IN-* items remain advisory/by-design and are listed under
|
||||
"Resolved / By-design"; none are actionable.
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues (resolved this phase — record preserved)
|
||||
## Critical Issues (resolved — record preserved)
|
||||
|
||||
### CR-01 (RESOLVED in commit `fcc680e`): global-setup TRUNCATE had no fail-closed guard
|
||||
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:26-44`
|
||||
**Status:** RESOLVED — verified sound end-to-end in this deep pass.
|
||||
**Status:** RESOLVED — re-verified sound this pass.
|
||||
|
||||
`globalSetup` runs `TRUNCATE TABLE` against `list_items`, `list_shares`, `lists`,
|
||||
`calendar_events` using whatever `DB_*` env points at. The fix adds a two-part guard that
|
||||
throws **before** opening any DB connection:
|
||||
|
||||
1. `NODE_ENV === 'production'` → throw (first check, before reading any other env var).
|
||||
`globalSetup` TRUNCATEs `list_items`, `list_shares`, `lists`, `calendar_events` against whatever
|
||||
`DB_*` points at. The fix throws **before** opening any DB connection:
|
||||
1. `NODE_ENV === 'production'` → throw (checked first).
|
||||
2. `DEV_AUTH_BYPASS !== 'true'` → throw.
|
||||
|
||||
**Deep-pass verification (call-chain consistency with the API guard):**
|
||||
|
||||
- `apps/api/src/auth/devBypass.ts:61-66` uses the *identical* ordering: `NODE_ENV==='production'`
|
||||
checked first, then `DEV_AUTH_BYPASS !== 'true'`. The harness guard mirrors it exactly.
|
||||
- `apps/api/src/index.ts:24-25` computes `devBypassActive = NODE_ENV !== 'production' &&
|
||||
DEV_AUTH_BYPASS === 'true'`, and **only mounts the OIDC guard when `!devBypassActive`**
|
||||
(lines 51-55). So the harness's required precondition (`DEV_AUTH_BYPASS=true`) is the same flag
|
||||
that makes the API serve Dev User 1 without OIDC — the guards are coupled to the same switch.
|
||||
- No production path serves authed data: in production `NODE_ENV==='production'` forces
|
||||
`devBypassActive=false`, the OIDC middleware is unconditionally mounted, and `devAuthBypass()`
|
||||
returns a pure passthrough. The harness guard additionally refuses to even *run* there.
|
||||
|
||||
**Residual note (see IN-05):** the guard couples a *data-mutation* safety check to an *auth-mode*
|
||||
flag. It is correct for this harness, but `DEV_AUTH_BYPASS=true` with `DB_*` pointed at a
|
||||
populated **dev** DB will still wipe that dev DB — the guard protects production, not "the wrong
|
||||
non-prod DB." This is acceptable for the stated design (D-06 deterministic reseed) and documented
|
||||
in README §"What globalSetup Does"; flagged only so it is not mistaken for broader protection.
|
||||
This mirrors the API guard (`apps/api/src/auth/devBypass.ts`) ordering exactly and is coupled to
|
||||
the same switch that makes the API serve Dev User 1 without OIDC (`index.ts:24-25, 51-55`).
|
||||
Residual scope note carried as IN-05 (guard protects production, not "the wrong dev DB" — by design).
|
||||
|
||||
---
|
||||
|
||||
## Blocker Findings (NEW — surfaced by call-chain analysis)
|
||||
## Blocker Findings (resolved — record preserved)
|
||||
|
||||
> **BOTH BLOCKERS RESOLVED in commit `53c3ca5`.**
|
||||
> - **BL-01:** the dead-`EmptyState` / always-rendered-wrapper assertions were replaced with a real
|
||||
> DB→UI proof — `getByText('Seeded Test Event')` must be visible in the grid. Verified non-vacuous:
|
||||
> passes with the seed on both profiles; with `/api/events` mocked to `[]` the title is absent
|
||||
> (the assertion would fail). The old `'Nothing here'` check was empirically confirmed dead
|
||||
> (count 0 even with zero events).
|
||||
> - **BL-02:** the reviewer's stated *mechanism* was inaccurate — the fetch window
|
||||
> `[monthStart−7d, monthEnd+7d]` (verified in `calendarStore.initialCalendarRange`) **does** include
|
||||
> `now+24h`, so the API window never excludes it. The real fragility is the rendered **month-agenda
|
||||
> view of the current month** (both profiles are phone-width): on a month's last day "tomorrow" is in
|
||||
> the next month and not displayed. The *conclusion* (a date-fragile visibility assertion) was correct.
|
||||
> Fixed by re-anchoring the seed to **noon-today (UTC)** — always today's local date, always in the
|
||||
> current-month view.
|
||||
### BL-01 (RESOLVED in commit `53c3ca5`): calendar populated-state assertions were vacuous
|
||||
|
||||
### BL-01: `calendar.spec.ts` populated-state assertions are vacuous — they cannot fail if the seed regresses
|
||||
**File:** `apps/pwa/e2e/calendar.spec.ts`
|
||||
**Status:** RESOLVED — re-verified non-vacuous this pass.
|
||||
|
||||
**Files:**
|
||||
- `apps/pwa/e2e/calendar.spec.ts:66-72` (`Schedule-X calendar grid is visible after seeding`)
|
||||
- `apps/pwa/e2e/calendar.spec.ts:74-78` (`EmptyState "Nothing here" is NOT present when events are seeded`)
|
||||
- Ground truth: `apps/pwa/src/components/CalendarShell.tsx:383-389`, `apps/pwa/src/components/EmptyState.tsx:45`
|
||||
The dead-`EmptyState` / always-rendered-wrapper assertions were replaced with a real DB→UI proof:
|
||||
`getByText('Seeded Test Event').first()` must be visible in the grid (`calendar.spec.ts:90-97`).
|
||||
Verified live: passes on both `iphone` (WebKit) and `pixel` (Chromium). With `/api/events`
|
||||
mocked empty the title is absent, so the assertion genuinely tracks the seed flowing
|
||||
DB → API → query → grid. The wrapper-visibility test (`:80-88`) was kept but its docstring now
|
||||
correctly states it only proves the grid mounts, not that the seed reached the UI.
|
||||
|
||||
**Issue:**
|
||||
### BL-02 (RESOLVED in commit `53c3ca5`): seed↔view month-boundary fragility
|
||||
|
||||
The spec docstring (`calendar.spec.ts:74-77`) asserts: *"CalendarShell renders EmptyState when the
|
||||
events query succeeds with zero occurrences."* **This is factually wrong.** `CalendarShell` does
|
||||
the opposite — its success branch (`CalendarShell.tsx:383-389`) **always** renders
|
||||
`<ScheduleXCalendar>` and its inline comment states explicitly:
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:119-154`
|
||||
**Status:** RESOLVED — re-verified deterministic this pass.
|
||||
|
||||
```
|
||||
// ALWAYS render the calendar even when the window has no events — its built-in
|
||||
// header carries the navigation, so swapping in an empty-state would strand the
|
||||
// user ...
|
||||
```
|
||||
|
||||
Consequences traced through source:
|
||||
|
||||
1. **`EmptyState` / the string "Nothing here" is dead code on `/calendar`.** A repo-wide search
|
||||
confirms `EmptyState.tsx` is imported by **nothing** (only `ListsEmptyState` is imported, by
|
||||
`ListsIndex`). So `page.getByText('Nothing here')` matches **zero** elements in every calendar
|
||||
state — seeded, empty, error, or loading. `toHaveCount(0)` is therefore **permanently green
|
||||
and independent of the seed**. If the seed inserted nothing, or the events query returned `[]`,
|
||||
or the API 500'd, this assertion would still pass. It validates nothing.
|
||||
|
||||
2. **`.sx-react-calendar-wrapper` is rendered on every successful auth**, with or without events
|
||||
(`CalendarShell.tsx:388` is in the non-error, non-loading branch which fires for *any*
|
||||
successful `eventsQuery`, including zero occurrences). So
|
||||
`expect(page.locator('.sx-react-calendar-wrapper')).toBeVisible()` passes whenever auth +
|
||||
the events fetch resolve — it does **not** prove the seeded event reached the grid. The test
|
||||
name "...visible **after seeding**" overclaims; it would stay green if the seed were deleted.
|
||||
|
||||
**Why this matters (adversarial):** TEST-01/TEST-02's acceptance bar is "the harness measures real
|
||||
rendered state and fails on regression." These two tests measure *auth + render-of-an-empty-grid*,
|
||||
not *populated state*. A regression that silently drops all events (broken seed, broken
|
||||
`/api/events` join, broken `hydrateEvents`) would ship green. The only test in the suite that
|
||||
actually proves data flows from DB → UI is on the **lists** side (`lists.spec.ts:42-47`,
|
||||
`getByRole('listitem')` not count 0), which IS sound. The calendar side has no equivalent.
|
||||
|
||||
**Fix:** Assert on something only present when the seed's event is actually rendered. Schedule-X
|
||||
renders an event element carrying the title. Add a positive assertion, e.g.:
|
||||
|
||||
```ts
|
||||
test('seeded event "Seeded Test Event" is rendered in the grid', async ({ page }) => {
|
||||
await page.goto('/calendar')
|
||||
await expect(page.getByRole('navigation', { name: 'Main navigation' })).toBeVisible()
|
||||
// Schedule-X renders the SUMMARY text inside the time/month grid.
|
||||
await expect(page.getByText('Seeded Test Event').first()).toBeVisible()
|
||||
})
|
||||
```
|
||||
|
||||
and **delete** the "Nothing here" assertion (it targets a non-existent render path) — or, if an
|
||||
empty-state proof is wanted, network-mock `/api/events*` to `{ occurrences: [] }` and assert the
|
||||
grid renders with **no** event elements (the app's real empty behaviour), not the dead
|
||||
`EmptyState`. Also correct the false docstring at `calendar.spec.ts:74-77`.
|
||||
The seed event is re-anchored to **noon-today (UTC)** (`global-setup.ts:127-129`) — always today's
|
||||
local calendar date, always inside the current-month view both phone-width profiles render. The
|
||||
prior `now+24h` could roll into the next month on a month's last day, making any "seeded event is
|
||||
visible" assertion date-fragile. The seed shape (`all_day=false`, `dtstart_utc` set, recurring
|
||||
flags false) matches the API's non-recurring-timed WHERE branch. Verified live on both engines.
|
||||
|
||||
---
|
||||
|
||||
### BL-02: Seed event start (`now + 24h`) can fall outside the PWA's initial fetch window (month-boundary flake)
|
||||
## Resolved this iteration (fixer commits — verified, no regression)
|
||||
|
||||
**Files:**
|
||||
- `apps/pwa/e2e/global-setup.ts:97` (`futureStart = new Date(Date.now() + 24*60*60*1000)`)
|
||||
- Ground truth: `apps/pwa/src/store/calendarStore.ts:124-137` (`initialCalendarRange`)
|
||||
- API window contract: `apps/api/src/routes/events.ts:133-214`
|
||||
### WR-01 (RESOLVED in `9c38dd3`): `/api/me` dev-bypass reachability gate
|
||||
|
||||
**Issue:**
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:75-90`
|
||||
|
||||
`initialCalendarRange()` builds the first TanStack-Query window as **first-of-current-month −7d**
|
||||
to **last-of-current-month +7d** (`calendarStore.ts:126-129`). `CalendarShell` issues the initial
|
||||
`fetchEvents(start, end)` with exactly this range (`CalendarShell.tsx:121-127`). The seed places
|
||||
the only event at `Date.now() + 24h` (`global-setup.ts:97`).
|
||||
The gate now probes `fetch(${baseURL}/api/me, { redirect: 'manual' })` after the `/health` poll
|
||||
and throws unless `res.ok`. Verified correct end-to-end:
|
||||
- **Dev-bypass-reachable API → 200.** `me.ts:30-42` short-circuits on `c.get('user')` (DEV_USER)
|
||||
with no DB round-trip, so the gate passes regardless of seed state and regardless of ordering
|
||||
(the probe runs before the seed — confirmed safe because `/api/me` has no DB dependency under
|
||||
bypass). The full suite passed with this gate live.
|
||||
- **Authelia-redirecting API → fails loudly.** With `redirect:'manual'`, a cross-origin 302 to
|
||||
Authelia surfaces as `type:'opaqueredirect'`, `status:0`, `ok:false` → gate throws. A
|
||||
same-origin redirect (e.g. `c.redirect('/')`) surfaces as `type:'basic'`, `status:302`,
|
||||
`ok:false` → also throws. Confirmed empirically against `/api/login` (302, `ok=false`).
|
||||
- **No false-fail in the supported setup:** in the dev-bypass stack the OIDC middleware is not
|
||||
mounted (`index.ts:51`), so `/api/me` always returns 200. No regression.
|
||||
|
||||
For ~23 days of the month "tomorrow" is inside `[monthStart−7, monthEnd+7)`. But when the suite
|
||||
runs on the **last 7 days of a month**, "tomorrow" rolls into the next month and lands **after**
|
||||
`monthEnd+7` → the seeded row is excluded by the API's date-window pre-filter
|
||||
(`events.ts:195-200`, the non-recurring timed branch requires `dtstartUtc < windowEnd`) → the
|
||||
`/api/events` response is `{ occurrences: [] }` → the grid renders empty.
|
||||
The error message string contains `opaqueredirect` with no space — cosmetic only (it is the exact
|
||||
`Response.type` token undici emits); not actionable.
|
||||
|
||||
Today this only *masks* itself because BL-01's assertions don't check for the event. But:
|
||||
- It is a real seed↔window contract violation: the deterministic seed is **not** deterministically
|
||||
inside the view the app fetches.
|
||||
- The moment BL-01 is fixed with a positive "seeded event is visible" assertion (as it must be),
|
||||
that assertion becomes **calendar-date-dependent** and will fail on roughly the last week of
|
||||
every month, plus any month-length edge (28/29/30/31). This is exactly the kind of
|
||||
"coincidentally-green / occasionally-red" flake the matrix is meant to eliminate.
|
||||
### WR-02 (RESOLVED in `9c38dd3`): explicit readiness flag
|
||||
|
||||
**Secondary correctness note:** the seed sets `all_day=false`, `dtstart_utc=<ts>`, and leaves
|
||||
`dtstart_date` NULL — this correctly matches the API's "non-recurring timed" WHERE branch
|
||||
(`events.ts:195-200`), so the *shape* is right. The problem is purely the *position in time*.
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:54-73`
|
||||
|
||||
**Fix:** Seed the event at a position guaranteed inside `initialCalendarRange()` independent of the
|
||||
run date — e.g. anchor it to "today at noon UTC" (today is always within the current-month ±7
|
||||
window) rather than +24h:
|
||||
The loop now uses an explicit `let ready = false` set inside the `res.ok` branch, and the
|
||||
post-loop check is `if (!ready) throw` — success is no longer inferred from `Date.now() >= deadline`.
|
||||
This removes both the false-positive-timeout (a success arriving in the final second can no longer
|
||||
be misreported as a timeout) and any false-positive-ready (the flag is only set on an actual
|
||||
`res.ok`). Timeout logic verified correct by reading; the gate ran green in the live suite.
|
||||
|
||||
```ts
|
||||
// Anchor inside the PWA's initial window (current month ± 7d) on every run date.
|
||||
const seedStart = new Date()
|
||||
seedStart.setUTCHours(12, 0, 0, 0) // noon today UTC — always inside the initial range
|
||||
```
|
||||
### WR-05 (RESOLVED in `2b745ad`): dropped redundant `unroute` calls
|
||||
|
||||
Document the coupling at the seed site: *"dtstart MUST stay inside
|
||||
calendarStore.initialCalendarRange() (current month ±7d) or calendar.spec populated assertions
|
||||
go dark."* This makes the seed↔window invariant explicit so it cannot drift silently (review item 2).
|
||||
**Files:** `apps/pwa/e2e/calendar.spec.ts:133-135, 154, 176`; `apps/pwa/e2e/lists.spec.ts:90-92, 118`
|
||||
|
||||
The trailing `page.unroute(...)` calls were removed and replaced with comments explaining that
|
||||
per-test context isolation handles cleanup. Verified this is correct, not a leak risk:
|
||||
- Every `page.route(...)` is registered **inside an individual test body**, never in a shared
|
||||
`beforeEach`/`beforeAll`. Playwright assigns each test a fresh `page`/`BrowserContext`, and route
|
||||
handlers are scoped to that page/context — they cannot leak into sibling tests.
|
||||
- The suite runs under `fullyParallel: true` with no `describe.serial`, so there is no shared-page
|
||||
path that could carry a route forward.
|
||||
- Cross-test isolation confirmed empirically: the populated-state calendar/lists tests (no mock)
|
||||
and the error/empty-state tests (with mock) all pass in the same run with no interference.
|
||||
|
||||
The removed `unroute` calls were genuinely dead — they never ran when an `expect` threw (the whole
|
||||
point of those tests), so they had guaranteed nothing. Dropping them is strictly an improvement.
|
||||
|
||||
### WR-06 (RESOLVED in `5322cfc`): self-validation comment corrected
|
||||
|
||||
**File:** `apps/pwa/e2e/layout.spec.ts:209-211, 250-252`
|
||||
|
||||
The misleading "remove by reload" comments now read "REMOVE the injected style by deleting the
|
||||
`<style>` element via evaluate (`styleHandle.evaluate(el => el.remove())` — no page reload)", which
|
||||
matches the actual code (`styleHandle.evaluate((el) => (el as Element).remove())`). Comment matches
|
||||
code. Trivial, confirmed.
|
||||
|
||||
### WR-07 (RESOLVED in `c564fc6`): SW-block test is now non-vacuous and honestly skips
|
||||
|
||||
**File:** `apps/pwa/e2e/calendar.spec.ts:41-68`
|
||||
|
||||
The test now (a) computes `swAvailable = 'serviceWorker' in navigator`, (b) `test.skip(!swAvailable, ...)`
|
||||
when absent, and (c) otherwise asserts `getRegistration()` resolves to `undefined`. Verified all three
|
||||
concerns live:
|
||||
|
||||
- **(a) Not vacuous on Chromium/pixel — AND not vacuous on WebKit/iphone either.** I probed both
|
||||
engines directly: `swAvailable=true` and `getRegistration()=undefined` on **both** `iphone`
|
||||
(WebKit) and `pixel` (Chromium) over `http://localhost`. So the genuine assertion runs on both
|
||||
profiles in this environment — `getRegistration()` is available and returns `undefined` under
|
||||
`serviceWorkers:'block'`. The SW test shows `✓ passed` (not `skipped`) on iphone, confirming the
|
||||
real assertion executed rather than being silently skipped.
|
||||
- **(b) `test.skip` is honest.** It is a real `test.skip(condition, reason)` that, when
|
||||
`serviceWorker` is genuinely absent (e.g. a future WebKit/runner where http://localhost is not a
|
||||
secure context), marks the test **skipped/visible** in the reporter — it does not let an
|
||||
unavailable API masquerade as a pass. In the current stack the skip branch is never taken, so it
|
||||
is correct dead-fallback, not a silent pass.
|
||||
- **(c) `getRegistration()` is the right probe under `serviceWorkers:'block'`.** With the block in
|
||||
effect no registration is ever created, so the promise resolves to `undefined`; if the block were
|
||||
lifted and the app registered `sw.js`, this would become a `ServiceWorkerRegistration` and the
|
||||
`toBeUndefined()` assertion would fail. This is a real, regression-sensitive signal (unlike the
|
||||
old `controller === null`, which was null on any first uncontrolled load regardless of the block).
|
||||
|
||||
No regression. The fix strictly strengthens the assertion.
|
||||
|
||||
---
|
||||
|
||||
## Warnings
|
||||
## Resolved / By-design (advisory — NOT actionable)
|
||||
|
||||
### WR-01 (CONFIRMED, deeper evidence): readiness gate accepts the SPA shell, not a working API/DB
|
||||
These were never code defects; they are design notes carried for traceability. None block shipping.
|
||||
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:50-65`; vite proxy `apps/pwa/vite.config.ts:server.proxy`
|
||||
|
||||
The gate polls `${baseURL}/health`. `baseURL` is the **Vite** origin (`:5173`), and
|
||||
`vite.config.ts` proxies `/health → http://localhost:3000`. So a 200 here *does* prove the API +
|
||||
DB round-trip (`apps/api/src/routes/health.ts` runs `SELECT 1`, returns 503 on DB failure) **and**
|
||||
that Vite is up and proxying — this is actually stronger than standard-depth credited. **However**,
|
||||
the gate does **not** prove `DEV_AUTH_BYPASS=true` is set *in the API process*. If the API was
|
||||
started without it, `/health` (unauthenticated, mounted before the guard — `index.ts:38`) still
|
||||
returns 200, the gate passes, the seed runs, then **every spec fails** at the first
|
||||
`/api/me`/`/api/events` (302 to Authelia). README §Prerequisites warns about this in prose but the
|
||||
harness cannot detect it. Low-cost hardening: after the seed, the gate could `fetch(baseURL +
|
||||
'/api/me', {redirect:'manual'})` and assert a 200 (dev-bypass) rather than an opaqueredirect, so a
|
||||
mis-started API fails *in setup* with a clear message instead of 40 confusing spec failures.
|
||||
|
||||
### WR-02 (CONFIRMED): readiness-gate success can be misreported as timeout near the deadline
|
||||
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:50-65`
|
||||
|
||||
The loop `break`s on `res.ok`, then line 60 re-checks `if (Date.now() >= deadline) throw`. If the
|
||||
successful `/health` response arrives in the final second (the `await fetch` itself can consume
|
||||
time), `Date.now()` may have crossed `deadline` by the time control reaches line 60 — throwing a
|
||||
false "health check never returned 200" **after a successful** health check. Use an explicit
|
||||
success flag instead of inferring success from the clock:
|
||||
|
||||
```ts
|
||||
let ready = false
|
||||
while (Date.now() < deadline) {
|
||||
try { const res = await fetch(`${baseURL}/health`); if (res.ok) { ready = true; break } }
|
||||
catch { /* keep polling */ }
|
||||
await new Promise((r) => setTimeout(r, 1_000))
|
||||
}
|
||||
if (!ready) throw new Error(`health check never returned 200 ...`)
|
||||
```
|
||||
|
||||
### WR-03 (CONFIRMED): `webServer` starts Vite but the proxy target (API:3000) is not managed → readiness gate is the only thing standing between "Vite up" and "specs fail"
|
||||
|
||||
**File:** `apps/pwa/playwright.config.ts:59-64`
|
||||
|
||||
`webServer` runs `pnpm --filter @familysync/pwa dev` (Vite only) — correct per D-10 (API + DB +
|
||||
Redis are compose-managed). But `webServer.url` is `:5173`; Playwright considers the server "ready"
|
||||
when Vite answers, **before** `globalSetup` polls `/health`. If the operator forgets the API,
|
||||
Playwright still launches; the failure is deferred to `globalSetup`'s 60s `/health` timeout. That
|
||||
is the intended contract (D-09: "you bring up the stack; the harness waits"), and the deferral is
|
||||
clean — keeping this as a WARNING only because `reuseExistingServer: !CI` (line 62) means in CI a
|
||||
fresh Vite is spawned that *also* needs the proxied API already up; the README §CI section relies on
|
||||
the CI job ordering the API before the runner. No code defect; documentation-coupling risk.
|
||||
|
||||
### WR-04 (DOWNGRADE → resolved-correct): `page.route('/api/lists')` exact match is correct, not too-narrow
|
||||
|
||||
**File:** `apps/pwa/e2e/lists.spec.ts:74, 96`
|
||||
|
||||
Standard-depth flagged the exact string `/api/lists` (no glob) as possibly missing the real request.
|
||||
Deep trace: `listsClient.fetchLists()` calls `apiFetch('/lists')` → `fetch('/api/lists')` with **no
|
||||
query string** (`listsClient.ts:12-21, 56-58`). Playwright resolves the bare path against `baseURL`
|
||||
→ `http://localhost:5173/api/lists`, which the exact matcher matches. There is no
|
||||
`/api/lists?foo` variant. The exact match is correct and *intentionally* narrow so it does **not**
|
||||
swallow `/api/lists/:id/items` (which would break if a glob were used). **No change needed** —
|
||||
recording the downgrade so it is not "fixed" into a brittle glob.
|
||||
|
||||
### WR-05 (CONFIRMED): `page.unroute` is not in a `finally` → a failing assertion leaks the mock to later tests
|
||||
|
||||
**Files:** `apps/pwa/e2e/calendar.spec.ts:116, 136, 159`; `apps/pwa/e2e/lists.spec.ts:92, 119`
|
||||
|
||||
Each error/empty-state test registers `page.route(...)` then calls `page.unroute(...)` at the end of
|
||||
the body. If any `expect` between them throws (the whole point of the test), `unroute` never runs.
|
||||
Playwright gives each test a **fresh page/context** by default, so route handlers do **not** leak
|
||||
across tests in practice — which is why this hasn't bitten. But the explicit `unroute` calls signal
|
||||
an *intent* to isolate that the code doesn't actually guarantee; under `fullyParallel` +
|
||||
`test.describe.serial` refactors, or if someone moves a `route` into `beforeEach`/`beforeAll`, the
|
||||
leak becomes real. Either drop the now-redundant `unroute` calls (rely on per-test context
|
||||
isolation) or wrap them in `try/finally`. Keeping as WARNING: dead-but-misleading cleanup code.
|
||||
|
||||
### WR-06 (CONFIRMED): self-validation "remove style by reload" comment is wrong; the test removes it via `evaluate`
|
||||
|
||||
**File:** `apps/pwa/e2e/layout.spec.ts:209-211, 250-251`
|
||||
|
||||
Comment says *"REMOVE the injected style by navigating (page.reload drops inline style tags)"* but
|
||||
the code removes it with `styleHandle.evaluate((el) => el.remove())` — no reload occurs. The code is
|
||||
correct; the comment is misleading and will send a maintainer down the wrong path. Fix the comment
|
||||
to describe the actual `evaluate(... .remove())` removal.
|
||||
|
||||
### WR-07 (CONFIRMED, deeper evidence): SW-controller assertion is near-vacuous on the WebKit (iPhone) profile
|
||||
|
||||
**File:** `apps/pwa/e2e/calendar.spec.ts:41-54`
|
||||
|
||||
`serviceWorkers: 'block'` is set on **both** profiles (`playwright.config.ts:44, 52`). The test reads
|
||||
`navigator.serviceWorker.controller` and asserts it is null. Two reasons it's weak:
|
||||
|
||||
1. On **WebKit over plain `http://localhost`**, `navigator.serviceWorker` is frequently
|
||||
**undefined** (SW requires a secure context; WebKit is stricter than Chromium about treating
|
||||
`localhost` as secure in emulation). The test's own guard (`if (!('serviceWorker' in navigator))
|
||||
return null`, line 50) then returns null and the assertion passes **without ever proving the
|
||||
block worked** — it passes because SW isn't available at all, not because it was blocked.
|
||||
2. Even on Chromium, `controller` is null on a first, uncontrolled load *regardless* of the
|
||||
`block` setting (a freshly-loaded page with a not-yet-activated SW also has null controller).
|
||||
|
||||
So this asserts "no controlling SW," which is true in the negative cases for reasons unrelated to
|
||||
`serviceWorkers:'block'`. To actually prove the config blocks registration, assert that
|
||||
`navigator.serviceWorker.getRegistration()` (where defined) resolves to `undefined`, and skip the
|
||||
test where `serviceWorker` is absent so an unavailable API doesn't masquerade as a passing block.
|
||||
- **WR-03 (by-design):** `webServer` manages Vite only; the API/DB/Redis are compose-managed per
|
||||
D-10. Playwright considers the server ready when Vite answers, before `globalSetup` polls
|
||||
`/health`; a missing API is deferred to the `/health` gate (now also the `/api/me` gate, WR-01).
|
||||
This is the intended D-09 contract. Documentation-coupling only.
|
||||
- **WR-04 (by-design):** `page.route('/api/lists')` exact-match is correct — `fetchLists()` requests
|
||||
the bare path with no query string, and the narrow matcher intentionally avoids swallowing
|
||||
`/api/lists/:id/items`. A glob would be brittle. No change.
|
||||
- **IN-01 (advisory):** `mysql2@3.22.4` is a PWA `devDependency` used only by the seed; correct
|
||||
placement (never bundled). Note: pinned independently from `apps/api`'s copy — keep in lockstep.
|
||||
- **IN-02 (advisory):** `tsconfig.e2e.json` `types:["node"]` + `lib:["DOM",...]` correctly types the
|
||||
Node seed while still typing `page.evaluate` DOM callbacks. `@playwright/test` types come via
|
||||
direct import. Sound.
|
||||
- **IN-03 (advisory):** vitest `exclude:['e2e/**']` and Playwright `testDir:'./e2e'` cleanly
|
||||
partition the two runners. Sound.
|
||||
- **IN-04 (advisory):** `typecheck` covers both tsconfigs (re-verified exit 0 this pass). Good.
|
||||
- **IN-05 (advisory):** the CR-01 guard protects *production*, not "the wrong dev DB" — pointing
|
||||
`DB_*` at a populated dev DB with `DEV_AUTH_BYPASS=true` will still TRUNCATE it. By design (D-06
|
||||
deterministic reseed) and documented. A defense-in-depth `E2E_ALLOW_TRUNCATE`/DB-name-pattern
|
||||
opt-in remains an optional hardening, not a defect.
|
||||
|
||||
---
|
||||
|
||||
## Info
|
||||
## Live-run evidence (iteration 2)
|
||||
|
||||
### IN-01: `mysql2` placed as a PWA `devDependency` — acceptable, with a caveat
|
||||
|
||||
**File:** `apps/pwa/package.json:38`
|
||||
|
||||
`mysql2@3.22.4` is a `devDependency` of `@familysync/pwa`, used only by `e2e/global-setup.ts`
|
||||
(`import mysql from 'mysql2/promise'`). It is also an `apps/api` dependency
|
||||
(`apps/api/package.json:24`) at the same pinned version. Placement is correct (the seed is
|
||||
dev/test-only and never bundled into the PWA — vitest excludes `e2e/**`, and Vite never imports it).
|
||||
Caveat: the version is pinned independently in two packages; if the API bumps `mysql2` and the
|
||||
harness doesn't, the seed could connect with a driver version skewed from the app's. Low risk
|
||||
(MariaDB wire protocol is stable) but worth a note to keep the two pins in lockstep.
|
||||
|
||||
### IN-02: `tsconfig.e2e.json` `types: ["node"]` correctly re-adds DOM via `lib`, but narrows ambient types
|
||||
|
||||
**File:** `apps/pwa/tsconfig.e2e.json:4-5`
|
||||
|
||||
The base `tsconfig.json` has **no** `types` field, so it includes all `@types/*` ambiently. The e2e
|
||||
config sets `types: ["node"]`, which *restricts* ambient `@types` to node only — intentional so the
|
||||
seed (Node `fetch`, `setTimeout`, `mysql2`) typechecks. DOM globals used in `page.evaluate`
|
||||
callbacks come from `lib: ["DOM", "DOM.Iterable"]` (line 5), which is correct because those
|
||||
callbacks are type-checked as DOM code. This is sound. Noting only that `@playwright/test` brings its
|
||||
own types via direct import (not ambient), so narrowing `types` doesn't break the specs. No action.
|
||||
|
||||
### IN-03: vitest `exclude: ['e2e/**']` correctly isolates Playwright specs from jsdom
|
||||
|
||||
**File:** `apps/pwa/vitest.config.ts:17`
|
||||
|
||||
Confirmed the exclusion prevents vitest from loading `e2e/*.spec.ts` (which import `@playwright/test`
|
||||
`devices`, unavailable in jsdom). The complementary direction is also covered: `playwright.config.ts`
|
||||
`testDir: './e2e'` + `testMatch: '**/*.spec.ts'` scopes Playwright to `e2e/` only, so it never picks
|
||||
up `src/**/*.test.tsx` vitest files. The two runners are cleanly partitioned. No action.
|
||||
|
||||
### IN-04: `typecheck` script covers the e2e tsconfig — good
|
||||
|
||||
**File:** `apps/pwa/package.json:10`
|
||||
|
||||
`typecheck` runs both `tsc --noEmit` and `tsc --project tsconfig.e2e.json --noEmit`, so the harness
|
||||
files are type-checked in CI (addresses the project-memory note "Vitest passes while tsc fails").
|
||||
The root `typecheck` (`package.json:12`, `pnpm -r typecheck`) fans this out. No action.
|
||||
|
||||
### IN-05: CR-01 guard protects *production*, not "the wrong dev DB"
|
||||
|
||||
**File:** `apps/pwa/e2e/global-setup.ts:34-44`
|
||||
|
||||
(See CR-01 residual note.) The guard refuses to run unless `DEV_AUTH_BYPASS=true` and
|
||||
`NODE_ENV!=='production'`. It does **not** distinguish a developer's *populated* local/staging
|
||||
MariaDB from a throwaway test DB — both satisfy the guard and both get TRUNCATEd. This is by design
|
||||
(D-06 reseed) and documented, but a defense-in-depth improvement would be to additionally require an
|
||||
explicit opt-in like `E2E_ALLOW_TRUNCATE=true` or assert the DB name matches a `*_test`/`*_e2e`
|
||||
pattern before truncating, so pointing `DB_*` at a real dev DB by accident doesn't silently wipe it.
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Full suite (both profiles) | 58 passed (55.0s) |
|
||||
| `iphone` SW-block test | ✓ passed (real assertion ran; not skipped) |
|
||||
| `pixel` SW-block test | ✓ passed |
|
||||
| Seeded-event DB→UI proof (iphone + pixel) | ✓ passed both |
|
||||
| `swAvailable` probe (both engines) | `true` / `getRegistration()=undefined` |
|
||||
| `redirect:'manual'` on a 302 | `ok=false` (gate throws — correct) |
|
||||
| `tsc --noEmit` + e2e tsconfig | exit 0 |
|
||||
|
||||
---
|
||||
|
||||
## Cross-File Soundness Matrix (assertion → real code traced)
|
||||
|
||||
| Spec assertion | Targets | Sound? | Notes |
|
||||
|---|---|---|---|
|
||||
| `getByRole('navigation', {name:'Main navigation'})` | `BottomTabBar.tsx:61-62` (`<nav aria-label>`) | YES | Only one nav landmark on mobile; DesktopNav nav hidden ≥768px |
|
||||
| Calendar/Lists tab ≥44px | `BottomTabBar.tsx` NavLink `minHeight:44px` + 56px bar | YES | Measures rendered geometry; self-validation proof present |
|
||||
| Settings button ≥44px, name `/open settings/i` | `AppNav.tsx:90-92` (`aria-label="${name} — open settings"`) | YES | matches |
|
||||
| `getByText('FamilySync', {exact:true})` | `AppNav.tsx:87` PhoneNav header text | YES | exact avoids "Install FamilySync" |
|
||||
| New Event FAB ≥56px, name `New Event` | `CalendarShell.tsx:467-470` (phone FAB) | YES | matches |
|
||||
| Error heading `Couldn't load events` + Retry ≥44px | `CalendarShell.tsx:354, 365-381` | YES | route-mock `/api/events*` → 500; matches `fetchEvents` URL `/api/events?...` |
|
||||
| `.sx-react-calendar-wrapper` visible "after seeding" | `CalendarShell.tsx:388` | **NO (BL-01)** | always rendered on success; not seed-dependent |
|
||||
| `Nothing here` count 0 when seeded | `EmptyState.tsx:45` (dead code) | **NO (BL-01)** | never rendered on /calendar; permanently green |
|
||||
| `Open list: E2E Grocery List` visible | `ListCard.tsx:60` (`aria-label`) | YES | seed list name matches; sound |
|
||||
| ≥1 `listitem` when seeded | `ListCard.tsx:52` (`role="listitem"`) | YES | the ONE real DB→UI proof in the suite |
|
||||
| `No lists yet` count 0 when seeded / visible when `[]` | `ListsEmptyState.tsx:46` via `ListsIndex.tsx:218` | YES | route-mock `/api/lists` → `{lists:[]}`; URL matches `fetchLists` |
|
||||
| `Tap + to create` visible | `ListsEmptyState.tsx:58` | YES | partial regex matches |
|
||||
| SW controller null | `serviceWorkers:'block'` | WEAK (WR-07) | near-vacuous on WebKit/http |
|
||||
| DEV_AUTH_BYPASS reached authed PWA | `devBypass.ts` + `index.ts:51-55` | YES | hostname stays localhost; nav appears post-auth |
|
||||
|
||||
---
|
||||
|
||||
_Reviewed: 2026-06-11T03:30:00Z_
|
||||
_Reviewed: 2026-06-11T12:30:00Z_
|
||||
_Reviewer: Claude (gsd-code-reviewer)_
|
||||
_Depth: deep_
|
||||
_Depth: deep (iteration 2 — --auto re-review)_
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- .gitea/workflows/runner-probe.yml
|
||||
autonomous: false
|
||||
requirements: [CI-01, CI-02]
|
||||
user_setup:
|
||||
- service: gitea-actions-runner
|
||||
why: "CI cannot run without a registered act_runner; 0 runners currently registered on git.bergerhouse.net"
|
||||
dashboard_config:
|
||||
- task: "Install + register act_runner on the Unraid host against git.bergerhouse.net, prefer Docker-executor mode (service containers require it)"
|
||||
location: "Unraid Community Applications → act_runner template; register with a runner-registration token from Gitea → Site Admin → Actions → Runners"
|
||||
- service: gitea-registry-pat
|
||||
why: "Publish job (CI-02) authenticates to the Gitea container registry; GITHUB_TOKEN/GITEA_TOKEN cannot push packages"
|
||||
env_vars:
|
||||
- name: GITEA_REGISTRY_PAT
|
||||
source: "Gitea → Settings → Applications → Generate Token with write:package (+ read:package) scope; add as repo secret GITEA_REGISTRY_PAT"
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A runner-probe workflow runs on the gsd/phase-08-gitea-ci branch and prints Node/pnpm versions, runner mode, Docker access, action resolution, and Playwright WebKit dep installability"
|
||||
- "The probe surfaces whether the runner is Docker-executor (services: works) or host-executor (docker run fallback needed) — the answer that forks W1/W2 DB bring-up"
|
||||
- "An act_runner is registered and visible in the Gitea Actions runners list (operator action)"
|
||||
- "A GITEA_REGISTRY_PAT repo secret with write:package scope exists (operator action)"
|
||||
artifacts:
|
||||
- path: ".gitea/workflows/runner-probe.yml"
|
||||
provides: "Probe-only workflow answering runner unknowns P-01..P-13"
|
||||
contains: "runner-probe"
|
||||
key_links:
|
||||
- from: ".gitea/workflows/runner-probe.yml"
|
||||
to: "self-hosted runner"
|
||||
via: "runs-on: self-hosted, on: push to gsd/phase-08-gitea-ci"
|
||||
pattern: "runs-on:\\s*self-hosted"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Establish the Gitea CI foundation by (a) registering the act_runner and creating the registry PAT (operator actions), and (b) landing a probe-only workflow that answers every runner unknown BEFORE any real test/build/publish step is trusted. This is Pitfall 12 (runner-probe-first) and the critical fork in 08-RESEARCH §Runner-Probe Checklist: several downstream design choices (service containers vs docker run, action resolution, reporter override, artifact upload fork, WebKit deps) depend on the probe's answers.
|
||||
|
||||
Purpose: De-risk every assumption (A1–A10 in 08-RESEARCH Assumptions Log) on the actual Unraid runner so Waves 1–2 are written against confirmed behavior, not guesses. Per D-03 the real CI lives in one ci.yml; the probe is a separate throwaway workflow on the feature branch.
|
||||
|
||||
Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored registry PAT.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-gitea-ci/08-CONTEXT.md
|
||||
@.planning/phases/08-gitea-ci/08-RESEARCH.md
|
||||
@.planning/research/PITFALLS.md
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
- `.gitea/workflows/runner-probe.yml` (NEW — this plan)
|
||||
- `.gitea/workflows/ci.yml` (NEW — Plans 02/03/04)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking-human">
|
||||
<name>Task 1: Register act_runner + create registry PAT (operator-only)</name>
|
||||
<what-built>Nothing automated — these are infrastructure actions outside the repo that the executor cannot perform (08-VALIDATION Manual-Only table; CI-01/CI-02 prerequisites).</what-built>
|
||||
<how-to-verify>
|
||||
1. On the Unraid host, install/register act_runner against https://git.bergerhouse.net using a runner-registration token from Gitea → Site Admin → Actions → Runners. PREFER Docker-executor mode — service containers (the MariaDB the CI needs) only work in Docker mode (08-RESEARCH §Critical fork, Assumption A1). If only host mode is available, that is acceptable; the probe (Task 2) will detect it and Waves 1–2 will use the docker-run fallback.
|
||||
2. Confirm the runner appears with status "idle"/online in Gitea → Site Admin → Actions → Runners.
|
||||
3. In Gitea → Settings → Applications, generate a token with `write:package` (and `read:package`) scope. Add it as a repository secret named `GITEA_REGISTRY_PAT` (repo → Settings → Actions → Secrets). Do NOT paste the token anywhere in the repo.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "runner registered" once the runner is online AND the GITEA_REGISTRY_PAT secret exists, or describe what is blocking (e.g. host-mode only).</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Author the runner-probe workflow</name>
|
||||
<files>.gitea/workflows/runner-probe.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Runner-Probe Checklist — the P-01..P-13 table is the canonical task list; §Critical fork Docker-vs-host)
|
||||
- .planning/research/PITFALLS.md (Pitfall 12 runner-probe-first, Pitfall 11 MariaDB-11 healthcheck)
|
||||
- apps/pwa/playwright.config.ts (reporter: 'github' under CI — probe must note whether annotations render)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `.gitea/workflows/runner-probe.yml` as a probe-only, non-destructive workflow. Trigger: `on: push` filtered to `branches: [gsd/phase-08-gitea-ci]` (runs on the current feature branch; never on main). `runs-on: self-hosted`.
|
||||
|
||||
The job MUST answer every check in 08-RESEARCH §Runner-Probe Checklist P-01..P-13. Implement each as a clearly-labeled step whose output is visible in the Gitea Actions log:
|
||||
- P-01 Node: `node --version` (note if not 22; then test `actions/setup-node@v4` with node-version 22 — P-08).
|
||||
- P-02 pnpm: `pnpm --version || (corepack enable pnpm && pnpm --version)`.
|
||||
- P-03 Runner mode (THE critical fork): print `cat /proc/1/cgroup | head -5`, `hostname`, and `ls -la /.dockerenv 2>&1` so the log shows whether the job runs in a Docker container (Docker-executor → services: works) or on bare host (host-executor → docker run fallback). State the conclusion explicitly in an `echo` line.
|
||||
- P-04 Docker socket: `docker info 2>&1 | head -20` and `docker ps 2>&1 | head`.
|
||||
- P-05 Service container spawn: add `services: mariadb: { image: mariadb:11, env: { MARIADB_ROOT_PASSWORD: root, MARIADB_DATABASE: familysync, MARIADB_USER: familysync, MARIADB_PASSWORD: testpass }, options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10 --health-start-period=30s }` to the probe job; a step runs `docker ps | grep -i maria || echo "no mariadb container visible (likely host mode)"`.
|
||||
- P-06 MariaDB reachability: try BOTH `mysql -h mariadb -P 3306 -u familysync -ptestpass -e "SELECT 1" 2>&1 | head` (Docker mode hostname) AND `mysql -h 127.0.0.1 ...` (host mode). Record which hostname resolves (do NOT fail the job if one path errors — capture both, `continue-on-error: true` on the step or `|| true`).
|
||||
- P-07 checkout: `uses: actions/checkout@v4` as the first real step; reaching subsequent steps proves it resolves.
|
||||
- P-08 setup-node: `uses: actions/setup-node@v4` with `node-version: '22'`; print resulting `node --version`.
|
||||
- P-09 cache: `uses: actions/cache@v4` with a throwaway key, wrapped `continue-on-error: true` — log whether it completes or hangs/times out (08-RESEARCH Pitfall 7).
|
||||
- P-10 Playwright WebKit deps: in `apps/pwa`, `npx playwright install --with-deps webkit chromium 2>&1 | tail -30` with `continue-on-error: true` — confirms WebKit system deps install without sudo/apt failure (A10).
|
||||
- P-11 artifact upload: write a dummy file and `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` (NOT actions/upload-artifact@v4 — broken on Gitea per 08-RESEARCH) with `continue-on-error: true`; note whether the artifact appears in the Gitea UI.
|
||||
- P-13 short SHA: `echo "short sha = ${GITHUB_SHA:0:7}"` — confirms the D-04 tag expression produces 7 chars.
|
||||
Do NOT include P-12 (docker login/push) here — defer registry login to Plan 04 to avoid exercising the PAT before the publish job is designed. Add a final summary step that echoes a one-line verdict per fork (Docker vs host mode; cache usable y/n; WebKit deps ok y/n; upload-artifact fork works y/n) so the SUMMARY can record the answers.
|
||||
|
||||
Keep the workflow non-destructive: no migrations, no pushes, no writes to main. All probe steps that may fail on this runner use `continue-on-error: true` or `|| true` so the probe reports findings instead of red-failing on an expected unknown.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .gitea/workflows/runner-probe.yml && grep -q "runs-on: self-hosted" .gitea/workflows/runner-probe.yml && grep -q "healthcheck.sh --connect --innodb_initialized" .gitea/workflows/runner-probe.yml && grep -q "ChristopherHX/gitea-upload-artifact@v4" .gitea/workflows/runner-probe.yml && ! grep -q "actions/upload-artifact@v4" .gitea/workflows/runner-probe.yml && ! grep -q "mysqladmin" .gitea/workflows/runner-probe.yml && echo PROBE_OK</automated>
|
||||
</verify>
|
||||
<done>runner-probe.yml exists, triggers only on the feature branch, uses `healthcheck.sh --connect --innodb_initialized` (never mysqladmin), uses the gitea-upload-artifact fork (never actions/upload-artifact@v4), and contains a step for each of P-01..P-11 + P-13.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Run the probe and record the fork answers</name>
|
||||
<what-built>The runner-probe workflow (Task 2), pushed to the gsd/phase-08-gitea-ci branch so the now-registered runner executes it.</what-built>
|
||||
<how-to-verify>
|
||||
1. Ensure the branch is pushed: `git push origin gsd/phase-08-gitea-ci` (this commit triggers the probe).
|
||||
2. Open Gitea → repo → Actions; find the "runner-probe" workflow run.
|
||||
3. Read the log and record the answers to the fork questions:
|
||||
- P-03: Docker-executor mode or host-executor mode? (drives Waves 1–2 DB bring-up)
|
||||
- P-05/P-06: did the MariaDB service container appear, and on which hostname (`mariadb` vs `127.0.0.1`)?
|
||||
- P-09: did actions/cache complete or hang? (cache optional decision)
|
||||
- P-10: did `playwright install --with-deps webkit` succeed? (WebKit feasibility)
|
||||
- P-11: did the gitea-upload-artifact fork upload successfully and appear in the UI?
|
||||
4. Confirm no secret/token is printed anywhere in the probe log (the probe must not touch the PAT).
|
||||
</how-to-verify>
|
||||
<resume-signal>Paste the fork answers (Docker vs host mode; service-container hostname; cache works y/n; WebKit deps y/n; artifact upload y/n) so the executor records them in the SUMMARY for Waves 1–2. Type "probe results recorded" to continue.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| CI workflow → self-hosted runner | Untrusted-ish: workflow YAML executes on operator infra with Docker socket access |
|
||||
| Repo secret store → workflow env | PAT crosses into the job; must never echo |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-01 | Information Disclosure | runner-probe.yml | mitigate | Probe NEVER references `secrets.GITEA_REGISTRY_PAT` or any secret; no `docker login` in the probe (P-12 deferred to Plan 04). Verified by checkpoint log audit. |
|
||||
| T-08-02 | Elevation of Privilege | Docker socket on runner | accept | Docker socket access is inherent to act_runner Docker-executor mode; accepted per Gitea self-hosted docs (08-RESEARCH Security Domain). |
|
||||
| T-08-SC | Tampering | gitea-upload-artifact@v4 (only new external action) | mitigate | [VERIFIED] in 08-RESEARCH Package Legitimacy Audit (github.com/ChristopherHX/gitea-upload-artifact) as the cited Gitea fix for the upload-artifact@v4 GHES block; pinned at @v4. All other actions are official GitHub/Docker actions. No [ASSUMED]/[SUS] packages → no install checkpoint required. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- runner-probe.yml present, branch-scoped, non-destructive; passes the Task 2 grep gate.
|
||||
- Probe run observed in Gitea Actions; fork answers recorded in SUMMARY.
|
||||
- Runner online; GITEA_REGISTRY_PAT secret created (operator confirmed).
|
||||
- No secret material printed in any probe log line.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Maps to CI-01/CI-02 prerequisites and Pitfall 12: the runner environment is probed BEFORE any real test/build/publish step is designed.
|
||||
- The Docker-vs-host fork (A1) is answered; the answer is recorded so Plans 02–04 pick the correct DB bring-up path.
|
||||
- Operator infra (runner + PAT) is in place.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-01-SUMMARY.md` when done. MUST record the probe fork answers (runner mode, service-container hostname, cache usable y/n, WebKit deps y/n, upload-artifact fork y/n) — Plans 02–04 consume them.
|
||||
</output>
|
||||
@@ -0,0 +1,185 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["08-01"]
|
||||
files_modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
autonomous: false
|
||||
requirements: [CI-01]
|
||||
must_haves:
|
||||
truths:
|
||||
- "Opening or updating a PR targeting main triggers ci.yml"
|
||||
- "A fast-checks job runs lint + typecheck (both apps) + PWA unit tests in parallel with the API job"
|
||||
- "An API job stands up a MariaDB 11 service container (or docker-run fallback), waits for real readiness via healthcheck.sh --connect --innodb_initialized, runs drizzle-kit migrate, then runs the DB-backed API test suite"
|
||||
- "Both jobs gate the PR — a failure in either blocks merge once required-checks branch protection is configured"
|
||||
artifacts:
|
||||
- path: ".gitea/workflows/ci.yml"
|
||||
provides: "PR-triggered fast-checks + API-integration jobs"
|
||||
contains: "pull_request"
|
||||
key_links:
|
||||
- from: ".gitea/workflows/ci.yml (api job)"
|
||||
to: "mariadb:11 service"
|
||||
via: "DB_HOST + drizzle-kit migrate + vitest"
|
||||
pattern: "healthcheck.sh --connect --innodb_initialized"
|
||||
- from: ".gitea/workflows/ci.yml (fast-checks job)"
|
||||
to: "pnpm scripts"
|
||||
via: "run: pnpm lint / typecheck / pwa test"
|
||||
pattern: "pnpm (-r )?(lint|typecheck)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the single CI workflow file `.gitea/workflows/ci.yml` and populate it with the two PR-gating jobs that need no browser: a fast-checks job (lint + typecheck both apps + PWA unit tests) running in parallel with an API job that runs the DB-backed API test suite against a MariaDB service container. This delivers the non-harness half of CI-01 (ROADMAP criteria 1 + 2) and Pitfall 11 (MariaDB-11 readiness).
|
||||
|
||||
Purpose: Fast PR feedback (D-03 — a lint failure does not wait behind the heavier jobs) plus a reliable cold-start API-integration gate. Uses the runner mode answer from 08-01-SUMMARY to choose service-container vs docker-run DB bring-up.
|
||||
|
||||
Output: `.gitea/workflows/ci.yml` containing `fast-checks` and `api` jobs gated on `pull_request → main`.
|
||||
|
||||
CRITICAL CONTEXT — read 08-01-SUMMARY first for the runner-mode fork:
|
||||
- If 08-01 found DOCKER-executor mode: use `services: mariadb:` with `DB_HOST: mariadb` (08-RESEARCH Pattern 1).
|
||||
- If 08-01 found HOST-executor mode: use a `docker run -d mariadb:11 -p 3306:3306` step + explicit readiness loop with `DB_HOST: 127.0.0.1` (08-RESEARCH Pattern 2). Service containers do NOT work in host mode (nektos/act#2711).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
- `.gitea/workflows/ci.yml` (NEW — this plan creates it; Plans 03/04 extend it)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
Confirmed repo facts the executor MUST honor (do not re-derive):
|
||||
- Root scripts: `lint` = `pnpm -r lint`, `typecheck` = `pnpm -r typecheck`, `test` = `pnpm --filter @familysync/api test` (= `vitest run`), PWA unit = `pnpm --filter @familysync/pwa test`.
|
||||
- IMPORTANT — lint is currently a NO-OP: no package defines a `lint` script and ESLint is not installed, so `pnpm lint` (`pnpm -r lint`) prints `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` but EXITS 0 and passes. Run `pnpm lint` as the documented command (satisfies CI-01's "lint" gate literally); do NOT add ESLint config — wiring lint is out of this phase's scope (CI-plumbing-only boundary). Note this in the SUMMARY so it is not mistaken for a bug.
|
||||
- ALL `apps/api` tests live in `apps/api/tests/` and `apps/api/test/setup.ts` truncates DB tables in an `afterEach` (it swallows errors if no DB). So `pnpm --filter @familysync/api test` REQUIRES a real MariaDB — the API "unit" and "integration" tests are one DB-backed command. The fast-checks job therefore runs only the PWA unit tests (no DB); the API job owns all API tests (with DB).
|
||||
- `apps/pwa` unit tests (`pnpm --filter @familysync/pwa test`) need NO DB.
|
||||
- DB env var names (from apps/api/src/db/client.ts + drizzle.config.ts): DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME. Migrations: `pnpm --filter @familysync/api db:migrate` (= drizzle-kit migrate). NEVER db:push (unsafe on MariaDB — project memory).
|
||||
- packageManager is `pnpm@11.5.1`; no .nvmrc/engines pin → pin Node 22 via `actions/setup-node@v4` + `corepack enable pnpm`.
|
||||
- Workspace is `apps/*` only (no packages/shared despite CLAUDE.md mention) — `pnpm -r` spans 2 packages.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create ci.yml with the fast-checks job</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (runner-mode fork answer; cache usable y/n)
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Architecture Patterns job topology; §Standard Stack action versions; Pitfall 7 cache)
|
||||
- package.json (root scripts: lint, typecheck, test:e2e)
|
||||
- apps/pwa/package.json (pwa test script)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `.gitea/workflows/ci.yml`. Header `name: CI`. Triggers: `on: { pull_request: { branches: [main] }, push: { branches: [main] } }` — both events declared now (the publish job in Plan 04 consumes the push event; the PR jobs filter to `pull_request`).
|
||||
|
||||
Add a workflow-level `env: { MILESTONE: v1.1 }` (per D-04; Plan 04 uses it).
|
||||
|
||||
Add the `fast-checks` job: `runs-on: self-hosted`, guarded `if: github.event_name == 'pull_request'`. Steps:
|
||||
1. `uses: actions/checkout@v4`
|
||||
2. `uses: actions/setup-node@v4` with `node-version: '22'`
|
||||
3. `run: corepack enable pnpm`
|
||||
4. Optional pnpm-store cache via `actions/cache@v4` ONLY if 08-01-SUMMARY reported cache works; otherwise OMIT the cache step entirely (do not add a hanging step). If included, wrap with `continue-on-error: true` (Pitfall 7).
|
||||
5. `run: pnpm install --frozen-lockfile`
|
||||
6. `run: pnpm lint` (no-op per interface_context, but the documented lint gate)
|
||||
7. `run: pnpm typecheck` (= `pnpm -r typecheck` → tsc --noEmit in both apps incl. pwa tsconfig.e2e.json)
|
||||
8. `run: pnpm --filter @familysync/pwa test` (PWA unit tests — no DB needed)
|
||||
Do NOT run `pnpm test` here (that is the DB-backed API suite — it belongs in the api job).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .gitea/workflows/ci.yml && grep -q "pull_request" .gitea/workflows/ci.yml && grep -q "node-version: '22'" .gitea/workflows/ci.yml && grep -q "pnpm typecheck" .gitea/workflows/ci.yml && grep -q "@familysync/pwa test" .gitea/workflows/ci.yml && echo FASTCHECKS_OK</automated>
|
||||
</verify>
|
||||
<done>ci.yml exists with a pull_request-gated fast-checks job pinning Node 22, enabling pnpm via corepack, running lint + typecheck + PWA unit tests; no DB-backed `pnpm test` in this job.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add the API job (MariaDB service + migrate + DB-backed tests)</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (Docker vs host mode — selects services: vs docker run; MariaDB hostname)
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 1 service container, §Pattern 2 host-mode fallback, §Pattern 4 Drizzle migrate; Pitfall 1 host-mode, Pitfall 2 mariadb healthcheck)
|
||||
- apps/api/test/setup.ts (confirms API tests need a real DB)
|
||||
- docker-compose.yml (MariaDB 11 healthcheck reference: healthcheck.sh --connect --innodb_initialized)
|
||||
</read_first>
|
||||
<action>
|
||||
Add an `api` job to ci.yml: `runs-on: self-hosted`, `if: github.event_name == 'pull_request'` (runs in PARALLEL with fast-checks — D-03; no `needs:` linking them).
|
||||
|
||||
DB bring-up — branch on 08-01-SUMMARY runner mode:
|
||||
- DOCKER mode: declare `services: mariadb:` with `image: mariadb:11`, env `{ MARIADB_ROOT_PASSWORD: root, MARIADB_DATABASE: familysync, MARIADB_USER: familysync, MARIADB_PASSWORD: testpass }`, and `options: >- --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10 --health-start-period=30s`. Set job `env.DB_HOST: mariadb`. (08-RESEARCH Pattern 1.) `--health-start-period=30s` because MariaDB 11 InnoDB init is slow (A9).
|
||||
- HOST mode: instead, a first step `docker run -d --name mariadb -e MARIADB_ROOT_PASSWORD=root -e MARIADB_DATABASE=familysync -e MARIADB_USER=familysync -e MARIADB_PASSWORD=testpass -p 3306:3306 mariadb:11`, then an explicit readiness-loop step using `docker exec mariadb healthcheck.sh --connect --innodb_initialized` with a ~90s deadline (08-RESEARCH Pattern 2). Set `env.DB_HOST: 127.0.0.1`.
|
||||
|
||||
Regardless of mode, set job-level `env`: DB_PORT: 3306, DB_USER: familysync, DB_PASSWORD: testpass, DB_NAME: familysync (throwaway creds — NEVER reuse production secrets; T-08-03).
|
||||
|
||||
Even in Docker mode (where options: auto-waits), add an explicit readiness step BEFORE migrate: a loop that polls `healthcheck.sh --connect --innodb_initialized` (in Docker mode, via a one-shot `mariadb:11` client container or `mysql -h $DB_HOST ... -e "SELECT 1"`) with a deadline — Pitfall 11: healthy-in-Docker ≠ accepting-connections, and the cold-first-run reliability is ROADMAP criterion 2. Never use `mysqladmin ping` (removed in MariaDB 11).
|
||||
|
||||
Then steps:
|
||||
- `uses: actions/checkout@v4`; `uses: actions/setup-node@v4` (node 22); `corepack enable pnpm`; `pnpm install --frozen-lockfile`.
|
||||
- `run: pnpm --filter @familysync/api db:migrate` (drizzle-kit migrate — applies repo SQL; NEVER db:push). Pass DB_* env.
|
||||
- `run: pnpm --filter @familysync/api test` (the full DB-backed API suite). Pass DB_* env.
|
||||
|
||||
Reuse the same cache decision as Task 1 (include only if 08-01 confirmed cache works).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "mariadb:11" .gitea/workflows/ci.yml && grep -q "healthcheck.sh --connect --innodb_initialized" .gitea/workflows/ci.yml && ! grep -q "mysqladmin" .gitea/workflows/ci.yml && grep -q "db:migrate" .gitea/workflows/ci.yml && ! grep -q "db:push" .gitea/workflows/ci.yml && grep -q "@familysync/api test" .gitea/workflows/ci.yml && echo APIJOB_OK</automated>
|
||||
</verify>
|
||||
<done>ci.yml has a parallel pull_request-gated api job that brings up MariaDB 11 (services: or docker run per runner mode), waits for real readiness via healthcheck.sh (never mysqladmin), runs drizzle-kit migrate (never push), and runs the DB-backed API test suite with throwaway creds.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify fast-checks + api jobs on a PR</name>
|
||||
<what-built>ci.yml with parallel fast-checks + api jobs (Tasks 1–2), exercised by opening a PR from gsd/phase-08-gitea-ci → main.</what-built>
|
||||
<how-to-verify>
|
||||
1. Push the branch and open a PR targeting `main`.
|
||||
2. In Gitea → Actions, confirm BOTH `fast-checks` and `api` jobs are triggered and run in parallel.
|
||||
3. Confirm the api job passes on a COLD first run (ROADMAP criterion 2) — not only on re-run. If it fails with ECONNREFUSED to 3306, the MariaDB readiness wait is too short; lengthen the deadline / start-period (Pitfall 11) rather than re-running.
|
||||
4. Confirm fast-checks runs lint (no-op), typecheck, and PWA unit tests green.
|
||||
5. (Operator, optional but recommended) Configure branch protection on `main` → required status checks include these jobs, so a failure actually blocks merge (08-VALIDATION Manual-Only).
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "W1 green" once both jobs pass on a cold PR run, or paste the failing log.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| PR head → CI runner | PR-triggered job runs untrusted branch content on operator infra |
|
||||
| Test DB creds → job env | Throwaway creds in CI env; must not be production secrets |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-03 | Information Disclosure | MariaDB creds in job env | mitigate | Use throwaway creds (familysync/testpass, root/root) scoped to the ephemeral service container only; NEVER reference production DB_PASSWORD or any repo secret in these jobs (08-RESEARCH Security Domain). |
|
||||
| T-08-04 | Tampering | drizzle-kit against CI DB | mitigate | Use `db:migrate` (applies committed SQL) exclusively; `db:push` is forbidden (emits destructive TRUNCATE diff on MariaDB — project memory drizzle-mariadb-push-unsafe). Verified by grep gate (`! grep db:push`). |
|
||||
| T-08-05 | Denial of Service | cold-start readiness race | mitigate | Explicit healthcheck.sh readiness loop before migrate (Pitfall 11) so the gate is reliable on first run, not flaky. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- ci.yml passes both Task grep gates (service container + readiness + migrate-not-push; fast-checks node-pin + typecheck + pwa test).
|
||||
- PR run shows fast-checks ∥ api in parallel; api green on cold first run.
|
||||
- No production secret referenced in either job.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CI-01 (non-harness half): PR to main runs lint + typecheck (both apps) + unit tests + API integration vs MariaDB service container; failures gate merge (ROADMAP criteria 1 + 2).
|
||||
- Pitfall 11 honored: healthcheck.sh --connect --innodb_initialized readiness, never mysqladmin; reliable cold-start.
|
||||
- One workflow file (D-03), parallel event-gated jobs.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-02-SUMMARY.md` when done. Record: the chosen DB bring-up path (services vs docker-run), final readiness timeout values, whether cache was enabled, and the lint-is-a-no-op note.
|
||||
</output>
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
- `.gitea/workflows/ci.yml` (EXTENDED — adds the harness job; created in Plan 02)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
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.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add the harness job — DB + migrate + API background process + readiness</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .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)
|
||||
</read_first>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Playwright install + run (both profiles) + artifact upload on failure</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- 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?)
|
||||
</read_first>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<done>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.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify the harness job on the PR</name>
|
||||
<what-built>The harness job (Tasks 1–2) added to ci.yml, exercised on the open PR to main.</what-built>
|
||||
<how-to-verify>
|
||||
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.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "harness green" once both device profiles pass against the CI-brought-up stack and artifact upload is confirmed, or paste the failing log.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## 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). |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- 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.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
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.
|
||||
</output>
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["08-03"]
|
||||
files_modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
autonomous: false
|
||||
requirements: [CI-02]
|
||||
user_setup:
|
||||
- service: gitea-registry-pat
|
||||
why: "Publish job authenticates to the Gitea container registry; created in Plan 01"
|
||||
env_vars:
|
||||
- name: GITEA_REGISTRY_PAT
|
||||
source: "Repo secret created in Plan 01 (write:package scope)"
|
||||
must_haves:
|
||||
truths:
|
||||
- "A merge (push) to main triggers a publish job that builds the API Docker production image and pushes it to the Gitea container registry"
|
||||
- "The image is pushed under two tags: :latest and :<milestone>-<shortsha> (e.g. v1.1-<7charsha>)"
|
||||
- "Registry authentication uses docker login --password-stdin with the PAT piped from a repo secret — the token never appears in plaintext in the CI log"
|
||||
- "The publish job runs only on push to main, never on pull_request, and never carries DEV_AUTH_BYPASS"
|
||||
artifacts:
|
||||
- path: ".gitea/workflows/ci.yml"
|
||||
provides: "push-to-main publish job (CI-02)"
|
||||
contains: "docker push"
|
||||
key_links:
|
||||
- from: ".gitea/workflows/ci.yml (publish job)"
|
||||
to: "git.bergerhouse.net registry"
|
||||
via: "docker login --password-stdin + docker build --target production + docker push"
|
||||
pattern: "--password-stdin"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the publish job to `.gitea/workflows/ci.yml`: on merge (push) to `main`, build the API Docker `production` image and push it to the Gitea container registry under `:latest` and `:<milestone>-<shortsha>`, authenticating with the operator PAT via `--password-stdin` so the credential never hits the log. This delivers CI-02 (ROADMAP criteria 5 + 6) and Pitfall 13 (--password-stdin).
|
||||
|
||||
Purpose: Every merge to main produces an immutable, traceable image (D-04) plus a moving :latest pointer, with zero credential exposure (ROADMAP criterion 6 is a hard requirement).
|
||||
|
||||
Output: a `publish` job in ci.yml gated on `push → main`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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/api/Dockerfile
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
- `.gitea/workflows/ci.yml` (EXTENDED — adds the publish job; closes the phase)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
Confirmed facts (do not re-derive):
|
||||
- Git remote: `https://git.bergerhouse.net/luckberg/familysync.git` → registry host `git.bergerhouse.net`, owner `luckberg`. Image: `git.bergerhouse.net/luckberg/familysync-api` (08-RESEARCH §Registry Details).
|
||||
- Dockerfile is multi-stage with a `production` target that builds API + PWA and serves both on :3000. It MUST be built from the REPO ROOT with `-f apps/api/Dockerfile .` (the Dockerfile header and 08-RESEARCH §Dockerfile Build Context say so — building from apps/api/ fails because it copies the root workspace manifest + lockfile).
|
||||
- Milestone = `v1.1` (PROJECT.md "Current Milestone"). Per D-04, expose it as the workflow-level `env.MILESTONE` (already added in Plan 02) rather than hardcoding inline; update at milestone boundaries.
|
||||
- Short SHA = `${GITHUB_SHA:0:7}` (CONFIRMED available in Gitea Actions; 08-RESEARCH). If 08-01-SUMMARY found GITHUB_SHA unavailable, fall back to `git rev-parse --short=7 HEAD`.
|
||||
- Registry auth: PAT with write:package in repo secret `GITEA_REGISTRY_PAT` (created Plan 01). `GITHUB_TOKEN`/`GITEA_TOKEN` CANNOT push packages (08-RESEARCH; Gitea forum) — must use the PAT. Username = `luckberg`.
|
||||
- Pitfall 13: NEVER `docker login -p $TOKEN` (token leaks to the log / process list). ALWAYS `echo "$PAT" | docker login git.bergerhouse.net -u luckberg --password-stdin`.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add the publish job (build + tag + login --password-stdin + push)</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 6 Docker publish; §Docker Registry Push; §Image Tag Strategy D-04; Pitfall on GITHUB_TOKEN)
|
||||
- .planning/research/PITFALLS.md (Pitfall 13 --password-stdin)
|
||||
- apps/api/Dockerfile (production target; build-from-root requirement)
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (docker socket access confirmed; GITHUB_SHA availability)
|
||||
</read_first>
|
||||
<action>
|
||||
Add a `publish` job to ci.yml: `runs-on: self-hosted`, guarded `if: github.event_name == 'push' && github.ref == 'refs/heads/main'` (push-to-main ONLY — never pull_request; D-03). It runs independently of the PR jobs (those are pull_request-gated and won't fire on push). Do NOT set DEV_AUTH_BYPASS anywhere in this job (T-08-06 boundary).
|
||||
|
||||
Steps:
|
||||
1. `uses: actions/checkout@v4`.
|
||||
2. Compute tags (id: tags). Derive `SHORT_SHA=${GITHUB_SHA:0:7}` (fallback `git rev-parse --short=7 HEAD` if 08-01 flagged GITHUB_SHA missing). Use the workflow-level `${{ env.MILESTONE }}` (= v1.1). Emit two outputs:
|
||||
`latest=git.bergerhouse.net/luckberg/familysync-api:latest`
|
||||
`sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}`
|
||||
(write to `$GITHUB_OUTPUT`).
|
||||
3. Docker login via stdin (Pitfall 13 — the load-bearing security step):
|
||||
`echo "${{ secrets.GITEA_REGISTRY_PAT }}" | docker login git.bergerhouse.net --username luckberg --password-stdin`
|
||||
NEVER use `-p`/`--password` with the token as an argument. Do not `echo` the secret anywhere else; do not set it as a plain env var.
|
||||
4. Build + push from REPO ROOT:
|
||||
`docker build --target production -f apps/api/Dockerfile -t <latest> -t <sha_tag> .`
|
||||
then `docker push <latest>` and `docker push <sha_tag>`.
|
||||
5. Final `if: always()` step: `docker logout git.bergerhouse.net || true` to drop the stored credential from the runner after push.
|
||||
|
||||
Use `docker/login-action`/`docker/build-push-action` ONLY if 08-01-SUMMARY confirmed they resolve AND you prefer them; the shell `docker login --password-stdin` + `docker build`/`docker push` form is the safer first iteration (08-RESEARCH §Pattern 6 note) and is the recommended path.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "github.event_name == 'push'" .gitea/workflows/ci.yml && grep -q "refs/heads/main" .gitea/workflows/ci.yml && grep -q -- "--password-stdin" .gitea/workflows/ci.yml && ! grep -E "docker login.*(-p |--password )[^-]" .gitea/workflows/ci.yml && grep -q "docker build --target production" .gitea/workflows/ci.yml && grep -q "familysync-api:latest" .gitea/workflows/ci.yml && grep -q 'familysync-api:${MILESTONE}' .gitea/workflows/ci.yml && grep -q "docker push" .gitea/workflows/ci.yml && ! grep -qi "DEV_AUTH_BYPASS" <(awk '/publish:/,0' .gitea/workflows/ci.yml) && echo PUBLISH_OK</automated>
|
||||
</verify>
|
||||
<done>The publish job runs only on push to main, logs in with --password-stdin (never -p), builds the production target from repo root, pushes :latest and :${MILESTONE}-<shortsha>, logs out, and never sets DEV_AUTH_BYPASS.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 2: Merge, audit the publish log, and verify both tags</name>
|
||||
<what-built>The publish job (Task 1). Verifying it requires merging the PR to main and auditing the resulting CI log + registry — the executor cannot merge a protected branch.</what-built>
|
||||
<how-to-verify>
|
||||
1. Merge the PR (all PR jobs green) into `main` — the push triggers the publish job.
|
||||
2. In Gitea → Actions, open the publish job log and AUDIT it line by line: the PAT must NOT appear in plaintext anywhere (ROADMAP criterion 6 — hard requirement). The `docker login` line should show `--password-stdin`, never the token. If the token is visible, STOP — rotate the PAT and fix before anything else.
|
||||
3. Confirm the build used `--target production -f apps/api/Dockerfile .` and succeeded.
|
||||
4. In Gitea → repo → Packages, confirm `familysync-api` exists with BOTH tags: `latest` and `v1.1-<7charsha>` matching the merge commit.
|
||||
5. (Optional) `docker pull git.bergerhouse.net/luckberg/familysync-api:latest` from a machine with registry access to confirm the image is pullable.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "publish verified" once both tags exist in the registry AND the log audit confirms no plaintext PAT, or describe the failure.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Repo secret → docker login | PAT crosses into the job; the single highest-value secret in this phase |
|
||||
| publish job → registry | Authenticated push to the package registry |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-08-PAT | Information Disclosure | GITEA_REGISTRY_PAT in publish job | mitigate | `docker login --password-stdin` exclusively — token piped via stdin, never an `-p`/`--password` argument (Pitfall 13). Token referenced only as `${{ secrets.GITEA_REGISTRY_PAT }}` (Gitea masks registered secrets in logs); never echoed elsewhere; `docker logout` after push. Grep gate forbids `-p`/`--password` forms. Checkpoint requires a line-by-line log audit (ROADMAP criterion 6). This is the load-bearing mitigation for the phase. |
|
||||
| T-08-09 | Spoofing | DEV_AUTH_BYPASS bleed into publish | mitigate | Publish job never sets DEV_AUTH_BYPASS (grep gate scoped to the publish: block); the bypass is confined to the harness job (Plan 03). |
|
||||
| T-08-10 | Tampering | wrong build context | mitigate | Build from repo root with `-f apps/api/Dockerfile .` (Dockerfile requires root context for the workspace manifest + lockfile); building from apps/api/ would fail or produce a broken image. |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- ci.yml passes the Task grep gate (push-to-main guard, --password-stdin, no -p, production target from root, both tags, no DEV_AUTH_BYPASS in publish block).
|
||||
- After merge: both tags present in the Gitea registry; log audit shows no plaintext PAT.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CI-02: on merge to main, the API production image is built and pushed to the Gitea registry under :latest + :v1.1-<shortsha> (ROADMAP criterion 5; D-04).
|
||||
- Registry credentials never appear in plaintext in the CI logs (ROADMAP criterion 6; Pitfall 13) — the load-bearing security outcome of the phase.
|
||||
- Publish runs only on push to main; DEV_AUTH_BYPASS never bleeds into it.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-04-SUMMARY.md` when done. Record: the final image name + both tags pushed, confirmation the log audit found no plaintext PAT, and whether the shell or docker/* action form was used.
|
||||
</output>
|
||||
@@ -0,0 +1,112 @@
|
||||
# Phase 8: Gitea CI - Context
|
||||
|
||||
**Gathered:** 2026-06-11
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Phase 8 adds CI on the existing self-hosted **Gitea Actions** runner. Two outcomes:
|
||||
|
||||
1. **PR regression gate** — every PR targeting `main` runs lint, typecheck (both apps), unit tests, API-integration tests against a MariaDB service container, **and the Phase 7 mobile Playwright harness** (against a CI-brought-up dev stack with `DEV_AUTH_BYPASS=true`). Any failure blocks the merge.
|
||||
2. **Publish on merge** — a push to `main` builds and publishes the API Docker image to the Gitea container registry.
|
||||
|
||||
This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readiness waits, image build/push. It does **not** modify the Phase 7 harness specs (CI reuses them unchanged), the Dockerfile (already multi-stage, builds API + PWA), or application code. Requirements: **CI-01, CI-02**.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Dev-stack bring-up in CI (for the harness step)
|
||||
- **D-01:** Bring up the stack with **bare background processes + a MariaDB service container** — NOT docker compose, NOT a production image.
|
||||
- MariaDB runs as a Gitea **service container** (the same one the API-integration job needs; `DB_HOST=127.0.0.1`, service creds).
|
||||
- The **API** runs as a background process via `pnpm dev:api` (or equivalent) with `DEV_AUTH_BYPASS=true` and `DB_HOST=127.0.0.1`, listening on `:3000`.
|
||||
- The **PWA** Vite dev server is started by **Playwright's own `webServer`** config (already present; `reuseExistingServer: !process.env.CI`), on `:5173`. Vite proxies `/api`, `/health`, `/callback` → `:3000`.
|
||||
- Rationale: no docker-in-docker on the self-hosted runner; matches the Phase 7 dev-server harness contract exactly; reuses the MariaDB service container already required for integration tests.
|
||||
- **D-02:** The harness step MUST wait for **both** the API (`:3000`) and the PWA Vite server (`:5173`) to accept connections before Playwright launches. The harness already polls `baseURL/health` (proxied to the API) in `global-setup.ts`; CI must additionally ensure the API process is up first. This is on top of the MariaDB-11 readiness loop (Pitfall 11 — `healthcheck.sh --connect --innodb_initialized`, never `mysqladmin ping`).
|
||||
|
||||
### Workflow topology & jobs
|
||||
- **D-03:** **One workflow file with parallel, event-gated jobs.**
|
||||
- `pull_request` → `main`: fast-checks job (lint + typecheck both apps + unit tests) runs **in parallel** with the heavier API-integration job and the harness job. Fast feedback — a lint failure does not wait behind the harness.
|
||||
- `push` → `main` (merge): build-and-publish job runs.
|
||||
- Single file so the whole regression + publish story lives in one place; accept the minor setup duplication (checkout, pnpm cache, Node-22 pin) across jobs.
|
||||
|
||||
### Docker image tag strategy (CI-02)
|
||||
- **D-04:** On merge to `main`, publish the API image with **two tags**: `:latest` (moving pointer) **and** `:<milestone>-<shortsha>` (immutable, e.g. `v1.1-4303a1b`).
|
||||
- The milestone string (e.g. `v1.1`) is read from PROJECT.md / ROADMAP.md, not hardcoded inline if avoidable.
|
||||
- `<shortsha>` is the short commit SHA of the merge commit.
|
||||
- Rationale: `:latest` for easy pulls; the milestone-prefixed SHA tag groups builds by release line and stays immutable for rollback/traceability.
|
||||
|
||||
### Failure artifacts & browser matrix
|
||||
- **D-05:** Run **both** device profiles in CI — iPhone 14/WebKit **and** Pixel 7/Chromium (the full Phase 7 matrix). Install whatever system deps WebKit needs on the runner (probe in the runner-probe step).
|
||||
- **D-06:** On harness **failure**, upload Playwright **traces / screenshots / videos** as CI artifacts for debugging. The config already emits `trace`/`video` `on-first-retry` and `screenshot: only-on-failure`; CI must upload the `test-results/` output. Note the config's `reporter: 'github'` may not render natively in Gitea Actions — verify during the runner probe and fall back to `list`/`html` if annotations don't surface.
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact job names, step ordering within a job, pnpm store cache key strategy, and whether fast-checks is one job or split — planner/executor decide.
|
||||
- Whether the API background process is launched with `pnpm dev:api` vs a built `node dist` — pick whatever gives reliable `:3000` readiness under `DEV_AUTH_BYPASS`; the harness only needs the authed PWA reachable (Dev User 1 has no CalDAV creds, so verify layout/flows, not live event-create).
|
||||
- Registry hostname / image repository path under the Gitea registry.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Phase scope & requirements
|
||||
- `.planning/ROADMAP.md` §"Phase 8: Gitea CI" — goal, 6 success criteria, pitfalls this phase owns.
|
||||
- `.planning/REQUIREMENTS.md` — CI-01 (PR regression incl. harness), CI-02 (publish image on merge).
|
||||
- `.planning/PITFALLS.md` — Pitfalls 11 (MariaDB-11 readiness), 12 (runner-probe first), 13 (`--password-stdin`), 15 (SW block, harness side).
|
||||
|
||||
### Harness the CI step runs (reused unchanged from Phase 7)
|
||||
- `apps/pwa/playwright.config.ts` — device matrix, `serviceWorkers: 'block'`, `webServer` (Vite-only, `reuseExistingServer: !CI`), `retries`/`workers`/`reporter` under `CI`, env-driven `PLAYWRIGHT_BASE_URL`.
|
||||
- `apps/pwa/e2e/global-setup.ts` — `/health` readiness poll, `/api/me` DEV_AUTH_BYPASS reachability gate, fail-closed env guard (refuses `NODE_ENV=production` or missing `DEV_AUTH_BYPASS`), mysql2 truncate-and-seed (calendar id 10, lists/items for user 1).
|
||||
- `.planning/phases/07-mobile-test-harness/07-CONTEXT.md` — Phase 7 decisions D-01..D-10 (auth strategy, SW block, env baseURL, compose-managed backend).
|
||||
|
||||
### Infra the CI builds/runs against
|
||||
- `apps/api/Dockerfile` — multi-stage: `builder` (API), `pwa-builder` (PWA dist → `./public`), `production` target. CI publishes the `production` target.
|
||||
- `docker-compose.yml` / `docker-compose.dev.yml` — service shape, MariaDB 11 healthcheck (`healthcheck.sh --connect --innodb_initialized`), dev override exposing 3306, API `dev` build target.
|
||||
- `apps/pwa/vite.config.ts` — dev proxy (`/api`, `/health`, `/callback` → `:3000`) the harness depends on.
|
||||
- `package.json` (root) — scripts: `dev:api`, `dev:pwa`, `test`, `test:e2e`, `lint`, `typecheck`.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- **Playwright config + global-setup (Phase 7):** ready to run headlessly in CI. `retries: 2`, `workers: 1`, `reporter: 'github'` already gated on `process.env.CI`. CI sets `CI=true` and `PLAYWRIGHT_BASE_URL` and the harness behaves correctly. No spec changes.
|
||||
- **MariaDB service-container pattern:** API-integration tests already require a real MariaDB with `DB_HOST=127.0.0.1` + service creds + Drizzle `generate`+`migrate` for schema. The harness's `global-setup` seeds the same DB directly via mysql2. One MariaDB service container can back both the integration job and the harness job.
|
||||
- **Multi-stage Dockerfile:** `production` target already builds API + PWA and serves both on `:3000`. CI build/push is a thin wrapper (`docker build --target production` + `docker login --password-stdin` + `docker push`).
|
||||
|
||||
### Established Patterns
|
||||
- **Gitea, not GitHub:** origin is self-hosted Gitea; `main` is protected (PRs only). Gitea Actions is GitHub-Actions-syntax-compatible but **do not assume `actions/setup-node` behaves identically** — runner-probe first (Pitfall 12), pin Node 22 explicitly.
|
||||
- **node-cron lesson (long-running process):** not directly relevant to CI, but the API in CI is short-lived/background — no scheduler concerns.
|
||||
|
||||
### Integration Points
|
||||
- CI orchestrates, in order, for the harness job: MariaDB service container (readiness loop) → Drizzle generate+migrate → API background process (`DEV_AUTH_BYPASS=true`, `:3000`, readiness wait) → Playwright (`webServer` starts Vite `:5173`, `global-setup` polls `/health` + `/api/me`) → specs → upload artifacts on failure.
|
||||
- Publish job depends on `apps/api/Dockerfile` `production` target + Gitea registry credentials (PAT with `write:package`, piped via `--password-stdin`).
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Image tag format locked to `:latest` + `:v1.1-<shortsha>` (milestone prefix + short SHA). Example: `v1.1-4303a1b`.
|
||||
- Start the very first CI iteration as a **runner-probe** only: `node --version` / `pnpm --version` / Docker access / WebKit dep availability on the `self-hosted` runner — before any real test/build steps are designed.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **ROADMAP status fix:** ROADMAP.md line 29 marks Phase 8 "completed 2026-06-11" while line 204 says "Not started" and no Phase 8 artifacts exist. This is a bookkeeping error to correct (Phase 8 is being started now) — a docs/roadmap cleanup, not Phase 8 scope.
|
||||
- **Desktop e2e coverage:** moved to backlog **Phase 999.15** — Phase 8 CI gates mobile only; adding a Desktop Playwright profile + desktop-safe specs is out of CI-plumbing scope.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 8-Gitea CI*
|
||||
*Context gathered: 2026-06-11*
|
||||
@@ -0,0 +1,74 @@
|
||||
# Phase 8: Gitea CI - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Phase:** 8-Gitea CI
|
||||
**Areas discussed:** Dev-stack bring-up in CI, Workflow topology & jobs, Docker image tag strategy, Failure artifacts & browser matrix
|
||||
|
||||
---
|
||||
|
||||
## Dev-stack bring-up in CI
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Bare processes + service MariaDB | MariaDB as a Gitea service container; API as background `pnpm dev:api` with DEV_AUTH_BYPASS; Playwright webServer starts Vite. No docker-in-docker. | ✓ |
|
||||
| docker compose dev override | Run docker-compose.yml + dev override inside the runner; adds image-build time + docker-in-docker. | |
|
||||
| Production image for harness | Build prod image, run harness against API-served PWA on :3000; diverges from Phase 7 dev-server contract. | |
|
||||
|
||||
**User's choice:** Bare processes + service MariaDB
|
||||
**Notes:** Reuses the MariaDB service container already needed by the API-integration tests; matches the Phase 7 harness contract exactly.
|
||||
|
||||
---
|
||||
|
||||
## Workflow topology & jobs
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| One file, parallel jobs | Single workflow; PR→main runs fast-checks in parallel with integration + harness; push→main publishes. | ✓ |
|
||||
| Two files | Separate ci.yml + publish.yml; cleaner split, duplicated setup. | |
|
||||
| One file, linear job | Single sequential job; simplest, slowest feedback. | |
|
||||
|
||||
**User's choice:** One file, parallel jobs
|
||||
**Notes:** Fast feedback prioritized; minor setup duplication across jobs accepted.
|
||||
|
||||
---
|
||||
|
||||
## Docker image tag strategy
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| latest + short SHA | :latest + :<short-sha> | |
|
||||
| Short SHA only | Immutable per-commit only | |
|
||||
| latest only | Single moving tag | |
|
||||
| semver from package.json | Version field + latest | |
|
||||
| **latest + milestone short (custom)** | :latest + :<milestone>-<shortsha>, e.g. v1.1-4303a1b | ✓ |
|
||||
|
||||
**User's choice:** latest + `v1.1-4303a1b` (milestone prefix + short SHA), confirmed in follow-up over a 3-tag variant and a no-milestone variant.
|
||||
**Notes:** Milestone string read from PROJECT.md/ROADMAP, not hardcoded; SHA tag immutable for rollback.
|
||||
|
||||
---
|
||||
|
||||
## Failure artifacts & browser matrix
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| Upload on failure + both profiles | iPhone/WebKit + Pixel/Chromium; upload traces/screenshots/videos on failure. | ✓ |
|
||||
| Upload on failure + Chromium only | Pixel/Chromium only; faster, loses iOS-engine coverage. | |
|
||||
| Both profiles, no artifacts | Full matrix, log-only failures. | |
|
||||
|
||||
**User's choice:** Upload on failure + both profiles
|
||||
**Notes:** Full mobile coverage + debuggable failures. `reporter: 'github'` may not render in Gitea — verify in runner probe, fall back if needed.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Exact job names/step ordering, pnpm cache key, fast-checks split.
|
||||
- API launch mechanism (`pnpm dev:api` vs built `node dist`) as long as `:3000` is reliably ready under DEV_AUTH_BYPASS.
|
||||
- Registry hostname / image repo path.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- ROADMAP status conflict (line 29 "completed" vs line 204 "Not started", no artifacts) — bookkeeping fix, not Phase 8 scope.
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
phase: 8
|
||||
slug: gitea-ci
|
||||
status: planned
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: 2026-06-11
|
||||
---
|
||||
|
||||
# Phase 8 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Vitest 4.1.x (API + PWA unit), @playwright/test 1.60.0 (E2E harness) |
|
||||
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/playwright.config.ts` |
|
||||
| **Quick run command** | `pnpm lint && pnpm typecheck` |
|
||||
| **Full suite command** | `pnpm test` then `pnpm test:e2e` (CI=true, dev stack up) |
|
||||
| **Estimated runtime** | unit ~tens of seconds; harness several minutes (2 device profiles) |
|
||||
|
||||
**Note:** Phase 8 delivers CI plumbing (`.gitea/workflows/*.yml`). The "tests" for this phase are the CI workflow runs themselves — validation is observed by triggering the workflow on a branch/PR and reading Gitea Actions logs, not by a local unit-test file per task.
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** YAML lint / `act_runner` dry-validate where possible; push branch and observe the probe/CI run in Gitea Actions
|
||||
- **After every plan wave:** Confirm all jobs in that wave pass on a test PR (W0 probe green → W1 fast-checks + integration green → W2 harness + publish green)
|
||||
- **Before `/gsd-verify-work`:** Full CI green on a real PR targeting `main`
|
||||
- **Max feedback latency:** one CI run (minutes), bounded by the harness job
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| P01-T1 register runner + PAT | 08-01 | 1 | CI-01/CI-02 | T-08-PAT | runner online; PAT secret stored, never in repo | operator/manual | Gitea Actions runner list + repo secret present | ❌ operator | ⬜ pending |
|
||||
| P01-T2 author runner-probe.yml | 08-01 | 1 | CI-01 | T-08-01 | probe is non-destructive; touches no secret | grep gate | `grep` healthcheck.sh + gitea-upload fork; `! grep` mysqladmin/upload-artifact@v4 | ❌ W0 | ⬜ pending |
|
||||
| P01-T3 run probe, record forks | 08-01 | 1 | CI-01 | T-08-01 | runner mode / cache / WebKit / artifact answers captured | workflow run | observe runner-probe run in Gitea Actions | ❌ W0 | ⬜ pending |
|
||||
| P02-T1 fast-checks job | 08-02 | 2 | CI-01 | — | lint+typecheck+PWA-unit gate the PR | grep gate + workflow run | `grep` node-pin/typecheck/pwa test; PR run green | ✅ scripts | ⬜ pending |
|
||||
| P02-T2 api job (MariaDB+migrate) | 08-02 | 2 | CI-01 | T-08-03/04/05 | DB-backed API tests pass cold; migrate-not-push; throwaway creds | grep gate + workflow run | `grep` healthcheck.sh+db:migrate; `! grep` mysqladmin/db:push; cold PR run green | ✅ scripts | ⬜ pending |
|
||||
| P02-T3 verify PR jobs | 08-02 | 2 | CI-01 | — | both jobs run parallel, api green cold | workflow run | observe fast-checks ∥ api on a PR | ❌ W1 | ⬜ pending |
|
||||
| P03-T1 harness stack bring-up | 08-03 | 3 | CI-01 | T-08-06/07/08 | API bg w/ inline DEV_AUTH_BYPASS; :3000 readiness before Playwright | grep gate + workflow run | `grep` inline-bypass+:3000/health+db:migrate; `! grep` db:push | ✅ Phase 7 specs | ⬜ pending |
|
||||
| P03-T2 playwright run + artifacts | 08-03 | 3 | CI-01 | T-08-06 | both profiles run; traces upload on failure; no spec edits | grep gate + workflow run | `grep` webkit+chromium+base-url+gitea-upload; `git diff --quiet` harness files | ✅ Phase 7 specs | ⬜ pending |
|
||||
| P03-T3 verify harness on PR | 08-03 | 3 | CI-01 | — | iphone+pixel green vs CI dev stack; artifact confirmed | workflow run | observe harness job on a PR | ❌ W2 | ⬜ pending |
|
||||
| P04-T1 publish job | 08-04 | 4 | CI-02 | T-08-PAT/09/10 | --password-stdin only; both tags; production target from root; no bypass | grep gate | `grep` --password-stdin+target production+both tags; `! grep` -p/--password/DEV_AUTH_BYPASS-in-publish | ❌ W2 | ⬜ pending |
|
||||
| P04-T2 merge, audit log, verify tags | 08-04 | 4 | CI-02 | T-08-PAT | no plaintext PAT in log; :latest + :v1.1-<sha> in registry | workflow run + log audit | merge → audit publish log + check Packages | ❌ W2 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky — planner expands one row per task.*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `.gitea/workflows/runner-probe.yml` — runner-probe workflow (new file; Wave 0 task). Must answer: runner mode (Docker vs host), Docker socket access, `actions/*` resolution, Node 22 / pnpm availability, internet access, WebKit dep installability.
|
||||
- [ ] `.gitea/workflows/ci.yml` — main CI workflow scaffolding (new file; Waves 1–2).
|
||||
|
||||
*Existing unit/integration/E2E infrastructure (Vitest + Playwright) is reused unchanged; no new local test framework is installed.*
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Runner registered on Unraid | CI-01/CI-02 | Operator infra action outside the repo; 0 runners currently registered | Install/register `act_runner` on Unraid against `git.bergerhouse.net`; confirm it appears in Gitea Actions runners list |
|
||||
| Gitea registry PAT created | CI-02 | Secret creation is an operator action; cannot be scripted in-repo | Create PAT with `write:package`; store as repo secret (e.g. `GITEA_REGISTRY_PAT`); confirm publish job authenticates |
|
||||
| PR merge actually blocked on failure | CI-01 | Requires branch-protection "required status checks" config in Gitea | Configure required checks on `main`; open a failing PR; confirm merge button is blocked |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [x] All tasks have an observable CI-run verification or Wave 0 dependency
|
||||
- [x] Sampling continuity: every wave has a green-gate before the next (W0 probe → W1 PR jobs → W2 harness → W3 publish, each gated by a checkpoint)
|
||||
- [x] Wave 0 covers all MISSING references (runner-probe answers all unknowns)
|
||||
- [x] No watch-mode flags
|
||||
- [x] `nyquist_compliant: true` set in frontmatter (after planner expands the map)
|
||||
|
||||
**Approval:** planned 2026-06-11 — map expanded, nyquist_compliant=true
|
||||
@@ -47,23 +47,48 @@ export default async function globalSetup(): Promise<void> {
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'
|
||||
const deadline = Date.now() + 60_000
|
||||
|
||||
// Use an explicit success flag (WR-02): inferring success from `Date.now() >= deadline`
|
||||
// after the loop can misreport a success that arrived in the final second as a timeout,
|
||||
// because the `await fetch` itself can push the clock past the deadline before the
|
||||
// post-loop check runs.
|
||||
let ready = false
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const res = await fetch(`${baseURL}/health`)
|
||||
if (res.ok) break
|
||||
if (res.ok) {
|
||||
ready = true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
// ECONNREFUSED or network error — stack not ready yet, keep polling
|
||||
}
|
||||
await new Promise<void>((r) => setTimeout(r, 1_000))
|
||||
}
|
||||
|
||||
if (Date.now() >= deadline) {
|
||||
if (!ready) {
|
||||
throw new Error(
|
||||
`health check never returned 200 at ${baseURL}/health — is the dev stack up?\n` +
|
||||
`Ensure the API is running with DEV_AUTH_BYPASS=true and the Vite dev server is on ${baseURL}.`,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Step 1b: DEV_AUTH_BYPASS reachability gate (WR-01) ──────────────────────
|
||||
// /health is unauthenticated and returns 200 even if the API was started WITHOUT
|
||||
// DEV_AUTH_BYPASS=true. In that case every spec would fail at the first /api/me or
|
||||
// /api/events with a 302 redirect to Authelia. Probe /api/me here so a mis-started
|
||||
// API fails loudly IN SETUP with a clear message instead of ~40 confusing spec failures.
|
||||
// redirect:'manual' surfaces the Authelia redirect as an opaque/3xx response instead of
|
||||
// silently following it.
|
||||
const meRes = await fetch(`${baseURL}/api/me`, { redirect: 'manual' })
|
||||
if (!meRes.ok) {
|
||||
throw new Error(
|
||||
`/api/me did not return 200 (got ${meRes.status} ${meRes.type}) at ${baseURL}/api/me — ` +
|
||||
`the API is reachable but DEV_AUTH_BYPASS is almost certainly NOT set in the API process.\n` +
|
||||
`A 3xx/opaqueredirect here means /api/me is redirecting to Authelia. ` +
|
||||
`Restart the API with DEV_AUTH_BYPASS=true so it serves Dev User 1 without OIDC.`,
|
||||
)
|
||||
}
|
||||
|
||||
// ── Step 2: Reset-and-seed (D-06 deterministic, D-07 in globalSetup) ────────
|
||||
// Uses exact env-var names from apps/api/src/db/client.ts.
|
||||
// DB_HOST defaults to '127.0.0.1' (NOT 'localhost') — per project memory api-integration-test-db.
|
||||
|
||||
Reference in New Issue
Block a user