docs(08): create phase plan — 4 plans (runner-probe, PR jobs, harness, publish)
This commit is contained in:
@@ -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-<sha> via --password-stdin)
|
||||
**UI hint**: yes
|
||||
|
||||
### Phase 9: Faster Write-Back
|
||||
|
||||
@@ -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>
|
||||
@@ -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 <registry>/<repo>: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-<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.*
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user