From 4e474cdd9cade21ff43bf7989775e7944c0999e0 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 09:26:48 -0400 Subject: [PATCH] =?UTF-8?q?docs(08):=20create=20phase=20plan=20=E2=80=94?= =?UTF-8?q?=204=20plans=20(runner-probe,=20PR=20jobs,=20harness,=20publish?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/ROADMAP.md | 7 +- .planning/phases/08-gitea-ci/08-01-PLAN.md | 165 ++++++++++++++++ .planning/phases/08-gitea-ci/08-02-PLAN.md | 185 ++++++++++++++++++ .planning/phases/08-gitea-ci/08-03-PLAN.md | 175 +++++++++++++++++ .planning/phases/08-gitea-ci/08-04-PLAN.md | 152 ++++++++++++++ .planning/phases/08-gitea-ci/08-VALIDATION.md | 31 +-- 6 files changed, 702 insertions(+), 13 deletions(-) create mode 100644 .planning/phases/08-gitea-ci/08-01-PLAN.md create mode 100644 .planning/phases/08-gitea-ci/08-02-PLAN.md create mode 100644 .planning/phases/08-gitea-ci/08-03-PLAN.md create mode 100644 .planning/phases/08-gitea-ci/08-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 7d332aa..f63c6d7 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -94,7 +94,12 @@ 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: +- [ ] 08-01-PLAN.md — Runner probe + operator runner/PAT registration (W0; answers the Docker-vs-host fork) +- [ ] 08-02-PLAN.md — ci.yml: fast-checks (lint/typecheck/PWA unit) + API job (MariaDB service + migrate + DB-backed tests) +- [ ] 08-03-PLAN.md — ci.yml: harness job (dev-stack bring-up + readiness waits + Phase 7 Playwright specs, both profiles) +- [ ] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1- via --password-stdin) **UI hint**: yes ### Phase 9: Faster Write-Back diff --git a/.planning/phases/08-gitea-ci/08-01-PLAN.md b/.planning/phases/08-gitea-ci/08-01-PLAN.md new file mode 100644 index 0000000..8144383 --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-01-PLAN.md @@ -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" +--- + + +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. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-gitea-ci/08-CONTEXT.md +@.planning/phases/08-gitea-ci/08-RESEARCH.md +@.planning/research/PITFALLS.md + + + +- `.gitea/workflows/runner-probe.yml` (NEW — this plan) +- `.gitea/workflows/ci.yml` (NEW — Plans 02/03/04) + + + + + + Task 1: Register act_runner + create registry PAT (operator-only) + Nothing automated — these are infrastructure actions outside the repo that the executor cannot perform (08-VALIDATION Manual-Only table; CI-01/CI-02 prerequisites). + + 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. + + 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). + + + + Task 2: Author the runner-probe workflow + .gitea/workflows/runner-probe.yml + + - .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) + + + 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. + + + 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 + + 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. + + + + Task 3: Run the probe and record the fork answers + The runner-probe workflow (Task 2), pushed to the gsd/phase-08-gitea-ci branch so the now-registered runner executes it. + + 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). + + 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. + + + + + +## 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. | + + + +- 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. + + + +- 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. + + + +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. + diff --git a/.planning/phases/08-gitea-ci/08-02-PLAN.md b/.planning/phases/08-gitea-ci/08-02-PLAN.md new file mode 100644 index 0000000..0d823c3 --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-02-PLAN.md @@ -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)" +--- + + +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). + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-gitea-ci/08-RESEARCH.md +@.planning/research/PITFALLS.md +@.planning/phases/08-gitea-ci/08-01-SUMMARY.md + + + +- `.gitea/workflows/ci.yml` (NEW — this plan creates it; Plans 03/04 extend it) + + + +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. + + + + + + Task 1: Create ci.yml with the fast-checks job + .gitea/workflows/ci.yml + + - .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) + + + 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). + + + 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 + + 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. + + + + Task 2: Add the API job (MariaDB service + migrate + DB-backed tests) + .gitea/workflows/ci.yml + + - .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) + + + 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). + + + 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 + + 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. + + + + Task 3: Verify fast-checks + api jobs on a PR + ci.yml with parallel fast-checks + api jobs (Tasks 1–2), exercised by opening a PR from gsd/phase-08-gitea-ci → main. + + 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). + + Type "W1 green" once both jobs pass on a cold PR run, or paste the failing log. + + + + + +## 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. | + + + +- 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. + + + +- 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. + + + +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. + diff --git a/.planning/phases/08-gitea-ci/08-03-PLAN.md b/.planning/phases/08-gitea-ci/08-03-PLAN.md new file mode 100644 index 0000000..a5f1b80 --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-03-PLAN.md @@ -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" +--- + + +Add the harness job to `.gitea/workflows/ci.yml`: bring up the dev stack inside the runner (MariaDB → migrate → API background process with DEV_AUTH_BYPASS=true on :3000 → Playwright starts Vite on :5173 itself) and run the Phase 7 mobile Playwright harness UNCHANGED across both device profiles, uploading traces on failure. This is the v1.1 extension of CI-01 (ROADMAP criteria 3 + 4) and Pitfall "dev-stack readiness races". + +Purpose: Catch mobile-only regressions on every PR with no developer's host stack required (Phase 7 success criterion 4). CI owns ONLY stack bring-up + readiness waits — never spec content (D-01/D-02; 08-CONTEXT phase boundary). + +Output: a `harness` job in ci.yml gated on `pull_request → main`. + +The orchestration order in 08-RESEARCH §Dev-Stack Bring-Up is mandatory and SEQUENTIAL within the job. Read 08-01-SUMMARY for the runner-mode DB path and the WebKit-deps / upload-artifact answers. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-gitea-ci/08-RESEARCH.md +@.planning/research/PITFALLS.md +@.planning/phases/08-gitea-ci/08-01-SUMMARY.md +@apps/pwa/playwright.config.ts +@apps/pwa/e2e/global-setup.ts +@apps/pwa/vite.config.ts + + + +- `.gitea/workflows/ci.yml` (EXTENDED — adds the harness job; created in Plan 02) + + + +Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.config.ts — do NOT modify these files): +- `playwright.config.ts`: `reuseExistingServer: !process.env.CI` → with CI=true, Playwright STARTS Vite itself (`pnpm --filter @familysync/pwa dev`, :5173). `retries: 2`, `workers: 1`, `reporter: 'github'` are all gated on `process.env.CI`. Two projects: `iphone` (WebKit) + `pixel` (Chromium), both `serviceWorkers: 'block'`. +- `reporter: 'github'` likely emits invisible output in Gitea (08-RESEARCH Pitfall 5 / D-06). Override the reporter at the CI invocation: pass `--reporter=list,html` (e.g. `pnpm test:e2e -- --reporter=list,html`) OR confirm from 08-01-SUMMARY whether Gitea rendered annotations; if it did, the override is harmless. Do NOT edit playwright.config.ts. +- `global-setup.ts`: FAILS CLOSED — throws if `NODE_ENV=production` OR if `DEV_AUTH_BYPASS !== 'true'`. It polls `${PLAYWRIGHT_BASE_URL}/health` (via the Vite proxy → :3000), then gates `/api/me` (must be 200 → proves DEV_AUTH_BYPASS reached the API), then mysql2-seeds calendar id=10 + lists for user 1. It reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (DB_HOST default 127.0.0.1). +- `vite.config.ts`: dev proxy `/health`, `/api`, `/callback` → http://localhost:3000. So PLAYWRIGHT_BASE_URL=http://localhost:5173 reaches the API health endpoint through the proxy. +- Root `test:e2e` = `pnpm --filter @familysync/pwa test:e2e` = `playwright test`. +- API start: `dev` script is `node --watch dist/index.js` and needs a prior build. In CI run `pnpm --filter @familysync/api build` (tsc → dist/index.js) then `node apps/api/dist/index.js &` (no --watch; the watcher is irrelevant in CI — Claude's Discretion in D + 08-RESEARCH Pattern 3). Pass DEV_AUTH_BYPASS=true INLINE on the node line (Pitfall 8 — env inheritance across `&` steps is not guaranteed). +- `npx playwright install --with-deps webkit chromium` must run from `apps/pwa` (where @playwright/test lives). Playwright explicitly says do NOT cache browser binaries (08-RESEARCH). +- DEV_AUTH_BYPASS user 1 has no CalDAV credential → harness verifies layout/flows, not live event-create (project memory). Specs already account for this; no change. + + + + + + Task 1: Add the harness job — DB + migrate + API background process + readiness + .gitea/workflows/ci.yml + + - .planning/phases/08-gitea-ci/08-RESEARCH.md (§Dev-Stack Bring-Up — the numbered 1..8 sequence is canonical; §Pattern 3 API background process; Pitfall 8 DEV_AUTH_BYPASS inline) + - .planning/phases/08-gitea-ci/08-01-SUMMARY.md (runner-mode DB path; WebKit deps y/n) + - apps/pwa/e2e/global-setup.ts (fail-closed guards; readiness order) + + + Add a `harness` job to ci.yml: `runs-on: self-hosted`, `if: github.event_name == 'pull_request'` (parallel with fast-checks + api — D-03; no `needs:`). + + DB bring-up: SAME runner-mode branch as Plan 02's api job (services: mariadb: for Docker mode with DB_HOST=mariadb, or `docker run -d` + readiness loop for host mode with DB_HOST=127.0.0.1). Set job env: DB_PORT 3306, DB_USER familysync, DB_PASSWORD testpass, DB_NAME familysync (throwaway creds). Include the explicit healthcheck.sh readiness loop before migrate (Pitfall 11; never mysqladmin). + + Steps, in this exact order (08-RESEARCH §Dev-Stack Bring-Up 1..6): + 1. `uses: actions/checkout@v4`; `uses: actions/setup-node@v4` (node 22); `corepack enable pnpm`. + 2. `run: pnpm install --frozen-lockfile`. + 3. (after DB ready) `run: pnpm --filter @familysync/api db:migrate` with DB_* env (drizzle-kit migrate; never push). + 4. `run: pnpm --filter @familysync/api build` (produces dist/index.js — Pitfall 4). + 5. Start API as a background process with DEV_AUTH_BYPASS INLINE: + `NODE_ENV=development DEV_AUTH_BYPASS=true DB_HOST=$DB_HOST DB_PORT=3306 DB_USER=familysync DB_PASSWORD=testpass DB_NAME=familysync node apps/api/dist/index.js & echo $! > /tmp/api.pid` (Pitfall 8). NODE_ENV must be `development` (not production — global-setup refuses; not test — dev-bypass activation checks development per 08-RESEARCH note). + 6. Wait for API :3000: a curl retry loop `until curl -sf http://localhost:3000/health` with a ~60s deadline; on timeout, `kill $(cat /tmp/api.pid)` and `exit 1`. This step-level wait (D-02) ensures the API is up BEFORE Playwright starts Vite — separate from and earlier than global-setup's own poll. + + + grep -q "harness" .gitea/workflows/ci.yml && grep -q "node apps/api/dist/index.js" .gitea/workflows/ci.yml && grep -q "DEV_AUTH_BYPASS=true node" .gitea/workflows/ci.yml && grep -q "localhost:3000/health" .gitea/workflows/ci.yml && grep -q "db:migrate" .gitea/workflows/ci.yml && ! grep -q "db:push" .gitea/workflows/ci.yml && echo HARNESS_STACK_OK + + The harness job brings up MariaDB (per runner mode), migrates, builds the API, starts it as a background process with DEV_AUTH_BYPASS=true passed inline on the node line, and waits for :3000/health before continuing. + + + + Task 2: Add Playwright install + run (both profiles) + artifact upload on failure + .gitea/workflows/ci.yml + + - apps/pwa/playwright.config.ts (CI gating: reuseExistingServer, reporter:'github', both projects) + - .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 5 Playwright harness; Pitfall 5 reporter override; Pitfall 6 upload-artifact fork) + - .planning/phases/08-gitea-ci/08-01-SUMMARY.md (WebKit deps y/n; upload-artifact fork y/n; did Gitea render 'github' reporter annotations?) + + + Continue the `harness` job (08-RESEARCH §Dev-Stack Bring-Up 7..8): + 7. Install browsers: `run: npx playwright install --with-deps webkit chromium` with `working-directory: apps/pwa`. (If 08-01-SUMMARY showed WebKit deps cannot install on this runner, record that as a phase blocker in the SUMMARY — do NOT silently drop the iphone profile; D-05 requires BOTH profiles. WebKit feasibility is a hard CI-01 input.) + 8. Run the harness: + `run: pnpm test:e2e -- --reporter=list,html` (the `--reporter=list,html` overrides the config's CI `'github'` reporter which renders invisibly in Gitea — Pitfall 5; skip the override only if 08-01-SUMMARY confirmed Gitea renders 'github' annotations, in which case it is harmless to keep). + env on this step: `CI: 'true'`, `PLAYWRIGHT_BASE_URL: http://localhost:5173`, `DEV_AUTH_BYPASS: 'true'`, `NODE_ENV: development`, plus DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (global-setup seeds the DB directly via mysql2). CI=true makes Playwright start Vite itself (:5173) and use retries:2/workers:1; the run covers both `iphone` and `pixel` projects by default (no --project filter). + 9. Upload artifacts on failure: a final step `if: failure()` `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` (NEVER actions/upload-artifact@v4 — GHES-blocked on Gitea, Pitfall 6) with `name: playwright-traces-${{ github.run_id }}`, `path: apps/pwa/test-results/`, `retention-days: 14` (D-06). Add a final `if: always()` step to `kill $(cat /tmp/api.pid) 2>/dev/null || true` to clean up the API background process. + + Do NOT modify playwright.config.ts, global-setup.ts, vite.config.ts, or any spec — CI owns bring-up only (D-01/D-02; phase boundary). + + + grep -q "playwright install --with-deps webkit chromium" .gitea/workflows/ci.yml && grep -q "test:e2e" .gitea/workflows/ci.yml && grep -q "PLAYWRIGHT_BASE_URL: http://localhost:5173" .gitea/workflows/ci.yml && grep -q "ChristopherHX/gitea-upload-artifact@v4" .gitea/workflows/ci.yml && ! grep -q "actions/upload-artifact@v4" .gitea/workflows/ci.yml && git diff --quiet -- apps/pwa/playwright.config.ts apps/pwa/e2e/global-setup.ts apps/pwa/vite.config.ts && echo HARNESS_RUN_OK + + The harness job installs webkit+chromium with deps, runs pnpm test:e2e (CI=true, DEV_AUTH_BYPASS=true, base URL :5173, DB env) across both profiles with a list,html reporter override, and uploads test-results/ on failure via the gitea fork. No Phase 7 harness file is modified. + + + + Task 3: Verify the harness job on the PR + The harness job (Tasks 1–2) added to ci.yml, exercised on the open PR to main. + + 1. Push the branch; on the PR, confirm the `harness` job runs alongside fast-checks + api. + 2. Confirm it brings up MariaDB → migrate → API (:3000) → Playwright starts Vite (:5173) → both `iphone` and `pixel` projects execute and pass (ROADMAP criteria 3 + 4). + 3. Confirm the readiness waits prevented a startup race (no "/api/me did not return 200" or ECONNREFUSED from global-setup on a cold run). If global-setup throws the DEV_AUTH_BYPASS error, the API was started without the inline flag (Pitfall 8) — fix the node invocation, do not re-run. + 4. Deliberately break a spec or seed once (or inspect a prior failure) to confirm test-results/ uploads as a downloadable artifact in the Gitea UI (D-06). Revert the break. + 5. Confirm test output is readable in the Gitea log (list reporter), not invisible 'github' annotations. + + Type "harness green" once both device profiles pass against the CI-brought-up stack and artifact upload is confirmed, or paste the failing log. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| DEV_AUTH_BYPASS in CI | Bypass auth flag active in the harness job only; must never reach the publish job | +| CI test DB → seed | global-setup TRUNCATEs tables; fail-closed guards protect against prod DB | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-08-06 | Spoofing | DEV_AUTH_BYPASS=true in harness job | mitigate | Bypass is set ONLY in the harness job env, against throwaway DB creds; it never appears in the publish job (Plan 04). global-setup.ts fails closed on NODE_ENV=production and on missing DEV_AUTH_BYPASS, so it cannot wipe/seed an unconfirmed DB (08-RESEARCH Security Domain). | +| T-08-07 | Tampering | drizzle migrate against CI DB | mitigate | db:migrate only; db:push forbidden (grep gate). Throwaway creds, ephemeral container. | +| T-08-08 | Denial of Service | dev-server startup race | mitigate | Explicit :3000/health curl loop before Playwright (D-02) on top of global-setup's :5173/health + /api/me gates; MariaDB healthcheck.sh readiness loop before migrate (Pitfall 11). | + + + +- ci.yml passes both Task grep gates (background API + inline bypass + :3000 readiness; both browsers + base URL + gitea upload fork; no harness-file edits). +- PR run shows the harness job green across iphone + pixel on a cold run. +- Artifact upload confirmed on a forced failure; output legible via list reporter. + + + +- CI-01 (harness half): PR to main brings up API + PWA dev servers + MariaDB with DEV_AUTH_BYPASS and runs the Phase 7 specs headlessly; failure gates merge (ROADMAP criterion 3). +- Readiness: waits for both :3000 and :5173 before Playwright (ROADMAP criterion 4; Pitfall dev-stack races). +- Phase 7 specs reused UNCHANGED (phase boundary); both device profiles run (D-05); traces upload on failure (D-06). + + + +Create `.planning/phases/08-gitea-ci/08-03-SUMMARY.md` when done. Record: final API readiness timeout, whether the reporter override was needed, WebKit-deps install outcome on the runner, and confirmation no Phase 7 harness file was modified. + diff --git a/.planning/phases/08-gitea-ci/08-04-PLAN.md b/.planning/phases/08-gitea-ci/08-04-PLAN.md new file mode 100644 index 0000000..6fa1242 --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-04-PLAN.md @@ -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 :- (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" +--- + + +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 `:-`, 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`. + + + +@$HOME/.claude/gsd-core/workflows/execute-plan.md +@$HOME/.claude/gsd-core/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/08-gitea-ci/08-RESEARCH.md +@.planning/research/PITFALLS.md +@.planning/phases/08-gitea-ci/08-01-SUMMARY.md +@apps/api/Dockerfile + + + +- `.gitea/workflows/ci.yml` (EXTENDED — adds the publish job; closes the phase) + + + +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`. + + + + + + Task 1: Add the publish job (build + tag + login --password-stdin + push) + .gitea/workflows/ci.yml + + - .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) + + + 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 -t .` + then `docker push ` and `docker push `. + 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. + + + 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 + + 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}-, logs out, and never sets DEV_AUTH_BYPASS. + + + + Task 2: Merge, audit the publish log, and verify both tags + 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. + + 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. + + Type "publish verified" once both tags exist in the registry AND the log audit confirms no plaintext PAT, or describe the failure. + + + + + +## 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. | + + + +- 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. + + + +- CI-02: on merge to main, the API production image is built and pushed to the Gitea registry under :latest + :v1.1- (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. + + + +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. + diff --git a/.planning/phases/08-gitea-ci/08-VALIDATION.md b/.planning/phases/08-gitea-ci/08-VALIDATION.md index e77238e..4e08a31 100644 --- a/.planning/phases/08-gitea-ci/08-VALIDATION.md +++ b/.planning/phases/08-gitea-ci/08-VALIDATION.md @@ -1,8 +1,8 @@ --- phase: 8 slug: gitea-ci -status: draft -nyquist_compliant: false +status: planned +nyquist_compliant: true wave_0_complete: false created: 2026-06-11 --- @@ -40,10 +40,17 @@ created: 2026-06-11 | Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | |---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| -| _planner-fills_ | — | 0 | CI-01 | — | runner-probe surfaces runner mode / Docker / action resolution | workflow run | observe `.gitea/workflows/runner-probe.yml` in Gitea Actions | ❌ W0 | ⬜ pending | -| _planner-fills_ | — | 1 | CI-01 | — | lint/typecheck/unit/integration gate the PR | workflow run | `pnpm lint`, `pnpm typecheck`, `pnpm test`, `pnpm --filter @familysync/api test` | ✅ (scripts exist) | ⬜ pending | -| _planner-fills_ | — | 2 | CI-01 | — | harness runs against CI dev stack, failure blocks merge | workflow run | `pnpm test:e2e` with `CI=true`, `DEV_AUTH_BYPASS=true` | ✅ (Phase 7 specs) | ⬜ pending | -| _planner-fills_ | — | 2 | CI-02 | T-PAT | image published, PAT never in logs | workflow run | `docker pull /:latest` | ❌ W2 | ⬜ pending | +| 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- 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.* @@ -70,10 +77,10 @@ created: 2026-06-11 ## Validation Sign-Off -- [ ] All tasks have an observable CI-run verification or Wave 0 dependency -- [ ] Sampling continuity: every wave has a green-gate before the next -- [ ] Wave 0 covers all MISSING references (runner-probe answers all unknowns) -- [ ] No watch-mode flags -- [ ] `nyquist_compliant: true` set in frontmatter (after planner expands the map) +- [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:** pending +**Approval:** planned 2026-06-11 — map expanded, nyquist_compliant=true