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.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
+30 -25
View File
@@ -10,30 +10,30 @@ 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"
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"
- 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"
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"
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)"
- '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"
- 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: "the registered act_runner"
via: "runs-on: ubuntu-latest (runner has no self-hosted label), on: push to gsd/phase-08-gitea-ci"
- from: '.gitea/workflows/runner-probe.yml'
to: 'the registered act_runner'
via: 'runs-on: ubuntu-latest (runner has no self-hosted label), on: push to gsd/phase-08-gitea-ci'
pattern: "runs-on:\\s*ubuntu-latest"
---
@@ -60,9 +60,10 @@ Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored r
</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>
</artifacts_this_phase_produces>
<tasks>
@@ -104,6 +105,7 @@ Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored r
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>
@@ -131,20 +133,22 @@ Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored r
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 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 |
| 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 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>
@@ -155,10 +159,11 @@ Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored r
</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 0204 pick the correct DB bring-up path.
- Operator infra (runner + PAT) is in place.
</success_criteria>
</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 0204 consume them.
+37 -35
View File
@@ -1,15 +1,15 @@
---
phase: 08-gitea-ci
plan: "01"
plan: '01'
subsystem: infra
tags: [gitea, ci, act_runner, github-actions, docker, playwright, mariadb, artifacts]
# Dependency graph
requires: []
provides:
- "Runner-probe workflow (.gitea/workflows/runner-probe.yml) confirming the Unraid act_runner environment"
- "Confirmed answers to all P-01..P-13 unknowns from 08-RESEARCH §Runner-Probe Checklist"
- "Registered act_runner + GITEA_REGISTRY_PAT repo secret (operator actions)"
- 'Runner-probe workflow (.gitea/workflows/runner-probe.yml) confirming the Unraid act_runner environment'
- 'Confirmed answers to all P-01..P-13 unknowns from 08-RESEARCH §Runner-Probe Checklist'
- 'Registered act_runner + GITEA_REGISTRY_PAT repo secret (operator actions)'
affects:
- 08-02-PLAN
- 08-03-PLAN
@@ -18,13 +18,13 @@ affects:
# Tech tracking
tech-stack:
added:
- "act_runner (Docker-executor mode, registered on git.bergerhouse.net)"
- "ChristopherHX/gitea-upload-artifact@v4 (Gitea-compatible artifact upload fork)"
- "actions/checkout@v4, actions/setup-node@v4 (resolved via github.com)"
- 'act_runner (Docker-executor mode, registered on git.bergerhouse.net)'
- 'ChristopherHX/gitea-upload-artifact@v4 (Gitea-compatible artifact upload fork)'
- 'actions/checkout@v4, actions/setup-node@v4 (resolved via github.com)'
patterns:
- "runner-probe-first: probe the runner environment before any real test/build/publish step"
- "healthcheck.sh --connect --innodb_initialized for MariaDB 11 readiness (not mysqladmin ping)"
- "ubuntu-latest runs-on label (runner advertises ubuntu-latest, not self-hosted)"
- 'runner-probe-first: probe the runner environment before any real test/build/publish step'
- 'healthcheck.sh --connect --innodb_initialized for MariaDB 11 readiness (not mysqladmin ping)'
- 'ubuntu-latest runs-on label (runner advertises ubuntu-latest, not self-hosted)'
key-files:
created:
@@ -32,25 +32,25 @@ key-files:
modified: []
key-decisions:
- "D-PROBE-01: runs-on must be ubuntu-latest (not self-hosted) — runner has no self-hosted label; all downstream ci.yml workflows (Plans 02-04) MUST use runs-on: ubuntu-latest"
- "D-PROBE-02: runner is Docker-executor mode (/.dockerenv present) — services: works; DB_HOST=mariadb in ci.yml"
- "D-PROBE-03: MariaDB service container works and is reachable on hostname mariadb; DB readiness must use healthcheck, NOT mysql CLI (not installed in runner image)"
- "D-PROBE-04: actions/cache@v4 is unreliable (timeout) — do NOT use cache in Plans 02/03; at most best-effort"
- "D-PROBE-05: Playwright WebKit + Chromium deps install cleanly (exit 0); Phase-7 harness is CI-feasible"
- "D-PROBE-06: ChristopherHX/gitea-upload-artifact@v4 works — plans 03/04 MUST use this fork, never actions/upload-artifact@v4"
- "D-PROBE-07: short SHA via ${GITHUB_SHA:0:7} produces 7 chars — D-04 publish tag expression confirmed valid"
- "D-PROBE-08: GITEA_REGISTRY_PAT deferred to Plan 04 (operator decision; PAT not exercised in probe)"
- 'D-PROBE-01: runs-on must be ubuntu-latest (not self-hosted) — runner has no self-hosted label; all downstream ci.yml workflows (Plans 02-04) MUST use runs-on: ubuntu-latest'
- 'D-PROBE-02: runner is Docker-executor mode (/.dockerenv present) — services: works; DB_HOST=mariadb in ci.yml'
- 'D-PROBE-03: MariaDB service container works and is reachable on hostname mariadb; DB readiness must use healthcheck, NOT mysql CLI (not installed in runner image)'
- 'D-PROBE-04: actions/cache@v4 is unreliable (timeout) — do NOT use cache in Plans 02/03; at most best-effort'
- 'D-PROBE-05: Playwright WebKit + Chromium deps install cleanly (exit 0); Phase-7 harness is CI-feasible'
- 'D-PROBE-06: ChristopherHX/gitea-upload-artifact@v4 works — plans 03/04 MUST use this fork, never actions/upload-artifact@v4'
- 'D-PROBE-07: short SHA via ${GITHUB_SHA:0:7} produces 7 chars — D-04 publish tag expression confirmed valid'
- 'D-PROBE-08: GITEA_REGISTRY_PAT deferred to Plan 04 (operator decision; PAT not exercised in probe)'
patterns-established:
- "Probe-before-build: all CI phase work starts with a non-destructive probe run to confirm runner unknowns"
- "No mysql CLI: DB readiness gating must use MariaDB service healthcheck or Node mysql2-based wait"
- "Gitea artifact upload: always ChristopherHX/gitea-upload-artifact@v4, never actions/upload-artifact@v4"
- 'Probe-before-build: all CI phase work starts with a non-destructive probe run to confirm runner unknowns'
- 'No mysql CLI: DB readiness gating must use MariaDB service healthcheck or Node mysql2-based wait'
- 'Gitea artifact upload: always ChristopherHX/gitea-upload-artifact@v4, never actions/upload-artifact@v4'
requirements-completed: [CI-01, CI-02]
# Metrics
duration: 30min (Tasks 1+2 authoring) + probe run ~5min
completed: "2026-06-11"
completed: '2026-06-11'
---
# Phase 08 Plan 01: Runner Probe Summary
@@ -75,17 +75,17 @@ completed: "2026-06-11"
These answers are the primary output of Plan 01. Plans 02, 03, and 04 MUST consume them.
| Probe | Question | Result | Implication |
|-------|----------|--------|-------------|
| P-03 | Runner mode | **Docker-executor** (`/.dockerenv` present) | `services:` works in all downstream jobs; `DB_HOST=mariadb` |
| P-05 | Service container spawn | **WORKS**`mariadb:11` started healthy (`Up (healthy) 3306/tcp`) | Use `services: mariadb` in ci.yml |
| P-06 | DB reachability via CLI | **INCONCLUSIVE**`mysql` CLI not installed in runner image (`command not found` for both `mariadb` and `127.0.0.1`); `mariadb` hostname resolves at Docker-network level | DB readiness gating in Plans 02/03 MUST NOT shell out to `mysql` CLI — use MariaDB healthcheck (`healthcheck.sh --connect --innodb_initialized`) and/or a Node `mysql2`-based wait; or explicitly install `mariadb-client` if a CLI step is required |
| P-08 | Action resolution | **WORKS**`actions/checkout@v4` and `actions/setup-node@v4` (node 22) cloned from github.com; first-run clone slow (~60-75 s each) but reliable | No local mirror needed; plan for slow cold starts |
| P-09 | `actions/cache@v4` | **UNRELIABLE** — restore timed out (`getCacheEntry failed: Request timeout`; Pitfall 7) | Do NOT use `actions/cache` in Plans 02/03; at most `continue-on-error: true` best-effort |
| P-10 | Playwright WebKit deps | **OK**`npx playwright install --with-deps webkit chromium` exits 0 (runs as root; no sudo/apt failure) | Phase-7 harness in CI is feasible; no extra apt workaround needed |
| P-11 | Artifact upload | **WORKS**`ChristopherHX/gitea-upload-artifact@v4` uploaded (Artifact ID 1, download URL returned) | Plans 03/04 MUST use this fork; `actions/upload-artifact@v4` is broken on Gitea |
| P-13 | Short SHA | **WORKS**`${GITHUB_SHA:0:7}` = `134d4db` (7 chars) | D-04 publish tag expression `git.bergerhouse.net/.../familysync:${GITHUB_SHA:0:7}` is valid |
| Security (T-08-01) | Secrets in probe log | **CLEAN** — probe references no secrets; log audit found no leak | PAT untouched in this plan |
| Probe | Question | Result | Implication |
| ------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P-03 | Runner mode | **Docker-executor** (`/.dockerenv` present) | `services:` works in all downstream jobs; `DB_HOST=mariadb` |
| P-05 | Service container spawn | **WORKS**`mariadb:11` started healthy (`Up (healthy) 3306/tcp`) | Use `services: mariadb` in ci.yml |
| P-06 | DB reachability via CLI | **INCONCLUSIVE**`mysql` CLI not installed in runner image (`command not found` for both `mariadb` and `127.0.0.1`); `mariadb` hostname resolves at Docker-network level | DB readiness gating in Plans 02/03 MUST NOT shell out to `mysql` CLI — use MariaDB healthcheck (`healthcheck.sh --connect --innodb_initialized`) and/or a Node `mysql2`-based wait; or explicitly install `mariadb-client` if a CLI step is required |
| P-08 | Action resolution | **WORKS**`actions/checkout@v4` and `actions/setup-node@v4` (node 22) cloned from github.com; first-run clone slow (~60-75 s each) but reliable | No local mirror needed; plan for slow cold starts |
| P-09 | `actions/cache@v4` | **UNRELIABLE** — restore timed out (`getCacheEntry failed: Request timeout`; Pitfall 7) | Do NOT use `actions/cache` in Plans 02/03; at most `continue-on-error: true` best-effort |
| P-10 | Playwright WebKit deps | **OK**`npx playwright install --with-deps webkit chromium` exits 0 (runs as root; no sudo/apt failure) | Phase-7 harness in CI is feasible; no extra apt workaround needed |
| P-11 | Artifact upload | **WORKS**`ChristopherHX/gitea-upload-artifact@v4` uploaded (Artifact ID 1, download URL returned) | Plans 03/04 MUST use this fork; `actions/upload-artifact@v4` is broken on Gitea |
| P-13 | Short SHA | **WORKS**`${GITHUB_SHA:0:7}` = `134d4db` (7 chars) | D-04 publish tag expression `git.bergerhouse.net/.../familysync:${GITHUB_SHA:0:7}` is valid |
| Security (T-08-01) | Secrets in probe log | **CLEAN** — probe references no secrets; log audit found no leak | PAT untouched in this plan |
### KEY DEVIATION for all downstream workflows
@@ -115,6 +115,7 @@ The runner advertises **`ubuntu-latest`** (and `ubuntu-24.04` / `ubuntu-22.04`),
### Auto-fixed Issues
**1. [Rule 1 - Bug] Fixed `runs-on: self-hosted` → `runs-on: ubuntu-latest`**
- **Found during:** Task 3 (probe run) — probe job stayed queued with no eligible runner
- **Issue:** The plan specified `runs-on: self-hosted` but the runner advertises `ubuntu-latest`/`ubuntu-24.04`/`ubuntu-22.04`, not the `self-hosted` label
- **Fix:** Changed `runs-on: self-hosted` to `runs-on: ubuntu-latest` in `.gitea/workflows/runner-probe.yml`; also updated the plan's `key_links.via` pattern to document the correct label
@@ -152,5 +153,6 @@ Plans 02-04 have everything they need from this probe:
No blockers for Plan 02 (test job authoring).
---
*Phase: 08-gitea-ci*
*Completed: 2026-06-11*
_Phase: 08-gitea-ci_
_Completed: 2026-06-11_
+37 -29
View File
@@ -3,30 +3,30 @@ phase: 08-gitea-ci
plan: 02
type: execute
wave: 2
depends_on: ["08-01"]
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"
- '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"
- 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)"
- 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>
@@ -37,9 +37,10 @@ Purpose: Fast PR feedback (D-03 — a lint failure does not wait behind the heav
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>
</objective>
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@@ -56,11 +57,13 @@ CRITICAL CONTEXT — read 08-01-SUMMARY first for the runner-mode fork:
</context>
<artifacts_this_phase_produces>
- `.gitea/workflows/ci.yml` (NEW — this plan creates it; Plans 03/04 extend it)
</artifacts_this_phase_produces>
</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).
@@ -68,7 +71,7 @@ Confirmed repo facts the executor MUST honor (do not re-derive):
- 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>
</interface_context>
<tasks>
@@ -96,6 +99,7 @@ Confirmed repo facts the executor MUST honor (do not re-derive):
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>
@@ -129,6 +133,7 @@ Confirmed repo facts the executor MUST honor (do not re-derive):
- `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>
@@ -152,20 +157,22 @@ Confirmed repo facts the executor MUST honor (do not re-derive):
</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 |
| 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 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>
@@ -175,10 +182,11 @@ Confirmed repo facts the executor MUST honor (do not re-derive):
</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>
</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.
+16 -13
View File
@@ -18,10 +18,10 @@ affects: [08-03, 08-04, phase-09, phase-10, phase-11, phase-12]
tech-stack:
added: []
patterns:
- "Docker-executor services: mariadb (not docker-run) — confirmed by 08-01 probe"
- "Node mysql2 poll for MariaDB readiness (no mysql CLI in runner image)"
- "drizzle-kit migrate (never db:push) on single squashed baseline migration"
- "pnpm -r --if-present lint as auto-gate (exits 0 until a package lint script lands)"
- 'Docker-executor services: mariadb (not docker-run) — confirmed by 08-01 probe'
- 'Node mysql2 poll for MariaDB readiness (no mysql CLI in runner image)'
- 'drizzle-kit migrate (never db:push) on single squashed baseline migration'
- 'pnpm -r --if-present lint as auto-gate (exits 0 until a package lint script lands)'
key-files:
created:
@@ -31,15 +31,15 @@ key-files:
- package.json
key-decisions:
- "D-PROBE-01/02 honored: runs-on ubuntu-latest (not self-hosted), DB_HOST=mariadb (Docker-executor services:)"
- "Cache DISABLED — actions/cache@v4 timed out in 08-01 probe (D-PROBE-04)"
- "Readiness: Node mysql2 poll (90s deadline) — no mysql CLI available in runner image (D-PROBE-03)"
- "Migration squash: all migrations collapsed to 0000_baseline.sql to fix broken cold drizzle-kit migrate"
- "Lint is a documented no-op placeholder; root script changed to pnpm -r --if-present lint; real ESLint deferred to BACKLOG 999.16"
- 'D-PROBE-01/02 honored: runs-on ubuntu-latest (not self-hosted), DB_HOST=mariadb (Docker-executor services:)'
- 'Cache DISABLED — actions/cache@v4 timed out in 08-01 probe (D-PROBE-04)'
- 'Readiness: Node mysql2 poll (90s deadline) — no mysql CLI available in runner image (D-PROBE-03)'
- 'Migration squash: all migrations collapsed to 0000_baseline.sql to fix broken cold drizzle-kit migrate'
- 'Lint is a documented no-op placeholder; root script changed to pnpm -r --if-present lint; real ESLint deferred to BACKLOG 999.16'
patterns-established:
- "PR-gate pattern: parallel fast-checks (no DB) + api (MariaDB services:) jobs both gated on pull_request"
- "Readiness poll pattern: Node mysql2 script with 90s deadline before drizzle-kit migrate"
- 'PR-gate pattern: parallel fast-checks (no DB) + api (MariaDB services:) jobs both gated on pull_request'
- 'Readiness poll pattern: Node mysql2 script with 90s deadline before drizzle-kit migrate'
requirements-completed: [CI-01]
@@ -99,6 +99,7 @@ completed: 2026-06-11
### Auto-fixed Issues
**1. [Rule 1 - Bug] Squashed fragmented drizzle-kit migrations to fix broken cold migrate**
- **Found during:** Task 3 (PR cold-run verification)
- **Issue:** Cold `drizzle-kit migrate` failed with "table already exists" — migration 0001_lists_schema recreated `lists`, `list_shares`, `list_items`, and the `calendars` unique-constraint that migration 0000 had already created. An orphaned migration `0001_calendars_user_url_unique` also existed. Cold migration was impossible on a fresh DB.
- **Fix:** Squashed all migrations into a single `apps/api/src/db/migrations/0000_baseline.sql` regenerated from `schema.ts` via `drizzle-kit generate`. Verified: fresh `db:migrate` succeeds, schema is structurally identical to dev DB, `drizzle-kit generate` reports no drift, 238 API tests pass. Local dev DBs must be rebuilt (drop + `db:migrate`); no production DB exists.
@@ -107,6 +108,7 @@ completed: 2026-06-11
- **Committed in:** `c0f892c`
**2. [Rule 1 - Bug] Fixed root lint script to exit 0 on no-script workspaces**
- **Found during:** Task 1/2 (fast-checks job authoring)
- **Issue:** `pnpm -r lint` emits `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` and exits non-zero when no package has a `lint` script. This would immediately block the CI gate even though ESLint is not yet wired.
- **Fix:** Changed root `package.json` lint script from `pnpm -r lint` to `pnpm -r --if-present lint`. The `--if-present` flag silently skips packages without the script; exits 0. When any package adds a lint script, it is auto-gated. ESLint wiring deferred to BACKLOG 999.16.
@@ -140,5 +142,6 @@ None — this plan produces CI workflow config only.
- **BACKLOG 999.16:** ESLint wiring is explicitly deferred. The `--if-present` lint gate in ci.yml will auto-activate once any package adds a `lint` script — no ci.yml change needed.
---
*Phase: 08-gitea-ci*
*Completed: 2026-06-11*
_Phase: 08-gitea-ci_
_Completed: 2026-06-11_
+33 -26
View File
@@ -3,7 +3,7 @@ phase: 08-gitea-ci
plan: 03
type: execute
wave: 3
depends_on: ["08-02"]
depends_on: ['08-02']
files_modified:
- .gitea/workflows/ci.yml
autonomous: false
@@ -11,22 +11,22 @@ 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"
- '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"
- 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"
- 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>
@@ -57,11 +57,13 @@ The orchestration order in 08-RESEARCH §Dev-Stack Bring-Up is mandatory and SEQ
</context>
<artifacts_this_phase_produces>
- `.gitea/workflows/ci.yml` (EXTENDED — adds the harness job; created in Plan 02)
</artifacts_this_phase_produces>
</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).
@@ -70,7 +72,7 @@ Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.c
- 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>
</interface_context>
<tasks>
@@ -95,6 +97,7 @@ Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.c
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>
@@ -119,6 +122,7 @@ Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.c
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>
@@ -142,20 +146,22 @@ Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.c
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 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 |
| 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 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>
@@ -165,10 +171,11 @@ Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.c
</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>
</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.
+15 -10
View File
@@ -19,10 +19,10 @@ affects: [08-04, phase-09, phase-10, phase-11, phase-12]
tech-stack:
added: []
patterns:
- "Combine API-start + readiness + test run in a single CI step so the API is not reaped at a step boundary"
- "NODE_OPTIONS=--dns-result-order=ipv4first when Vite is IPv4-only and the runner resolves localhost to ::1 first"
- "Idempotent seed step (INSERT IGNORE) for the DEV_AUTH_BYPASS user before global-setup runs — FK chain requires it"
- "Call the PWA test:e2e script directly with --filter instead of root test:e2e -- -- to avoid double-pnpm arg forwarding"
- 'Combine API-start + readiness + test run in a single CI step so the API is not reaped at a step boundary'
- 'NODE_OPTIONS=--dns-result-order=ipv4first when Vite is IPv4-only and the runner resolves localhost to ::1 first'
- 'Idempotent seed step (INSERT IGNORE) for the DEV_AUTH_BYPASS user before global-setup runs — FK chain requires it'
- 'Call the PWA test:e2e script directly with --filter instead of root test:e2e -- -- to avoid double-pnpm arg forwarding'
key-files:
created: []
@@ -31,14 +31,14 @@ key-files:
key-decisions:
- "FIX-1 (53a989c): Start API + run e2e in a single step — bare 'node &' in an early step is reaped when that step exits; the API must remain a child of the test shell through the entire Playwright run"
- "FIX-2 (7389740): Use PLAYWRIGHT_BASE_URL=http://127.0.0.1:5173 and NODE_OPTIONS=--dns-result-order=ipv4first — Vite binds IPv4-only; Node fetch does not fall back from ::1 to 127.0.0.1 unlike curl"
- "FIX-3 (e486c6b): Seed dev user id=1 (INSERT IGNORE) after migrate, before API start — DEV_AUTH_BYPASS injects the user in-memory only; on a fresh CI DB the FK constraint silently aborted the calendars seed"
- 'FIX-2 (7389740): Use PLAYWRIGHT_BASE_URL=http://127.0.0.1:5173 and NODE_OPTIONS=--dns-result-order=ipv4first — Vite binds IPv4-only; Node fetch does not fall back from ::1 to 127.0.0.1 unlike curl'
- 'FIX-3 (e486c6b): Seed dev user id=1 (INSERT IGNORE) after migrate, before API start — DEV_AUTH_BYPASS injects the user in-memory only; on a fresh CI DB the FK constraint silently aborted the calendars seed'
- "FIX-4 (03e8088): Call 'pnpm --filter @familysync/pwa test:e2e --reporter=list,html' directly — 'pnpm test:e2e -- --reporter=list,html' double-forwards '--' through two pnpm layers; Playwright treats --reporter as a test-file filter and finds no tests"
- "Phase 7 harness files (playwright.config.ts, global-setup.ts, vite.config.ts, all specs) were NOT modified — CI owns stack bring-up only (D-01/D-02 phase boundary held)"
- 'Phase 7 harness files (playwright.config.ts, global-setup.ts, vite.config.ts, all specs) were NOT modified — CI owns stack bring-up only (D-01/D-02 phase boundary held)'
- "Reporter override --reporter=list,html kept: Gitea does not render 'github' annotations; list output is legible in the log"
patterns-established:
- "Harness step pattern: install browsers, then start API + wait for :3000/health, then run Playwright — all in one step"
- 'Harness step pattern: install browsers, then start API + wait for :3000/health, then run Playwright — all in one step'
- "Idempotent user seed step: INSERT IGNORE + upsert pattern for DEV_AUTH_BYPASS user before global-setup's FK-dependent seeds"
requirements-completed: [CI-01]
@@ -95,6 +95,7 @@ completed: 2026-06-11
### Auto-fixed Issues (all Rule 3 — blocking)
**1. [Rule 3 - Blocking] API reaped at step boundary**
- **Found during:** CI run after Task 1+2 commits
- **Issue:** Bare `node apps/api/dist/index.js &` in an early step was reaped when that step exited. The browser install (multi-minute) ran next, then the test step found no API.
- **Fix:** Merged API start + curl :3000/health readiness loop + `pnpm test:e2e` into a single step; moved browser install to the step immediately before it.
@@ -102,6 +103,7 @@ completed: 2026-06-11
- **Committed in:** 53a989c
**2. [Rule 3 - Blocking] global-setup ECONNREFUSED on Vite :5173**
- **Found during:** CI run post fix 1
- **Issue:** `global-setup.ts` fetched `${PLAYWRIGHT_BASE_URL}/health`; `PLAYWRIGHT_BASE_URL` defaulted to `http://localhost:5173`; runner resolved `localhost``::1`; Vite bound only `127.0.0.1:5173``ECONNREFUSED`.
- **Fix:** Added `PLAYWRIGHT_BASE_URL: http://127.0.0.1:5173` and `NODE_OPTIONS: --dns-result-order=ipv4first` to the harness step env.
@@ -109,6 +111,7 @@ completed: 2026-06-11
- **Committed in:** 7389740
**3. [Rule 3 - Blocking] Missing dev user id=1 causes FK error in global-setup seed**
- **Found during:** CI run post fix 2
- **Issue:** `global-setup.ts` seeds `calendars` + `calendar_events` for `user_id=1`. `DEV_AUTH_BYPASS` injects that user in-memory only (no DB row). On a fresh CI DB, the `INSERT IGNORE INTO calendars` silently aborted on the `users` FK; calendar id=10 was absent; the `calendar_events` insert then failed on the calendars FK.
- **Fix:** Added a "Seed dev user" step after `db:migrate`: `INSERT IGNORE INTO users` with `id=1, oidc_iss='dev', oidc_sub='dev-user', display_name='Dev User', color='#4A90D9'`.
@@ -116,6 +119,7 @@ completed: 2026-06-11
- **Committed in:** e486c6b
**4. [Rule 3 - Blocking] --reporter flag treated as test-file filter**
- **Found during:** CI run post fix 3
- **Issue:** `pnpm test:e2e -- --reporter=list,html` from the workspace root double-forwarded `--` through two pnpm invocations, delivering `playwright test -- --reporter=list,html`; Playwright interpreted `--reporter=list,html` as a test-file path and found no tests.
- **Fix:** Changed invocation to `pnpm --filter @familysync/pwa test:e2e --reporter=list,html` — bypasses the root script delegation entirely.
@@ -171,5 +175,6 @@ None — no new network endpoints or auth paths introduced. The `DEV_AUTH_BYPASS
- Plan 04 needs `GITEA_REGISTRY_PAT` (deferred D-PROBE-08) — operator must create the PAT before the publish step can push to the Gitea container registry
---
*Phase: 08-gitea-ci*
*Completed: 2026-06-11*
_Phase: 08-gitea-ci_
_Completed: 2026-06-11_
+31 -25
View File
@@ -3,32 +3,32 @@ phase: 08-gitea-ci
plan: 04
type: execute
wave: 4
depends_on: ["08-03"]
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"
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)"
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"
- '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"
- 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"
- 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>
@@ -55,18 +55,20 @@ Output: a `publish` job in ci.yml gated on `push → main`.
</context>
<artifacts_this_phase_produces>
- `.gitea/workflows/ci.yml` (EXTENDED — adds the publish job; closes the phase)
</artifacts_this_phase_produces>
</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>
</interface_context>
<tasks>
@@ -97,6 +99,7 @@ Confirmed facts (do not re-derive):
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>
@@ -120,20 +123,22 @@ Confirmed facts (do not re-derive):
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| 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 |
| 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 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>
@@ -142,10 +147,11 @@ Confirmed facts (do not re-derive):
</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>
</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.
+18 -16
View File
@@ -1,6 +1,6 @@
---
phase: 08-gitea-ci
plan: "04"
plan: '04'
subsystem: infra
tags: [gitea, docker, ci, registry, publish, security]
@@ -18,11 +18,11 @@ affects: [phase-09, phase-10, phase-11, phase-12]
tech-stack:
added: []
patterns:
- "docker login --password-stdin (PAT piped via stdin; -p flag forbidden)"
- "dual-tag publish: :latest + :<milestone>-<shortsha> (D-04 traceability)"
- "docker build from repo root: -f apps/api/Dockerfile . (Dockerfile requires workspace manifest)"
- "docker logout in always() step to drop credential from runner after push"
- "Gitea secret prefix exclusion: REGISTRY_PAT not GITEA_REGISTRY_PAT (Gitea reserves GITEA_ prefix)"
- 'docker login --password-stdin (PAT piped via stdin; -p flag forbidden)'
- 'dual-tag publish: :latest + :<milestone>-<shortsha> (D-04 traceability)'
- 'docker build from repo root: -f apps/api/Dockerfile . (Dockerfile requires workspace manifest)'
- 'docker logout in always() step to drop credential from runner after push'
- 'Gitea secret prefix exclusion: REGISTRY_PAT not GITEA_REGISTRY_PAT (Gitea reserves GITEA_ prefix)'
key-files:
created: []
@@ -30,15 +30,15 @@ key-files:
- .gitea/workflows/ci.yml
key-decisions:
- "D-PAT-NAMING: Gitea runner silently ignores secrets with the GITEA_ prefix; secret renamed from GITEA_REGISTRY_PAT to REGISTRY_PAT in both the Gitea repo secret and ci.yml (commit 73eecf7). This is the load-bearing fix that unblocked the publish job."
- "D-SHELL-FORM: Used shell docker login/build/push (not docker/login-action or docker/build-push-action) — matches 08-RESEARCH §Pattern 6 recommendation; simpler and has no marketplace action dependency."
- "D-DUAL-TAG: Two docker push calls for :latest and :v1.1-<shortsha>; same digest, two tags. Traceability tag links image to the exact merge commit."
- "D-LOGOUT: docker logout in always() step — drops stored credential from runner regardless of push success/failure (security hygiene)."
- 'D-PAT-NAMING: Gitea runner silently ignores secrets with the GITEA_ prefix; secret renamed from GITEA_REGISTRY_PAT to REGISTRY_PAT in both the Gitea repo secret and ci.yml (commit 73eecf7). This is the load-bearing fix that unblocked the publish job.'
- 'D-SHELL-FORM: Used shell docker login/build/push (not docker/login-action or docker/build-push-action) — matches 08-RESEARCH §Pattern 6 recommendation; simpler and has no marketplace action dependency.'
- 'D-DUAL-TAG: Two docker push calls for :latest and :v1.1-<shortsha>; same digest, two tags. Traceability tag links image to the exact merge commit.'
- 'D-LOGOUT: docker logout in always() step — drops stored credential from runner regardless of push success/failure (security hygiene).'
requirements-completed: [CI-02]
duration: checkpoint-verified
completed: "2026-06-11"
completed: '2026-06-11'
---
# Phase 08 Plan 04: Publish Job Summary
@@ -58,12 +58,12 @@ completed: "2026-06-11"
- Added the `publish` job to `.gitea/workflows/ci.yml`: gated on `github.event_name == 'push' && github.ref == 'refs/heads/main'`, never fires on pull_request.
- Run #14 (merge commit 98acff8 pushed to main) confirmed SUCCESS (1m): docker login succeeded, both image tags pushed (same digest sha256:ce724852…) — `familysync-api:latest` and `familysync-api:v1.1-98acff8` present in Gitea Packages API.
- Security audit of the CI log passed: PAT masked as `***` throughout (Gitea secret scrubber); `--password-stdin` used exclusively; no `-p`/`--password` form anywhere; `docker logout` ran in the `always()` cleanup step. No plaintext token anywhere in the log.
- Identified and fixed the GITEA_-prefix secret naming bug: Gitea runner silently drops secrets whose names start with `GITEA_`; renamed secret from `GITEA_REGISTRY_PAT` to `REGISTRY_PAT` in both the repo secret and ci.yml (commit 73eecf7).
- Identified and fixed the GITEA*-prefix secret naming bug: Gitea runner silently drops secrets whose names start with `GITEA*`; renamed secret from `GITEA_REGISTRY_PAT`to`REGISTRY_PAT` in both the repo secret and ci.yml (commit 73eecf7).
## Task Commits
1. **Task 1: Add the publish job** - `ebcc38d` (feat)
2. **Task 1 fix: REGISTRY_PAT naming** - `73eecf7` (fix — GITEA_ prefix exclusion)
2. **Task 1 fix: REGISTRY_PAT naming** - `73eecf7` (fix — GITEA\_ prefix exclusion)
3. **Pre-merge cleanup: remove throwaway runner-probe.yml** - `dcf4242` (chore)
4. **Task 2: Human-verify checkpoint** — verified green (run #14); no code commit required
@@ -82,6 +82,7 @@ completed: "2026-06-11"
### Auto-fixed Issues
**1. [Rule 1 - Bug] GITEA_REGISTRY_PAT secret prefix rejected by Gitea runner**
- **Found during:** Task 1 → human-verify checkpoint (run #14 initially failed login)
- **Issue:** Gitea Actions runner silently ignores secrets with the `GITEA_` prefix (reserved namespace). The `GITEA_REGISTRY_PAT` secret was never injected into the job environment, causing `docker login` to fail with an empty password.
- **Fix:** Renamed the Gitea repo secret from `GITEA_REGISTRY_PAT` to `REGISTRY_PAT` and updated the ci.yml reference accordingly (commit 73eecf7).
@@ -96,7 +97,7 @@ completed: "2026-06-11"
## Issues Encountered
The GITEA_-prefix restriction was not documented in the plan or research notes; it was discovered empirically when the first publish run failed at docker login. The fix was straightforward once identified. No other issues encountered.
The GITEA\_-prefix restriction was not documented in the plan or research notes; it was discovered empirically when the first publish run failed at docker login. The fix was straightforward once identified. No other issues encountered.
## Next Phase Readiness
@@ -107,5 +108,6 @@ The GITEA_-prefix restriction was not documented in the plan or research notes;
- Phase 9 (Faster Write-Back) is fully independent and can start immediately.
---
*Phase: 08-gitea-ci*
*Completed: 2026-06-11*
_Phase: 08-gitea-ci_
_Completed: 2026-06-11_
+15 -2
View File
@@ -19,6 +19,7 @@ This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readi
## Implementation Decisions
### Dev-stack bring-up in CI (for the harness step)
- **D-01:** Bring up the stack with **bare background processes + a MariaDB service container** — NOT docker compose, NOT a production image.
- MariaDB runs as a Gitea **service container** (the same one the API-integration job needs; `DB_HOST=127.0.0.1`, service creds).
- The **API** runs as a background process via `pnpm dev:api` (or equivalent) with `DEV_AUTH_BYPASS=true` and `DB_HOST=127.0.0.1`, listening on `:3000`.
@@ -27,22 +28,26 @@ This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readi
- **D-02:** The harness step MUST wait for **both** the API (`:3000`) and the PWA Vite server (`:5173`) to accept connections before Playwright launches. The harness already polls `baseURL/health` (proxied to the API) in `global-setup.ts`; CI must additionally ensure the API process is up first. This is on top of the MariaDB-11 readiness loop (Pitfall 11 — `healthcheck.sh --connect --innodb_initialized`, never `mysqladmin ping`).
### Workflow topology & jobs
- **D-03:** **One workflow file with parallel, event-gated jobs.**
- `pull_request``main`: fast-checks job (lint + typecheck both apps + unit tests) runs **in parallel** with the heavier API-integration job and the harness job. Fast feedback — a lint failure does not wait behind the harness.
- `push``main` (merge): build-and-publish job runs.
- Single file so the whole regression + publish story lives in one place; accept the minor setup duplication (checkout, pnpm cache, Node-22 pin) across jobs.
### Docker image tag strategy (CI-02)
- **D-04:** On merge to `main`, publish the API image with **two tags**: `:latest` (moving pointer) **and** `:<milestone>-<shortsha>` (immutable, e.g. `v1.1-4303a1b`).
- The milestone string (e.g. `v1.1`) is read from PROJECT.md / ROADMAP.md, not hardcoded inline if avoidable.
- `<shortsha>` is the short commit SHA of the merge commit.
- Rationale: `:latest` for easy pulls; the milestone-prefixed SHA tag groups builds by release line and stays immutable for rollback/traceability.
### Failure artifacts & browser matrix
- **D-05:** Run **both** device profiles in CI — iPhone 14/WebKit **and** Pixel 7/Chromium (the full Phase 7 matrix). Install whatever system deps WebKit needs on the runner (probe in the runner-probe step).
- **D-06:** On harness **failure**, upload Playwright **traces / screenshots / videos** as CI artifacts for debugging. The config already emits `trace`/`video` `on-first-retry` and `screenshot: only-on-failure`; CI must upload the `test-results/` output. Note the config's `reporter: 'github'` may not render natively in Gitea Actions — verify during the runner probe and fall back to `list`/`html` if annotations don't surface.
### Claude's Discretion
- Exact job names, step ordering within a job, pnpm store cache key strategy, and whether fast-checks is one job or split — planner/executor decide.
- Whether the API background process is launched with `pnpm dev:api` vs a built `node dist` — pick whatever gives reliable `:3000` readiness under `DEV_AUTH_BYPASS`; the harness only needs the authed PWA reachable (Dev User 1 has no CalDAV creds, so verify layout/flows, not live event-create).
- Registry hostname / image repository path under the Gitea registry.
@@ -50,21 +55,25 @@ This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readi
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Phase scope & requirements
- `.planning/ROADMAP.md` §"Phase 8: Gitea CI" — goal, 6 success criteria, pitfalls this phase owns.
- `.planning/REQUIREMENTS.md` — CI-01 (PR regression incl. harness), CI-02 (publish image on merge).
- `.planning/PITFALLS.md` — Pitfalls 11 (MariaDB-11 readiness), 12 (runner-probe first), 13 (`--password-stdin`), 15 (SW block, harness side).
### Harness the CI step runs (reused unchanged from Phase 7)
- `apps/pwa/playwright.config.ts` — device matrix, `serviceWorkers: 'block'`, `webServer` (Vite-only, `reuseExistingServer: !CI`), `retries`/`workers`/`reporter` under `CI`, env-driven `PLAYWRIGHT_BASE_URL`.
- `apps/pwa/e2e/global-setup.ts``/health` readiness poll, `/api/me` DEV_AUTH_BYPASS reachability gate, fail-closed env guard (refuses `NODE_ENV=production` or missing `DEV_AUTH_BYPASS`), mysql2 truncate-and-seed (calendar id 10, lists/items for user 1).
- `.planning/phases/07-mobile-test-harness/07-CONTEXT.md` — Phase 7 decisions D-01..D-10 (auth strategy, SW block, env baseURL, compose-managed backend).
### Infra the CI builds/runs against
- `apps/api/Dockerfile` — multi-stage: `builder` (API), `pwa-builder` (PWA dist → `./public`), `production` target. CI publishes the `production` target.
- `docker-compose.yml` / `docker-compose.dev.yml` — service shape, MariaDB 11 healthcheck (`healthcheck.sh --connect --innodb_initialized`), dev override exposing 3306, API `dev` build target.
- `apps/pwa/vite.config.ts` — dev proxy (`/api`, `/health`, `/callback``:3000`) the harness depends on.
@@ -73,18 +82,22 @@ This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readi
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- **Playwright config + global-setup (Phase 7):** ready to run headlessly in CI. `retries: 2`, `workers: 1`, `reporter: 'github'` already gated on `process.env.CI`. CI sets `CI=true` and `PLAYWRIGHT_BASE_URL` and the harness behaves correctly. No spec changes.
- **MariaDB service-container pattern:** API-integration tests already require a real MariaDB with `DB_HOST=127.0.0.1` + service creds + Drizzle `generate`+`migrate` for schema. The harness's `global-setup` seeds the same DB directly via mysql2. One MariaDB service container can back both the integration job and the harness job.
- **Multi-stage Dockerfile:** `production` target already builds API + PWA and serves both on `:3000`. CI build/push is a thin wrapper (`docker build --target production` + `docker login --password-stdin` + `docker push`).
### Established Patterns
- **Gitea, not GitHub:** origin is self-hosted Gitea; `main` is protected (PRs only). Gitea Actions is GitHub-Actions-syntax-compatible but **do not assume `actions/setup-node` behaves identically** — runner-probe first (Pitfall 12), pin Node 22 explicitly.
- **node-cron lesson (long-running process):** not directly relevant to CI, but the API in CI is short-lived/background — no scheduler concerns.
### Integration Points
- CI orchestrates, in order, for the harness job: MariaDB service container (readiness loop) → Drizzle generate+migrate → API background process (`DEV_AUTH_BYPASS=true`, `:3000`, readiness wait) → Playwright (`webServer` starts Vite `:5173`, `global-setup` polls `/health` + `/api/me`) → specs → upload artifacts on failure.
- Publish job depends on `apps/api/Dockerfile` `production` target + Gitea registry credentials (PAT with `write:package`, piped via `--password-stdin`).
@@ -108,5 +121,5 @@ This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readi
---
*Phase: 8-Gitea CI*
*Context gathered: 2026-06-11*
_Phase: 8-Gitea CI_
_Context gathered: 2026-06-11_
@@ -11,11 +11,11 @@
## Dev-stack bring-up in CI
| Option | Description | Selected |
|--------|-------------|----------|
| Bare processes + service MariaDB | MariaDB as a Gitea service container; API as background `pnpm dev:api` with DEV_AUTH_BYPASS; Playwright webServer starts Vite. No docker-in-docker. | ✓ |
| docker compose dev override | Run docker-compose.yml + dev override inside the runner; adds image-build time + docker-in-docker. | |
| Production image for harness | Build prod image, run harness against API-served PWA on :3000; diverges from Phase 7 dev-server contract. | |
| Option | Description | Selected |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
| Bare processes + service MariaDB | MariaDB as a Gitea service container; API as background `pnpm dev:api` with DEV_AUTH_BYPASS; Playwright webServer starts Vite. No docker-in-docker. | ✓ |
| docker compose dev override | Run docker-compose.yml + dev override inside the runner; adds image-build time + docker-in-docker. | |
| Production image for harness | Build prod image, run harness against API-served PWA on :3000; diverges from Phase 7 dev-server contract. | |
**User's choice:** Bare processes + service MariaDB
**Notes:** Reuses the MariaDB service container already needed by the API-integration tests; matches the Phase 7 harness contract exactly.
@@ -24,11 +24,11 @@
## Workflow topology & jobs
| Option | Description | Selected |
|--------|-------------|----------|
| One file, parallel jobs | Single workflow; PR→main runs fast-checks in parallel with integration + harness; push→main publishes. | ✓ |
| Two files | Separate ci.yml + publish.yml; cleaner split, duplicated setup. | |
| One file, linear job | Single sequential job; simplest, slowest feedback. | |
| Option | Description | Selected |
| ----------------------- | ------------------------------------------------------------------------------------------------------ | -------- |
| One file, parallel jobs | Single workflow; PR→main runs fast-checks in parallel with integration + harness; push→main publishes. | ✓ |
| Two files | Separate ci.yml + publish.yml; cleaner split, duplicated setup. | |
| One file, linear job | Single sequential job; simplest, slowest feedback. | |
**User's choice:** One file, parallel jobs
**Notes:** Fast feedback prioritized; minor setup duplication across jobs accepted.
@@ -37,13 +37,13 @@
## Docker image tag strategy
| Option | Description | Selected |
|--------|-------------|----------|
| latest + short SHA | :latest + :<short-sha> | |
| Short SHA only | Immutable per-commit only | |
| latest only | Single moving tag | |
| semver from package.json | Version field + latest | |
| **latest + milestone short (custom)** | :latest + :<milestone>-<shortsha>, e.g. v1.1-4303a1b | ✓ |
| Option | Description | Selected |
| ------------------------------------- | ---------------------------------------------------- | -------- |
| latest + short SHA | :latest + :<short-sha> | |
| Short SHA only | Immutable per-commit only | |
| latest only | Single moving tag | |
| semver from package.json | Version field + latest | |
| **latest + milestone short (custom)** | :latest + :<milestone>-<shortsha>, e.g. v1.1-4303a1b | ✓ |
**User's choice:** latest + `v1.1-4303a1b` (milestone prefix + short SHA), confirmed in follow-up over a 3-tag variant and a no-milestone variant.
**Notes:** Milestone string read from PROJECT.md/ROADMAP, not hardcoded; SHA tag immutable for rollback.
@@ -52,11 +52,11 @@
## Failure artifacts & browser matrix
| Option | Description | Selected |
|--------|-------------|----------|
| Upload on failure + both profiles | iPhone/WebKit + Pixel/Chromium; upload traces/screenshots/videos on failure. | ✓ |
| Upload on failure + Chromium only | Pixel/Chromium only; faster, loses iOS-engine coverage. | |
| Both profiles, no artifacts | Full matrix, log-only failures. | |
| Option | Description | Selected |
| --------------------------------- | ---------------------------------------------------------------------------- | -------- |
| Upload on failure + both profiles | iPhone/WebKit + Pixel/Chromium; upload traces/screenshots/videos on failure. | ✓ |
| Upload on failure + Chromium only | Pixel/Chromium only; faster, loses iOS-engine coverage. | |
| Both profiles, no artifacts | Full matrix, log-only failures. | |
**User's choice:** Upload on failure + both profiles
**Notes:** Full mobile coverage + debuggable failures. `reporter: 'github'` may not render in Gitea — verify in runner probe, fall back if needed.
+258 -247
View File
@@ -7,6 +7,7 @@
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
@@ -32,15 +33,17 @@
### Deferred Ideas (OUT OF SCOPE)
- ROADMAP.md bookkeeping error (Phase 8 marked completed at line 29 while line 204 says "Not started") — docs cleanup, not CI scope.
</user_constraints>
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| ID | Description | Research Support |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CI-01 | Every PR targeting `main` runs full regression — lint, typecheck (both apps), unit tests, API integration tests against a MariaDB service container, and the Phase 7 mobile Playwright harness (CI brings up API + PWA dev servers + MariaDB + DEV_AUTH_BYPASS) — result gates merge. | See §Architecture Patterns for job topology, §Service Containers for MariaDB, §Dev-Stack Bring-Up for harness orchestration, §Runner-Probe Checklist for what must be verified first. |
| CI-02 | On merge to `main`, API Docker image is built and published to the Gitea container registry. | See §Docker Registry Push for Gitea registry mechanics, §Image Tagging for `v1.1-<sha>` strategy. |
| CI-02 | On merge to `main`, API Docker image is built and published to the Gitea container registry. | See §Docker Registry Push for Gitea registry mechanics, §Image Tagging for `v1.1-<sha>` strategy. |
</phase_requirements>
---
@@ -59,16 +62,16 @@ For the publish job, the Gitea container registry path is `git.bergerhouse.net/l
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Workflow orchestration | CI runner (Gitea Actions) | — | Gitea Actions owns job scheduling |
| MariaDB service container | CI runner (act_runner Docker daemon) | — | Spawned as a sibling container by act_runner |
| API background process | CI runner (host or job container) | — | `pnpm build && node dist/index.js` in a step |
| Vite dev server | Playwright webServer config | — | Playwright starts it; reuseExistingServer=false in CI |
| DB seed (global-setup) | Playwright globalSetup | API (through mysql2 direct connection) | global-setup.ts connects directly to MariaDB |
| Docker image build | CI runner (Docker socket / DinD) | — | `docker build` in a workflow step |
| Container registry push | Gitea package registry | — | `docker push git.bergerhouse.net/luckberg/familysync-api` |
| Artifact upload (traces) | Gitea Actions artifact storage | — | Via `gitea-upload-artifact` fork (see §Artifacts) |
| Capability | Primary Tier | Secondary Tier | Rationale |
| ------------------------- | ------------------------------------ | -------------------------------------- | --------------------------------------------------------- |
| Workflow orchestration | CI runner (Gitea Actions) | — | Gitea Actions owns job scheduling |
| MariaDB service container | CI runner (act_runner Docker daemon) | — | Spawned as a sibling container by act_runner |
| API background process | CI runner (host or job container) | — | `pnpm build && node dist/index.js` in a step |
| Vite dev server | Playwright webServer config | — | Playwright starts it; reuseExistingServer=false in CI |
| DB seed (global-setup) | Playwright globalSetup | API (through mysql2 direct connection) | global-setup.ts connects directly to MariaDB |
| Docker image build | CI runner (Docker socket / DinD) | — | `docker build` in a workflow step |
| Container registry push | Gitea package registry | — | `docker push git.bergerhouse.net/luckberg/familysync-api` |
| Artifact upload (traces) | Gitea Actions artifact storage | — | Via `gitea-upload-artifact` fork (see §Artifacts) |
---
@@ -76,14 +79,14 @@ For the publish job, the Gitea container registry path is `git.bergerhouse.net/l
### Workflow Actions
| Action | Version | Purpose | Status |
|--------|---------|---------|--------|
| `actions/checkout` | `@v4` | Clone repo into job | [ASSUMED] Mirrored at `gitea.com/actions/checkout`; resolves from GitHub by default via `DEFAULT_ACTIONS_URL`. Probe confirms. |
| `actions/setup-node` | `@v4` | Pin Node.js 22 | [ASSUMED] Mirrored at `gitea.com/actions/setup-node`. Probe confirms. |
| `actions/cache` | `@v4` | pnpm store cache | [ASSUMED] Known networking issue: cache server runs in runner container but job container is on a different network. May time out. Probe is required — fall back to no-cache if it fails. |
| `https://github.com/ChristopherHX/gitea-upload-artifact` | `@v4` | Upload Playwright traces | [VERIFIED: github.com/ChristopherHX/gitea-upload-artifact] Required replacement for `actions/upload-artifact@v4` which detects Gitea as GHES and aborts. |
| `docker/login-action` | `@v3` | Authenticate to Gitea registry | [ASSUMED] Referenced from GitHub by absolute URL; probe confirms. |
| `docker/build-push-action` | `@v6` | Build and push Docker image | [ASSUMED] Referenced from GitHub by absolute URL; probe confirms. |
| Action | Version | Purpose | Status |
| -------------------------------------------------------- | ------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `actions/checkout` | `@v4` | Clone repo into job | [ASSUMED] Mirrored at `gitea.com/actions/checkout`; resolves from GitHub by default via `DEFAULT_ACTIONS_URL`. Probe confirms. |
| `actions/setup-node` | `@v4` | Pin Node.js 22 | [ASSUMED] Mirrored at `gitea.com/actions/setup-node`. Probe confirms. |
| `actions/cache` | `@v4` | pnpm store cache | [ASSUMED] Known networking issue: cache server runs in runner container but job container is on a different network. May time out. Probe is required — fall back to no-cache if it fails. |
| `https://github.com/ChristopherHX/gitea-upload-artifact` | `@v4` | Upload Playwright traces | [VERIFIED: github.com/ChristopherHX/gitea-upload-artifact] Required replacement for `actions/upload-artifact@v4` which detects Gitea as GHES and aborts. |
| `docker/login-action` | `@v3` | Authenticate to Gitea registry | [ASSUMED] Referenced from GitHub by absolute URL; probe confirms. |
| `docker/build-push-action` | `@v6` | Build and push Docker image | [ASSUMED] Referenced from GitHub by absolute URL; probe confirms. |
### No `pnpm/action-setup` needed
@@ -99,14 +102,14 @@ The repo root `package.json` declares `"packageManager": "pnpm@11.5.1"`. With No
Only `gitea-upload-artifact` is an external action introduced by this phase. All other tools are GitHub-maintained official actions or Docker-maintained actions that are well-established.
| Package / Action | Registry / Source | Age | Downloads | Source Repo | Verdict | Disposition |
|---------|----------|-----|-----------|-------------|---------|-------------|
| `actions/checkout@v4` | github.com/actions/checkout | 5+ yrs | Millions | github.com/actions/checkout | OK | Approved |
| `actions/setup-node@v4` | github.com/actions/setup-node | 5+ yrs | Millions | github.com/actions/setup-node | OK | Approved |
| `actions/cache@v4` | github.com/actions/cache | 5+ yrs | Millions | github.com/actions/cache | OK | Approved — but probe may fall back |
| `ChristopherHX/gitea-upload-artifact@v4` | github.com/ChristopherHX/gitea-upload-artifact | ~2 yrs | Moderate, known fix for Gitea | github.com/ChristopherHX/gitea-upload-artifact | OK | Approved — known and cited solution to v4 GHES blocker |
| `docker/login-action@v3` | github.com/docker/login-action | 4+ yrs | Millions | github.com/docker/login-action | OK | Approved |
| `docker/build-push-action@v6` | github.com/docker/build-push-action | 4+ yrs | Millions | github.com/docker/build-push-action | OK | Approved |
| Package / Action | Registry / Source | Age | Downloads | Source Repo | Verdict | Disposition |
| ---------------------------------------- | ---------------------------------------------- | ------ | ----------------------------- | ---------------------------------------------- | ------- | ------------------------------------------------------ |
| `actions/checkout@v4` | github.com/actions/checkout | 5+ yrs | Millions | github.com/actions/checkout | OK | Approved |
| `actions/setup-node@v4` | github.com/actions/setup-node | 5+ yrs | Millions | github.com/actions/setup-node | OK | Approved |
| `actions/cache@v4` | github.com/actions/cache | 5+ yrs | Millions | github.com/actions/cache | OK | Approved — but probe may fall back |
| `ChristopherHX/gitea-upload-artifact@v4` | github.com/ChristopherHX/gitea-upload-artifact | ~2 yrs | Moderate, known fix for Gitea | github.com/ChristopherHX/gitea-upload-artifact | OK | Approved — known and cited solution to v4 GHES blocker |
| `docker/login-action@v3` | github.com/docker/login-action | 4+ yrs | Millions | github.com/docker/login-action | OK | Approved |
| `docker/build-push-action@v6` | github.com/docker/build-push-action | 4+ yrs | Millions | github.com/docker/build-push-action | OK | Approved |
**Packages removed due to SLOP verdict:** none
**Packages flagged as suspicious SUS:** none
@@ -121,31 +124,33 @@ The runner-probe workflow lives at `.gitea/workflows/runner-probe.yml`, runs onl
### What the probe must answer
| # | Check | Command in Probe | What it confirms |
|---|-------|-----------------|-----------------|
| P-01 | Node.js version | `node --version` | Node 22 available or needs `setup-node` |
| P-02 | pnpm availability | `pnpm --version` OR `corepack enable pnpm && pnpm --version` | pnpm reachable; version matches 11.x |
| P-03 | Runner mode | `cat /proc/1/cgroup | head -5` and `hostname` and `ls /.dockerenv 2>/dev/null` | Is the job running in a Docker container (act_runner Docker mode) or on bare host? This is the critical fork: service containers only work in Docker mode. |
| P-04 | Docker socket access | `docker info 2>&1 | head -10` | Docker accessible from job; needed for service containers AND publish job |
| P-05 | Service container spawn | Add `services: mariadb: image: mariadb:11` to probe job; check if `docker ps` in a step shows the mariadb container | Service containers work at all |
| P-06 | MariaDB reachability | After P-05: `mysql -h 127.0.0.1 -P 3306 -u root -proot -e "SELECT 1"` (host runner) OR `-h mariadb` (job container) | Which hostname resolves to the MariaDB service |
| P-07 | `actions/checkout` | `uses: actions/checkout@v4` | Action resolves; DEFAULT_ACTIONS_URL is set to github.com |
| P-08 | `actions/setup-node` | `uses: actions/setup-node@v4` with `node-version: '22'` | setup-node works; pins Node 22 |
| P-09 | `actions/cache` | `uses: actions/cache@v4` with a test key | Cache works without timeout; if it hangs, confirm no-cache fallback |
| P-10 | Playwright deps (WebKit) | `npx playwright install --with-deps webkit chromium 2>&1 | tail -20` | System deps installed; no sudo/apt failures |
| P-11 | `upload-artifact` | `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` with a dummy file | Upload succeeds; artifact appears in Gitea UI |
| P-12 | Docker login + push | `echo $SECRET | docker login git.bergerhouse.net --username luckberg --password-stdin` | Registry auth works with PAT |
| P-13 | `GITHUB_SHA` | `echo ${GITHUB_SHA:0:7}` | Short SHA expression produces 7-char string |
| # | Check | Command in Probe | What it confirms |
| ---- | ------------------------ | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| P-01 | Node.js version | `node --version` | Node 22 available or needs `setup-node` |
| P-02 | pnpm availability | `pnpm --version` OR `corepack enable pnpm && pnpm --version` | pnpm reachable; version matches 11.x |
| P-03 | Runner mode | `cat /proc/1/cgroup | head -5`and`hostname`and`ls /.dockerenv 2>/dev/null` | Is the job running in a Docker container (act_runner Docker mode) or on bare host? This is the critical fork: service containers only work in Docker mode. |
| P-04 | Docker socket access | `docker info 2>&1 | head -10` | Docker accessible from job; needed for service containers AND publish job |
| P-05 | Service container spawn | Add `services: mariadb: image: mariadb:11` to probe job; check if `docker ps` in a step shows the mariadb container | Service containers work at all |
| P-06 | MariaDB reachability | After P-05: `mysql -h 127.0.0.1 -P 3306 -u root -proot -e "SELECT 1"` (host runner) OR `-h mariadb` (job container) | Which hostname resolves to the MariaDB service |
| P-07 | `actions/checkout` | `uses: actions/checkout@v4` | Action resolves; DEFAULT_ACTIONS_URL is set to github.com |
| P-08 | `actions/setup-node` | `uses: actions/setup-node@v4` with `node-version: '22'` | setup-node works; pins Node 22 |
| P-09 | `actions/cache` | `uses: actions/cache@v4` with a test key | Cache works without timeout; if it hangs, confirm no-cache fallback |
| P-10 | Playwright deps (WebKit) | `npx playwright install --with-deps webkit chromium 2>&1 | tail -20` | System deps installed; no sudo/apt failures |
| P-11 | `upload-artifact` | `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` with a dummy file | Upload succeeds; artifact appears in Gitea UI |
| P-12 | Docker login + push | `echo $SECRET | docker login git.bergerhouse.net --username luckberg --password-stdin` | Registry auth works with PAT |
| P-13 | `GITHUB_SHA` | `echo ${GITHUB_SHA:0:7}` | Short SHA expression produces 7-char string |
### Critical fork: Docker mode vs host mode (P-03)
**If job runs in a Docker container (Docker mode — the recommended act_runner default):**
- Service container hostname = service label name (e.g. `mariadb`)
- Job container and service container share a Docker network automatically
- `DB_HOST=mariadb` in job env; NO port mapping needed in workflow
- This is the GitHub-Actions-compatible path; service containers work as documented
**If job runs directly on host (host mode):**
- Service containers are NOT supported by act_runner's host executor [CITED: github.com/nektos/act/issues/2711]
- The plan must use a `docker run -d --name mariadb mariadb:11 ...` step instead of `services:`
- `DB_HOST=127.0.0.1` with port `3306:3306` mapping in the `docker run` step
@@ -156,28 +161,28 @@ The runner-probe workflow lives at `.gitea/workflows/runner-probe.yml`, runs onl
## CONFIRMED-vs-VERIFY Table
| Item | Status | Notes |
|------|--------|-------|
| Workflow file at `.gitea/workflows/ci.yml` | CONFIRMED | [CITED: docs.gitea.com/usage/actions/quickstart] |
| `on: pull_request` and `on: push` triggers | CONFIRMED | Standard GitHub Actions syntax; Gitea supports these [CITED: comparison page] |
| `actions/checkout@v4` resolves from GitHub | CONFIRMED (per docs) | DEFAULT_ACTIONS_URL defaults to github.com; VERIFY-ON-RUNNER (P-07) |
| `actions/setup-node@v4` resolves | ASSUMED | Mirrored at gitea.com/actions/setup-node; VERIFY-ON-RUNNER (P-08) |
| `actions/cache@v4` works in Docker mode | ASSUMED with caveat | Known networking issue between runner container and job container; VERIFY-ON-RUNNER (P-09) |
| `services:` key starts MariaDB in Docker mode | ASSUMED from GitHub Actions docs | act_runner implements this for Docker mode; does NOT implement for host mode [CITED: nektos/act#2711]; VERIFY-ON-RUNNER (P-03 + P-05) |
| MariaDB hostname in Docker mode = service name | ASSUMED from GitHub Actions semantics | "hostname automatically mapped to label name" for containerized jobs [CITED: docs.github.com]; VERIFY-ON-RUNNER (P-06) |
| MariaDB hostname in host mode = `127.0.0.1` | CONFIRMED for host-mode + port-mapped service | [CITED: firefart.at MySQL-GitHub-Actions] |
| `healthcheck.sh --connect --innodb_initialized` works in `options:` | CONFIRMED | [CITED: mariadb.com/docs healthcheck.sh page] |
| `mysqladmin ping` does NOT work with MariaDB 11 | CONFIRMED | `mysqladmin` binary was removed from the `mariadb:11` image [CITED: github.com/mage-os/github-actions/issues/365] |
| `actions/upload-artifact@v4` works natively on Gitea | CONFIRMED BROKEN | Gitea detected as GHES; v4 aborts with `reqPackageAccess` error [CITED: github.com/go-gitea/gitea/issues/31256] |
| `ChristopherHX/gitea-upload-artifact@v4` works | ASSUMED | Known workaround; VERIFY-ON-RUNNER (P-11) |
| `reporter: 'github'` renders annotations in Gitea | UNCONFIRMED | Gitea does not fully implement GitHub workflow commands; annotations likely silently ignored. VERIFY-ON-RUNNER — fall back to `['list', 'html']` if annotations don't appear |
| `GITHUB_SHA` available in Gitea Actions | CONFIRMED | Gitea uses GitHub-compatible env var names [CITED: forum.gitea.com/t/using-github-sha-or-gitea-sha] |
| Short SHA via `${GITHUB_SHA:0:7}` | CONFIRMED | Bash substring; same forum thread |
| Docker login to Gitea registry with PAT | CONFIRMED (approach) | `secrets.GITEA_TOKEN` does NOT work for packages [CITED: forum.gitea.com/t/proper-container-registry-procedure]; use PAT with `write:package` scope [CITED: docs.gitea.com/usage/packages/container] |
| Gitea registry image path: `git.bergerhouse.net/luckberg/<image>` | CONFIRMED | Registry uses `{host}/{owner}/{image}` format [CITED: docs.gitea.com/usage/packages/container] |
| `docker/login-action@v3` + `docker/build-push-action@v6` resolve | ASSUMED | Referenced by absolute GitHub URL; VERIFY-ON-RUNNER (P-12) |
| Playwright `--with-deps` installs system deps without sudo | CONFIRMED for most cases | Playwright handles su internally; may fail if runner has no internet/apt access [CITED: playwright.dev/docs/ci] |
| `npx playwright install` does NOT cache browser binaries | CONFIRMED (deliberate) | Playwright explicitly recommends against caching browser binaries in CI [CITED: playwright.dev/docs/ci] |
| Item | Status | Notes |
| ------------------------------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Workflow file at `.gitea/workflows/ci.yml` | CONFIRMED | [CITED: docs.gitea.com/usage/actions/quickstart] |
| `on: pull_request` and `on: push` triggers | CONFIRMED | Standard GitHub Actions syntax; Gitea supports these [CITED: comparison page] |
| `actions/checkout@v4` resolves from GitHub | CONFIRMED (per docs) | DEFAULT_ACTIONS_URL defaults to github.com; VERIFY-ON-RUNNER (P-07) |
| `actions/setup-node@v4` resolves | ASSUMED | Mirrored at gitea.com/actions/setup-node; VERIFY-ON-RUNNER (P-08) |
| `actions/cache@v4` works in Docker mode | ASSUMED with caveat | Known networking issue between runner container and job container; VERIFY-ON-RUNNER (P-09) |
| `services:` key starts MariaDB in Docker mode | ASSUMED from GitHub Actions docs | act_runner implements this for Docker mode; does NOT implement for host mode [CITED: nektos/act#2711]; VERIFY-ON-RUNNER (P-03 + P-05) |
| MariaDB hostname in Docker mode = service name | ASSUMED from GitHub Actions semantics | "hostname automatically mapped to label name" for containerized jobs [CITED: docs.github.com]; VERIFY-ON-RUNNER (P-06) |
| MariaDB hostname in host mode = `127.0.0.1` | CONFIRMED for host-mode + port-mapped service | [CITED: firefart.at MySQL-GitHub-Actions] |
| `healthcheck.sh --connect --innodb_initialized` works in `options:` | CONFIRMED | [CITED: mariadb.com/docs healthcheck.sh page] |
| `mysqladmin ping` does NOT work with MariaDB 11 | CONFIRMED | `mysqladmin` binary was removed from the `mariadb:11` image [CITED: github.com/mage-os/github-actions/issues/365] |
| `actions/upload-artifact@v4` works natively on Gitea | CONFIRMED BROKEN | Gitea detected as GHES; v4 aborts with `reqPackageAccess` error [CITED: github.com/go-gitea/gitea/issues/31256] |
| `ChristopherHX/gitea-upload-artifact@v4` works | ASSUMED | Known workaround; VERIFY-ON-RUNNER (P-11) |
| `reporter: 'github'` renders annotations in Gitea | UNCONFIRMED | Gitea does not fully implement GitHub workflow commands; annotations likely silently ignored. VERIFY-ON-RUNNER — fall back to `['list', 'html']` if annotations don't appear |
| `GITHUB_SHA` available in Gitea Actions | CONFIRMED | Gitea uses GitHub-compatible env var names [CITED: forum.gitea.com/t/using-github-sha-or-gitea-sha] |
| Short SHA via `${GITHUB_SHA:0:7}` | CONFIRMED | Bash substring; same forum thread |
| Docker login to Gitea registry with PAT | CONFIRMED (approach) | `secrets.GITEA_TOKEN` does NOT work for packages [CITED: forum.gitea.com/t/proper-container-registry-procedure]; use PAT with `write:package` scope [CITED: docs.gitea.com/usage/packages/container] |
| Gitea registry image path: `git.bergerhouse.net/luckberg/<image>` | CONFIRMED | Registry uses `{host}/{owner}/{image}` format [CITED: docs.gitea.com/usage/packages/container] |
| `docker/login-action@v3` + `docker/build-push-action@v6` resolve | ASSUMED | Referenced by absolute GitHub URL; VERIFY-ON-RUNNER (P-12) |
| Playwright `--with-deps` installs system deps without sudo | CONFIRMED for most cases | Playwright handles su internally; may fail if runner has no internet/apt access [CITED: playwright.dev/docs/ci] |
| `npx playwright install` does NOT cache browser binaries | CONFIRMED (deliberate) | Playwright explicitly recommends against caching browser binaries in CI [CITED: playwright.dev/docs/ci] |
---
@@ -261,7 +266,7 @@ jobs:
--health-retries=10
--health-start-period=30s
env:
DB_HOST: mariadb # service label name — Docker mode only
DB_HOST: mariadb # service label name — Docker mode only
DB_PORT: 3306
DB_USER: familysync
DB_PASSWORD: testpass
@@ -278,32 +283,32 @@ jobs:
```yaml
# Source: [ASSUMED — standard workaround for host-mode runners]
steps:
- name: Start MariaDB
run: |
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
- name: Wait for MariaDB
run: |
deadline=$((SECONDS + 90))
until healthcheck_output=$(docker exec mariadb healthcheck.sh --connect --innodb_initialized 2>&1) \
&& [ $? -eq 0 ]; do
if [ $SECONDS -ge $deadline ]; then
echo "MariaDB did not become ready in time"
docker logs mariadb | tail -30
exit 1
fi
sleep 3
done
echo "MariaDB ready"
env:
DB_HOST: 127.0.0.1 # host-mode: service on Docker host reachable via localhost
DB_PORT: 3306
steps:
- name: Start MariaDB
run: |
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
- name: Wait for MariaDB
run: |
deadline=$((SECONDS + 90))
until healthcheck_output=$(docker exec mariadb healthcheck.sh --connect --innodb_initialized 2>&1) \
&& [ $? -eq 0 ]; do
if [ $SECONDS -ge $deadline ]; then
echo "MariaDB did not become ready in time"
docker logs mariadb | tail -30
exit 1
fi
sleep 3
done
echo "MariaDB ready"
env:
DB_HOST: 127.0.0.1 # host-mode: service on Docker host reachable via localhost
DB_PORT: 3306
```
### Pattern 3: API Background Process
@@ -314,35 +319,35 @@ jobs:
```yaml
# Source: [ASSUMED — standard CI background-process pattern]
- name: Build API
run: pnpm --filter @familysync/api build
env:
NODE_ENV: development
- name: Build API
run: pnpm --filter @familysync/api build
env:
NODE_ENV: development
- name: Start API
run: |
NODE_ENV=development \
DEV_AUTH_BYPASS=true \
DB_HOST=${{ env.DB_HOST }} \
DB_USER=familysync \
DB_PASSWORD=testpass \
DB_NAME=familysync \
node apps/api/dist/index.js &
echo $! > /tmp/api.pid
echo "API PID: $(cat /tmp/api.pid)"
- name: Start API
run: |
NODE_ENV=development \
DEV_AUTH_BYPASS=true \
DB_HOST=${{ env.DB_HOST }} \
DB_USER=familysync \
DB_PASSWORD=testpass \
DB_NAME=familysync \
node apps/api/dist/index.js &
echo $! > /tmp/api.pid
echo "API PID: $(cat /tmp/api.pid)"
- name: Wait for API (:3000)
run: |
deadline=$((SECONDS + 60))
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
if [ $SECONDS -ge $deadline ]; then
echo "API did not start in time"
kill $(cat /tmp/api.pid) 2>/dev/null || true
exit 1
fi
sleep 2
done
echo "API ready"
- name: Wait for API (:3000)
run: |
deadline=$((SECONDS + 60))
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do
if [ $SECONDS -ge $deadline ]; then
echo "API did not start in time"
kill $(cat /tmp/api.pid) 2>/dev/null || true
exit 1
fi
sleep 2
done
echo "API ready"
```
**Why `node apps/api/dist/index.js` not `pnpm dev:api`:** The `dev` script is `node --watch dist/index.js` — it needs a prior `pnpm --filter @familysync/api build` (`tsc`). Running via `node` directly (without `--watch`) is cleaner for CI since the file watcher is irrelevant. D-discretion covers this choice.
@@ -353,14 +358,14 @@ jobs:
```yaml
# Source: [ASSUMED — confirmed in project memory and PITFALLS section]
- name: Run DB migrations
run: pnpm --filter @familysync/api db:migrate
env:
DB_HOST: ${{ env.DB_HOST }}
DB_PORT: 3306
DB_USER: familysync
DB_PASSWORD: testpass
DB_NAME: familysync
- name: Run DB migrations
run: pnpm --filter @familysync/api db:migrate
env:
DB_HOST: ${{ env.DB_HOST }}
DB_PORT: 3306
DB_USER: familysync
DB_PASSWORD: testpass
DB_NAME: familysync
```
Migrations live at `apps/api/src/db/migrations/`. The `db:migrate` script calls `drizzle-kit migrate` which applies existing SQL files — safe because the schema SQL is already in the repo (from `generate` runs during development). No `generate` step needed in CI unless the schema changed in the same PR.
@@ -371,30 +376,30 @@ Migrations live at `apps/api/src/db/migrations/`. The `db:migrate` script calls
```yaml
# Source: [ASSUMED — based on playwright.config.ts and global-setup.ts already in repo]
- name: Install Playwright browsers
run: npx playwright install --with-deps webkit chromium
working-directory: apps/pwa
- name: Install Playwright browsers
run: npx playwright install --with-deps webkit chromium
working-directory: apps/pwa
- name: Run Playwright harness
run: pnpm test:e2e
env:
CI: true
PLAYWRIGHT_BASE_URL: http://localhost:5173
DEV_AUTH_BYPASS: "true"
NODE_ENV: development
DB_HOST: ${{ env.DB_HOST }}
DB_PORT: 3306
DB_USER: familysync
DB_PASSWORD: testpass
DB_NAME: familysync
- name: Run Playwright harness
run: pnpm test:e2e
env:
CI: true
PLAYWRIGHT_BASE_URL: http://localhost:5173
DEV_AUTH_BYPASS: 'true'
NODE_ENV: development
DB_HOST: ${{ env.DB_HOST }}
DB_PORT: 3306
DB_USER: familysync
DB_PASSWORD: testpass
DB_NAME: familysync
- name: Upload test artifacts
if: failure()
uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4
with:
name: playwright-traces-${{ github.run_id }}
path: apps/pwa/test-results/
retention-days: 14
- name: Upload test artifacts
if: failure()
uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4
with:
name: playwright-traces-${{ github.run_id }}
path: apps/pwa/test-results/
retention-days: 14
```
**Note on `working-directory` for playwright install:** `npx playwright install` must be run from the package root where `@playwright/test` is installed — `apps/pwa/`. [ASSUMED]
@@ -403,36 +408,36 @@ Migrations live at `apps/api/src/db/migrations/`. The `db:migrate` script calls
```yaml
# Source: [ASSUMED — based on Gitea container registry docs and forum]
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Compute image tags
id: tags
run: |
SHORT_SHA=${GITHUB_SHA:0:7}
MILESTONE="v1.1" # read from PROJECT.md in executor if preferred
echo "latest=git.bergerhouse.net/luckberg/familysync-api:latest" >> $GITHUB_OUTPUT
echo "sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Compute image tags
id: tags
run: |
SHORT_SHA=${GITHUB_SHA:0:7}
MILESTONE="v1.1" # read from PROJECT.md in executor if preferred
echo "latest=git.bergerhouse.net/luckberg/familysync-api:latest" >> $GITHUB_OUTPUT
echo "sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}" >> $GITHUB_OUTPUT
- name: Docker login
run: |
echo "${{ secrets.GITEA_REGISTRY_PAT }}" | \
docker login git.bergerhouse.net \
--username luckberg \
--password-stdin
- name: Docker login
run: |
echo "${{ secrets.GITEA_REGISTRY_PAT }}" | \
docker login git.bergerhouse.net \
--username luckberg \
--password-stdin
- name: Build and push
run: |
docker build \
--target production \
-t ${{ steps.tags.outputs.latest }} \
-t ${{ steps.tags.outputs.sha_tag }} \
.
docker push ${{ steps.tags.outputs.latest }}
docker push ${{ steps.tags.outputs.sha_tag }}
- name: Build and push
run: |
docker build \
--target production \
-t ${{ steps.tags.outputs.latest }} \
-t ${{ steps.tags.outputs.sha_tag }} \
.
docker push ${{ steps.tags.outputs.latest }}
docker push ${{ steps.tags.outputs.sha_tag }}
```
**Secret name:** `GITEA_REGISTRY_PAT` — a PAT with `write:package` (and `read:package`) scope created by `luckberg`. Must be added to the repo secrets in Gitea UI before the publish job runs.
@@ -498,20 +503,20 @@ The orchestration order is critical. All of the following must be sequential wit
### Registry Details
| Property | Value | Source |
|----------|-------|--------|
| Registry host | `git.bergerhouse.net` | [CONFIRMED from git remote URL] |
| Image path format | `git.bergerhouse.net/{owner}/{image}` | [CITED: docs.gitea.com/usage/packages/container] |
| Image name | `git.bergerhouse.net/luckberg/familysync-api` | [ASSUMED — owner = `luckberg`, image name = `familysync-api`] |
| Auth method | PAT with `write:package` scope | [CONFIRMED: Gitea forum, registry docs] |
| Token variable | `secrets.GITEA_REGISTRY_PAT` | [ASSUMED — name chosen by planner/executor] |
| `docker login` approach | `echo $PAT \| docker login git.bergerhouse.net --username luckberg --password-stdin` | [CONFIRMED: Pitfall 13] |
| Property | Value | Source |
| ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------- |
| Registry host | `git.bergerhouse.net` | [CONFIRMED from git remote URL] |
| Image path format | `git.bergerhouse.net/{owner}/{image}` | [CITED: docs.gitea.com/usage/packages/container] |
| Image name | `git.bergerhouse.net/luckberg/familysync-api` | [ASSUMED — owner = `luckberg`, image name = `familysync-api`] |
| Auth method | PAT with `write:package` scope | [CONFIRMED: Gitea forum, registry docs] |
| Token variable | `secrets.GITEA_REGISTRY_PAT` | [ASSUMED — name chosen by planner/executor] |
| `docker login` approach | `echo $PAT \| docker login git.bergerhouse.net --username luckberg --password-stdin` | [CONFIRMED: Pitfall 13] |
### Image Tag Strategy (D-04)
| Tag | Example | Purpose |
|-----|---------|---------|
| `:latest` | `git.bergerhouse.net/luckberg/familysync-api:latest` | Moving pointer for easy pulls |
| Tag | Example | Purpose |
| ------------------------- | ---------------------------------------------------------- | ----------------------------- |
| `:latest` | `git.bergerhouse.net/luckberg/familysync-api:latest` | Moving pointer for easy pulls |
| `:<milestone>-<shortsha>` | `git.bergerhouse.net/luckberg/familysync-api:v1.1-4303a1b` | Immutable, rollback-traceable |
The milestone string `v1.1` is hardcoded in the workflow as `MILESTONE="v1.1"` for now (reading it from `PROJECT.md` dynamically adds complexity with minimal benefit). The executor can make it a workflow-level env var for easy updates.
@@ -606,14 +611,14 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| MariaDB health check | Custom TCP-ping script | `healthcheck.sh --connect --innodb_initialized` | Ships in the `mariadb:11` image; handles InnoDB init correctly |
| Upload artifacts to Gitea | curl to Gitea API | `ChristopherHX/gitea-upload-artifact@v4` | upload-artifact v4 protocol is complex; the fork wraps it correctly |
| Docker registry auth | Hand-rolled auth header | `docker login --password-stdin` | Prevents PAT from appearing in process list |
| Playwright browser install | Manual apt package list | `npx playwright install --with-deps` | Playwright knows the correct system deps for each browser version |
| API readiness check | Arbitrary sleep | curl retry loop against `/health` | Sleep is flaky; a deterministic health poll is both faster and correct |
| CI MariaDB in host mode | `mysqladmin` ping loop | `docker exec mariadb healthcheck.sh --connect --innodb_initialized` | Avoids mysqladmin-missing error; reuses same logic as Docker healthcheck |
| Problem | Don't Build | Use Instead | Why |
| -------------------------- | ----------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| MariaDB health check | Custom TCP-ping script | `healthcheck.sh --connect --innodb_initialized` | Ships in the `mariadb:11` image; handles InnoDB init correctly |
| Upload artifacts to Gitea | curl to Gitea API | `ChristopherHX/gitea-upload-artifact@v4` | upload-artifact v4 protocol is complex; the fork wraps it correctly |
| Docker registry auth | Hand-rolled auth header | `docker login --password-stdin` | Prevents PAT from appearing in process list |
| Playwright browser install | Manual apt package list | `npx playwright install --with-deps` | Playwright knows the correct system deps for each browser version |
| API readiness check | Arbitrary sleep | curl retry loop against `/health` | Sleep is flaky; a deterministic health poll is both faster and correct |
| CI MariaDB in host mode | `mysqladmin` ping loop | `docker exec mariadb healthcheck.sh --connect --innodb_initialized` | Avoids mysqladmin-missing error; reuses same logic as Docker healthcheck |
---
@@ -621,25 +626,25 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
### Test Framework
| Property | Value |
|----------|-------|
| API unit tests | Vitest 4.1.x, config at `apps/api/vitest.config.ts` |
| API test command | `pnpm --filter @familysync/api test` (= `vitest run`) |
| PWA unit tests | Vitest (same framework), command `pnpm --filter @familysync/pwa test` |
| E2E harness | `@playwright/test` 1.60.0, config at `apps/pwa/playwright.config.ts` |
| E2E command | `pnpm test:e2e` (from root) = `pnpm --filter @familysync/pwa test:e2e` = `playwright test` |
| Property | Value |
| ---------------- | ------------------------------------------------------------------------------------------ |
| API unit tests | Vitest 4.1.x, config at `apps/api/vitest.config.ts` |
| API test command | `pnpm --filter @familysync/api test` (= `vitest run`) |
| PWA unit tests | Vitest (same framework), command `pnpm --filter @familysync/pwa test` |
| E2E harness | `@playwright/test` 1.60.0, config at `apps/pwa/playwright.config.ts` |
| E2E command | `pnpm test:e2e` (from root) = `pnpm --filter @familysync/pwa test:e2e` = `playwright test` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | Exists? |
|--------|----------|-----------|-------------------|---------|
| CI-01 | PR gate triggers on `pull_request → main` | workflow trigger test | Push a PR and observe | After W0 |
| CI-01 | lint passes | CI step | `pnpm lint` | ✅ |
| CI-01 | typecheck both apps passes | CI step | `pnpm typecheck` | ✅ |
| CI-01 | unit tests pass | CI step | `pnpm test` | ✅ |
| CI-01 | API integration tests pass with MariaDB | CI step | `pnpm --filter @familysync/api test` + DB env | ✅ |
| CI-01 | Playwright harness passes in CI | CI step | `pnpm test:e2e` with CI=true | ✅ (Phase 7 specs) |
| CI-02 | Docker image pushed to Gitea registry on merge | CI step | `docker pull git.bergerhouse.net/luckberg/familysync-api:latest` | After W2 |
| Req ID | Behavior | Test Type | Automated Command | Exists? |
| ------ | ---------------------------------------------- | --------------------- | ---------------------------------------------------------------- | ------------------ |
| CI-01 | PR gate triggers on `pull_request → main` | workflow trigger test | Push a PR and observe | After W0 |
| CI-01 | lint passes | CI step | `pnpm lint` | ✅ |
| CI-01 | typecheck both apps passes | CI step | `pnpm typecheck` | ✅ |
| CI-01 | unit tests pass | CI step | `pnpm test` | ✅ |
| CI-01 | API integration tests pass with MariaDB | CI step | `pnpm --filter @familysync/api test` + DB env | ✅ |
| CI-01 | Playwright harness passes in CI | CI step | `pnpm test:e2e` with CI=true | ✅ (Phase 7 specs) |
| CI-02 | Docker image pushed to Gitea registry on merge | CI step | `docker pull git.bergerhouse.net/luckberg/familysync-api:latest` | After W2 |
### Sampling Rate
@@ -658,42 +663,44 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | Auth is not modified by this phase |
| V3 Session Management | no | Not modified |
| V4 Access Control | no | Not modified |
| V5 Input Validation | no | No new API endpoints |
| V6 Cryptography | yes (marginal) | PAT stored as Gitea repository secret; never in workflow YAML |
| ASVS Category | Applies | Standard Control |
| --------------------- | -------------- | ------------------------------------------------------------- |
| V2 Authentication | no | Auth is not modified by this phase |
| V3 Session Management | no | Not modified |
| V4 Access Control | no | Not modified |
| V5 Input Validation | no | No new API endpoints |
| V6 Cryptography | yes (marginal) | PAT stored as Gitea repository secret; never in workflow YAML |
### Known Threat Patterns for CI/Docker
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| PAT in workflow YAML | Information Disclosure | Store as `secrets.GITEA_REGISTRY_PAT`; never echo or print |
| Docker socket mount (if runner uses it) | Elevation of Privilege | Known risk; accepted for Unraid self-hosted runner per Gitea docs |
| DB creds in CI env | Information Disclosure | Use throwaway test creds (not production DB_PASSWORD); never reuse production secrets |
| `DEV_AUTH_BYPASS=true` in CI | Spoofing | Only active in harness job; never bleeds to publish job; global-setup guard refuses `NODE_ENV=production` |
| Pattern | STRIDE | Standard Mitigation |
| --------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
| PAT in workflow YAML | Information Disclosure | Store as `secrets.GITEA_REGISTRY_PAT`; never echo or print |
| Docker socket mount (if runner uses it) | Elevation of Privilege | Known risk; accepted for Unraid self-hosted runner per Gitea docs |
| DB creds in CI env | Information Disclosure | Use throwaway test creds (not production DB_PASSWORD); never reuse production secrets |
| `DEV_AUTH_BYPASS=true` in CI | Spoofing | Only active in harness job; never bleeds to publish job; global-setup guard refuses `NODE_ENV=production` |
---
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Gitea instance | All | ✓ | 1.26.2 | — |
| Gitea Actions runner | All | Unknown — 0 registered | Unknown | Must register runner before Phase 8 can proceed |
| Docker on runner | service containers, publish | Unknown | Unknown | Phase 8 is blocked without Docker on runner |
| Node.js 22 on runner | fast-checks, integration | Unknown | Unknown | `actions/setup-node@v4` (probe P-01/P-08) |
| pnpm 11 on runner | All | Unknown | Unknown | `corepack enable pnpm` (probe P-02) |
| Internet access from runner | actions resolution, npm, Playwright install | Unknown | — | Probe P-07 confirms |
| Gitea registry PAT | CI-02 | Not yet created | — | Operator must create before publish job |
| Dependency | Required By | Available | Version | Fallback |
| --------------------------- | ------------------------------------------- | ---------------------- | ------- | ----------------------------------------------- |
| Gitea instance | All | ✓ | 1.26.2 | — |
| Gitea Actions runner | All | Unknown — 0 registered | Unknown | Must register runner before Phase 8 can proceed |
| Docker on runner | service containers, publish | Unknown | Unknown | Phase 8 is blocked without Docker on runner |
| Node.js 22 on runner | fast-checks, integration | Unknown | Unknown | `actions/setup-node@v4` (probe P-01/P-08) |
| pnpm 11 on runner | All | Unknown | Unknown | `corepack enable pnpm` (probe P-02) |
| Internet access from runner | actions resolution, npm, Playwright install | Unknown | — | Probe P-07 confirms |
| Gitea registry PAT | CI-02 | Not yet created | — | Operator must create before publish job |
**Missing dependencies with no fallback:**
- Gitea Actions runner on Unraid (0 registered) — must be installed and registered before any CI runs
- Docker on runner — if absent, service containers and publish job both fail; no CI-relevant fallback
**Missing dependencies with fallback:**
- Node.js 22 — `actions/setup-node@v4` installs it
- pnpm — `corepack enable pnpm` resolves it
@@ -701,18 +708,18 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | Runner is configured in Docker mode (not host mode) | Service Containers, Dev-Stack Bring-Up | Entire `services:` approach breaks; must pivot to `docker run -d` pattern |
| A2 | `actions/checkout@v4` and `actions/setup-node@v4` resolve via DEFAULT_ACTIONS_URL=github.com | Standard Stack | CI fails at checkout; need to mirror or use absolute URLs |
| A3 | `actions/cache@v4` works without timeout in this runner's Docker network setup | Standard Stack | Cache steps time out; must remove and accept full install on every run |
| A4 | `ChristopherHX/gitea-upload-artifact@v4` uploads successfully to Gitea 1.26.2 | Standard Stack | No artifact upload on failure; lose traces; manual debug only |
| A5 | `docker/login-action@v3` and `docker/build-push-action@v6` resolve from GitHub | Standard Stack | Must use shell-level `docker login` + `docker build`/`docker push` instead |
| A6 | `reporter: 'github'` produces invisible output in Gitea (not rendered as annotations) | Anti-Patterns | If Gitea DOES render them, the `--reporter=list` override is unnecessary but harmless |
| A7 | `GITHUB_SHA` is available in Gitea Actions workflows | Image Tagging | Cannot compute short SHA via `${GITHUB_SHA:0:7}`; must use `git rev-parse --short HEAD` |
| A8 | Image name follows `git.bergerhouse.net/luckberg/familysync-api` convention | Registry Details | Push fails with 404; image name may need adjustment |
| A9 | MariaDB `--health-start-period=30s` is sufficient for initialization | Patterns | Flaky health-check failures on slow runners; tune upward |
| A10 | Playwright install `--with-deps` succeeds without root on runner | Dev-Stack Bring-Up | WebKit missing system libs; jobs fail with browser launch error |
| # | Claim | Section | Risk if Wrong |
| --- | -------------------------------------------------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------- |
| A1 | Runner is configured in Docker mode (not host mode) | Service Containers, Dev-Stack Bring-Up | Entire `services:` approach breaks; must pivot to `docker run -d` pattern |
| A2 | `actions/checkout@v4` and `actions/setup-node@v4` resolve via DEFAULT_ACTIONS_URL=github.com | Standard Stack | CI fails at checkout; need to mirror or use absolute URLs |
| A3 | `actions/cache@v4` works without timeout in this runner's Docker network setup | Standard Stack | Cache steps time out; must remove and accept full install on every run |
| A4 | `ChristopherHX/gitea-upload-artifact@v4` uploads successfully to Gitea 1.26.2 | Standard Stack | No artifact upload on failure; lose traces; manual debug only |
| A5 | `docker/login-action@v3` and `docker/build-push-action@v6` resolve from GitHub | Standard Stack | Must use shell-level `docker login` + `docker build`/`docker push` instead |
| A6 | `reporter: 'github'` produces invisible output in Gitea (not rendered as annotations) | Anti-Patterns | If Gitea DOES render them, the `--reporter=list` override is unnecessary but harmless |
| A7 | `GITHUB_SHA` is available in Gitea Actions workflows | Image Tagging | Cannot compute short SHA via `${GITHUB_SHA:0:7}`; must use `git rev-parse --short HEAD` |
| A8 | Image name follows `git.bergerhouse.net/luckberg/familysync-api` convention | Registry Details | Push fails with 404; image name may need adjustment |
| A9 | MariaDB `--health-start-period=30s` is sufficient for initialization | Patterns | Flaky health-check failures on slow runners; tune upward |
| A10 | Playwright install `--with-deps` succeeds without root on runner | Dev-Stack Bring-Up | WebKit missing system libs; jobs fail with browser launch error |
---
@@ -748,6 +755,7 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
## Sources
### Primary (HIGH confidence)
- [Gitea container registry docs](https://docs.gitea.com/usage/packages/container) — registry host format, image naming, PAT auth requirement
- [Gitea Actions comparison page](https://docs.gitea.com/usage/actions/comparison) — what is and isn't supported vs GitHub Actions
- [Gitea Actions quickstart](https://docs.gitea.com/usage/actions/quickstart) — `.gitea/workflows/` location confirmed
@@ -762,11 +770,13 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
- [Playwright CI docs](https://playwright.dev/docs/ci) — `--with-deps` install, no-cache recommendation
### Secondary (MEDIUM confidence)
- [firefart.at: MySQL service with GitHub Actions](https://firefart.at/post/using-mysql-service-with-github-actions/) — service container pattern when job runs on host (port mapping, 127.0.0.1)
- [Gitea forum: service container not starting](https://forum.gitea.com/t/service-container-not-starting/9287) — evidence service containers are unreliable in some configurations; unresolved in forum
- Various community blog posts on Gitea Actions (chrisliebaer, botmonster) — cross-check on action resolution and registry
### Tertiary (LOW confidence / ASSUMED)
- All items tagged `[ASSUMED]` in this document — confirmed via training knowledge + community reports but not directly verified against the Unraid act_runner; confirmed by runner-probe
---
@@ -774,6 +784,7 @@ This is because the Dockerfile copies the pnpm workspace manifest and lockfile f
## Metadata
**Confidence breakdown:**
- Gitea Actions workflow syntax: HIGH — standard GitHub Actions YAML; confirmed supported
- Service containers: MEDIUM — Docker mode works per docs/act design; host mode does not; runner mode unknown
- MariaDB healthcheck: HIGH — confirmed in official docs and multiple issue threads
+27 -27
View File
@@ -15,13 +15,13 @@ created: 2026-06-11
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | Vitest 4.1.x (API + PWA unit), @playwright/test 1.60.0 (E2E harness) |
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/playwright.config.ts` |
| **Quick run command** | `pnpm lint && pnpm typecheck` |
| **Full suite command** | `pnpm test` then `pnpm test:e2e` (CI=true, dev stack up) |
| **Estimated runtime** | unit ~tens of seconds; harness several minutes (2 device profiles) |
| Property | Value |
| ---------------------- | -------------------------------------------------------------------- |
| **Framework** | Vitest 4.1.x (API + PWA unit), @playwright/test 1.60.0 (E2E harness) |
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/playwright.config.ts` |
| **Quick run command** | `pnpm lint && pnpm typecheck` |
| **Full suite command** | `pnpm test` then `pnpm test:e2e` (CI=true, dev stack up) |
| **Estimated runtime** | unit ~tens of seconds; harness several minutes (2 device profiles) |
**Note:** Phase 8 delivers CI plumbing (`.gitea/workflows/*.yml`). The "tests" for this phase are the CI workflow runs themselves — validation is observed by triggering the workflow on a branch/PR and reading Gitea Actions logs, not by a local unit-test file per task.
@@ -38,21 +38,21 @@ created: 2026-06-11
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| P01-T1 register runner + PAT | 08-01 | 1 | CI-01/CI-02 | T-08-PAT | runner online; PAT secret stored, never in repo | operator/manual | Gitea Actions runner list + repo secret present | ❌ operator | ⬜ pending |
| P01-T2 author runner-probe.yml | 08-01 | 1 | CI-01 | T-08-01 | probe is non-destructive; touches no secret | grep gate | `grep` healthcheck.sh + gitea-upload fork; `! grep` mysqladmin/upload-artifact@v4 | ❌ W0 | ⬜ pending |
| P01-T3 run probe, record forks | 08-01 | 1 | CI-01 | T-08-01 | runner mode / cache / WebKit / artifact answers captured | workflow run | observe runner-probe run in Gitea Actions | ❌ W0 | ⬜ pending |
| P02-T1 fast-checks job | 08-02 | 2 | CI-01 | — | lint+typecheck+PWA-unit gate the PR | grep gate + workflow run | `grep` node-pin/typecheck/pwa test; PR run green | ✅ scripts | ⬜ pending |
| P02-T2 api job (MariaDB+migrate) | 08-02 | 2 | CI-01 | T-08-03/04/05 | DB-backed API tests pass cold; migrate-not-push; throwaway creds | grep gate + workflow run | `grep` healthcheck.sh+db:migrate; `! grep` mysqladmin/db:push; cold PR run green | ✅ scripts | ⬜ pending |
| P02-T3 verify PR jobs | 08-02 | 2 | CI-01 | — | both jobs run parallel, api green cold | workflow run | observe fast-checks ∥ api on a PR | ❌ W1 | ⬜ pending |
| P03-T1 harness stack bring-up | 08-03 | 3 | CI-01 | T-08-06/07/08 | API bg w/ inline DEV_AUTH_BYPASS; :3000 readiness before Playwright | grep gate + workflow run | `grep` inline-bypass+:3000/health+db:migrate; `! grep` db:push | ✅ Phase 7 specs | ⬜ pending |
| P03-T2 playwright run + artifacts | 08-03 | 3 | CI-01 | T-08-06 | both profiles run; traces upload on failure; no spec edits | grep gate + workflow run | `grep` webkit+chromium+base-url+gitea-upload; `git diff --quiet` harness files | ✅ Phase 7 specs | ⬜ pending |
| P03-T3 verify harness on PR | 08-03 | 3 | CI-01 | — | iphone+pixel green vs CI dev stack; artifact confirmed | workflow run | observe harness job on a PR | ❌ W2 | ⬜ pending |
| P04-T1 publish job | 08-04 | 4 | CI-02 | T-08-PAT/09/10 | --password-stdin only; both tags; production target from root; no bypass | grep gate | `grep` --password-stdin+target production+both tags; `! grep` -p/--password/DEV_AUTH_BYPASS-in-publish | ❌ W2 | ⬜ pending |
| P04-T2 merge, audit log, verify tags | 08-04 | 4 | CI-02 | T-08-PAT | no plaintext PAT in log; :latest + :v1.1-<sha> in registry | workflow run + log audit | merge → audit publish log + check Packages | ❌ W2 | ⬜ pending |
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
| ------------------------------------ | ----- | ---- | ----------- | -------------- | ------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------- | ---------- |
| P01-T1 register runner + PAT | 08-01 | 1 | CI-01/CI-02 | T-08-PAT | runner online; PAT secret stored, never in repo | operator/manual | Gitea Actions runner list + repo secret present | ❌ operator | ⬜ pending |
| P01-T2 author runner-probe.yml | 08-01 | 1 | CI-01 | T-08-01 | probe is non-destructive; touches no secret | grep gate | `grep` healthcheck.sh + gitea-upload fork; `! grep` mysqladmin/upload-artifact@v4 | ❌ W0 | ⬜ pending |
| P01-T3 run probe, record forks | 08-01 | 1 | CI-01 | T-08-01 | runner mode / cache / WebKit / artifact answers captured | workflow run | observe runner-probe run in Gitea Actions | ❌ W0 | ⬜ pending |
| P02-T1 fast-checks job | 08-02 | 2 | CI-01 | — | lint+typecheck+PWA-unit gate the PR | grep gate + workflow run | `grep` node-pin/typecheck/pwa test; PR run green | ✅ scripts | ⬜ pending |
| P02-T2 api job (MariaDB+migrate) | 08-02 | 2 | CI-01 | T-08-03/04/05 | DB-backed API tests pass cold; migrate-not-push; throwaway creds | grep gate + workflow run | `grep` healthcheck.sh+db:migrate; `! grep` mysqladmin/db:push; cold PR run green | ✅ scripts | ⬜ pending |
| P02-T3 verify PR jobs | 08-02 | 2 | CI-01 | — | both jobs run parallel, api green cold | workflow run | observe fast-checks ∥ api on a PR | ❌ W1 | ⬜ pending |
| P03-T1 harness stack bring-up | 08-03 | 3 | CI-01 | T-08-06/07/08 | API bg w/ inline DEV_AUTH_BYPASS; :3000 readiness before Playwright | grep gate + workflow run | `grep` inline-bypass+:3000/health+db:migrate; `! grep` db:push | ✅ Phase 7 specs | ⬜ pending |
| P03-T2 playwright run + artifacts | 08-03 | 3 | CI-01 | T-08-06 | both profiles run; traces upload on failure; no spec edits | grep gate + workflow run | `grep` webkit+chromium+base-url+gitea-upload; `git diff --quiet` harness files | ✅ Phase 7 specs | ⬜ pending |
| P03-T3 verify harness on PR | 08-03 | 3 | CI-01 | — | iphone+pixel green vs CI dev stack; artifact confirmed | workflow run | observe harness job on a PR | ❌ W2 | ⬜ pending |
| P04-T1 publish job | 08-04 | 4 | CI-02 | T-08-PAT/09/10 | --password-stdin only; both tags; production target from root; no bypass | grep gate | `grep` --password-stdin+target production+both tags; `! grep` -p/--password/DEV_AUTH_BYPASS-in-publish | ❌ W2 | ⬜ pending |
| P04-T2 merge, audit log, verify tags | 08-04 | 4 | CI-02 | T-08-PAT | no plaintext PAT in log; :latest + :v1.1-<sha> in registry | workflow run + log audit | merge → audit publish log + check Packages | ❌ W2 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky — planner expands one row per task.*
_Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky — planner expands one row per task._
---
@@ -61,17 +61,17 @@ created: 2026-06-11
- [ ] `.gitea/workflows/runner-probe.yml` — runner-probe workflow (new file; Wave 0 task). Must answer: runner mode (Docker vs host), Docker socket access, `actions/*` resolution, Node 22 / pnpm availability, internet access, WebKit dep installability.
- [ ] `.gitea/workflows/ci.yml` — main CI workflow scaffolding (new file; Waves 12).
*Existing unit/integration/E2E infrastructure (Vitest + Playwright) is reused unchanged; no new local test framework is installed.*
_Existing unit/integration/E2E infrastructure (Vitest + Playwright) is reused unchanged; no new local test framework is installed._
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Runner registered on Unraid | CI-01/CI-02 | Operator infra action outside the repo; 0 runners currently registered | Install/register `act_runner` on Unraid against `git.bergerhouse.net`; confirm it appears in Gitea Actions runners list |
| Gitea registry PAT created | CI-02 | Secret creation is an operator action; cannot be scripted in-repo | Create PAT with `write:package`; store as repo secret (e.g. `GITEA_REGISTRY_PAT`); confirm publish job authenticates |
| PR merge actually blocked on failure | CI-01 | Requires branch-protection "required status checks" config in Gitea | Configure required checks on `main`; open a failing PR; confirm merge button is blocked |
| Behavior | Requirement | Why Manual | Test Instructions |
| ------------------------------------ | ----------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Runner registered on Unraid | CI-01/CI-02 | Operator infra action outside the repo; 0 runners currently registered | Install/register `act_runner` on Unraid against `git.bergerhouse.net`; confirm it appears in Gitea Actions runners list |
| Gitea registry PAT created | CI-02 | Secret creation is an operator action; cannot be scripted in-repo | Create PAT with `write:package`; store as repo secret (e.g. `GITEA_REGISTRY_PAT`); confirm publish job authenticates |
| PR merge actually blocked on failure | CI-01 | Requires branch-protection "required status checks" config in Gitea | Configure required checks on `main`; open a failing PR; confirm merge button is blocked |
---
+17 -17
View File
@@ -12,22 +12,22 @@ Every PR to `main` runs a full regression that gates the merge — lint, typeche
## Six-Criteria Evidence Table
| # | Success Criterion | Status | Evidence |
|---|-------------------|--------|----------|
| 1 | PR triggers lint + typecheck + unit + API-integration vs MariaDB service container; failing run blocks merge | **PASSED** | Runs #11 and #12: `fast-checks` (191s, lint no-op + tsc both apps + PWA vitest) and `api` (238s, MariaDB cold start + drizzle-kit migrate + DB-backed integration tests) both green. Run blocking enforced by Gitea branch protection on `main`. |
| 2 | API integration tests connect to service-container MariaDB (DB_HOST=127.0.0.1) and pass reliably on cold first run | **PASSED** | Run #11 cold start: MariaDB readiness wait (mysql2 loop) completed before migrate; squashed migration baseline (commit c0f892c) eliminated false drizzle-kit destructive diff; all API integration tests passed on first attempt. |
| 3 | PR workflow brings up dev stack (API + PWA dev servers + MariaDB, DEV_AUTH_BYPASS=true) and runs Phase 7 mobile Playwright harness headlessly; harness failure blocks merge | **PASSED** | Run #11: `harness` job — 58 specs across iphone/WebKit + pixel/Chromium — all green in 1.6 min. Four CI-specific infrastructure fixes applied (API-reap timeout, ipv4first/127.0.0.1 DNS, dev-user seed, direct test:e2e call). No Phase 7 harness files modified. |
| 4 | Harness step waits for both API and PWA dev servers before launching Playwright (no startup-race flake) | **PASSED** | mysql2 readiness loop (DB) → `:3000/health` poll (API) → global-setup `:5173` poll (PWA) — three sequential readiness gates before Playwright launches. No flake observed on cold or warm runs. |
| 5 | Merge to `main` builds and pushes API Docker image under a sensible tag | **PASSED** | Run #14 (push of merge commit 98acff8): `docker build --target production -f apps/api/Dockerfile .` succeeded; both tags pushed — `familysync-api:latest` and `familysync-api:v1.1-98acff8` — same digest `sha256:ce724852…`; confirmed present in Gitea Packages API. |
| 6 | Registry credentials never appear in plaintext in the CI logs | **PASSED** | Log audit of run #14: PAT masked as `***` throughout (Gitea secret scrubber); `--password-stdin` used exclusively (token piped via stdin); no `-p`/`--password` argument form anywhere; `docker logout` ran in `always()` cleanup. REGISTRY_PAT naming used (GITEA_ prefix excluded to avoid Gitea's silent secret-drop behavior). |
| # | Success Criterion | Status | Evidence |
| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | PR triggers lint + typecheck + unit + API-integration vs MariaDB service container; failing run blocks merge | **PASSED** | Runs #11 and #12: `fast-checks` (191s, lint no-op + tsc both apps + PWA vitest) and `api` (238s, MariaDB cold start + drizzle-kit migrate + DB-backed integration tests) both green. Run blocking enforced by Gitea branch protection on `main`. |
| 2 | API integration tests connect to service-container MariaDB (DB_HOST=127.0.0.1) and pass reliably on cold first run | **PASSED** | Run #11 cold start: MariaDB readiness wait (mysql2 loop) completed before migrate; squashed migration baseline (commit c0f892c) eliminated false drizzle-kit destructive diff; all API integration tests passed on first attempt. |
| 3 | PR workflow brings up dev stack (API + PWA dev servers + MariaDB, DEV_AUTH_BYPASS=true) and runs Phase 7 mobile Playwright harness headlessly; harness failure blocks merge | **PASSED** | Run #11: `harness` job — 58 specs across iphone/WebKit + pixel/Chromium — all green in 1.6 min. Four CI-specific infrastructure fixes applied (API-reap timeout, ipv4first/127.0.0.1 DNS, dev-user seed, direct test:e2e call). No Phase 7 harness files modified. |
| 4 | Harness step waits for both API and PWA dev servers before launching Playwright (no startup-race flake) | **PASSED** | mysql2 readiness loop (DB) → `:3000/health` poll (API) → global-setup `:5173` poll (PWA) — three sequential readiness gates before Playwright launches. No flake observed on cold or warm runs. |
| 5 | Merge to `main` builds and pushes API Docker image under a sensible tag | **PASSED** | Run #14 (push of merge commit 98acff8): `docker build --target production -f apps/api/Dockerfile .` succeeded; both tags pushed — `familysync-api:latest` and `familysync-api:v1.1-98acff8` — same digest `sha256:ce724852…`; confirmed present in Gitea Packages API. |
| 6 | Registry credentials never appear in plaintext in the CI logs | **PASSED** | Log audit of run #14: PAT masked as `***` throughout (Gitea secret scrubber); `--password-stdin` used exclusively (token piped via stdin); no `-p`/`--password` argument form anywhere; `docker logout` ran in `always()` cleanup. REGISTRY*PAT naming used (GITEA* prefix excluded to avoid Gitea's silent secret-drop behavior). |
## Plans Delivering the Criteria
| Plan | Contribution |
|------|-------------|
| 08-01 | Runner probe — confirmed ubuntu-latest label, Docker executor (/.dockerenv), GITHUB_SHA availability, Playwright deps install cleanly |
| 08-02 | ci.yml: fast-checks job (lint, tsc, PWA vitest) + api job (MariaDB service, mysql2 readiness, drizzle-kit migrate, integration tests). Delivers criteria 1 + 2. |
| 08-03 | ci.yml: harness job (dev-stack bring-up, readiness waits, pnpm test:e2e). Delivers criteria 3 + 4. Four CI infrastructure fixes (no Phase 7 files modified). |
| Plan | Contribution |
| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 08-01 | Runner probe — confirmed ubuntu-latest label, Docker executor (/.dockerenv), GITHUB_SHA availability, Playwright deps install cleanly |
| 08-02 | ci.yml: fast-checks job (lint, tsc, PWA vitest) + api job (MariaDB service, mysql2 readiness, drizzle-kit migrate, integration tests). Delivers criteria 1 + 2. |
| 08-03 | ci.yml: harness job (dev-stack bring-up, readiness waits, pnpm test:e2e). Delivers criteria 3 + 4. Four CI infrastructure fixes (no Phase 7 files modified). |
| 08-04 | ci.yml: publish job (docker login --password-stdin, build --target production from repo root, dual push :latest + :v1.1-<sha>, docker logout). Delivers criteria 5 + 6. |
## Key Fixes Applied During Phase 8 (CI-side only, no harness changes)
@@ -40,10 +40,10 @@ Every PR to `main` runs a full regression that gates the merge — lint, typeche
## Requirements Delivered
| REQ-ID | Description | Status |
|--------|-------------|--------|
| CI-01 | PR regression gates merge (lint + typecheck + unit + API-integration + mobile harness) | **Complete** |
| CI-02 | Push to main builds + publishes API Docker image | **Complete** |
| REQ-ID | Description | Status |
| ------ | -------------------------------------------------------------------------------------- | ------------ |
| CI-01 | PR regression gates merge (lint + typecheck + unit + API-integration + mobile harness) | **Complete** |
| CI-02 | Push to main builds + publishes API Docker image | **Complete** |
## Phase 8 Outcome