Files
familysync/.planning/phases/08-gitea-ci/08-02-PLAN.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

194 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 12), 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>