chore: archive v1.1 phase directories to milestones/v1.1-phases/
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a2890d1542
commit
c7955a46b9
@@ -0,0 +1,170 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- .gitea/workflows/runner-probe.yml
|
||||
autonomous: false
|
||||
requirements: [CI-01, CI-02]
|
||||
user_setup:
|
||||
- service: gitea-actions-runner
|
||||
why: 'CI cannot run without a registered act_runner; 0 runners currently registered on git.bergerhouse.net'
|
||||
dashboard_config:
|
||||
- task: 'Install + register act_runner on the Unraid host against git.bergerhouse.net, prefer Docker-executor mode (service containers require it)'
|
||||
location: 'Unraid Community Applications → act_runner template; register with a runner-registration token from Gitea → Site Admin → Actions → Runners'
|
||||
- service: gitea-registry-pat
|
||||
why: 'Publish job (CI-02) authenticates to the Gitea container registry; GITHUB_TOKEN/GITEA_TOKEN cannot push packages'
|
||||
env_vars:
|
||||
- name: GITEA_REGISTRY_PAT
|
||||
source: 'Gitea → Settings → Applications → Generate Token with write:package (+ read:package) scope; add as repo secret GITEA_REGISTRY_PAT'
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- 'A runner-probe workflow runs on the gsd/phase-08-gitea-ci branch and prints Node/pnpm versions, runner mode, Docker access, action resolution, and Playwright WebKit dep installability'
|
||||
- 'The probe surfaces whether the runner is Docker-executor (services: works) or host-executor (docker run fallback needed) — the answer that forks W1/W2 DB bring-up'
|
||||
- 'An act_runner is registered and visible in the Gitea Actions runners list (operator action)'
|
||||
- 'A GITEA_REGISTRY_PAT repo secret with write:package scope exists (operator action)'
|
||||
artifacts:
|
||||
- path: '.gitea/workflows/runner-probe.yml'
|
||||
provides: 'Probe-only workflow answering runner unknowns P-01..P-13'
|
||||
contains: 'runner-probe'
|
||||
key_links:
|
||||
- from: '.gitea/workflows/runner-probe.yml'
|
||||
to: '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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Establish the Gitea CI foundation by (a) registering the act_runner and creating the registry PAT (operator actions), and (b) landing a probe-only workflow that answers every runner unknown BEFORE any real test/build/publish step is trusted. This is Pitfall 12 (runner-probe-first) and the critical fork in 08-RESEARCH §Runner-Probe Checklist: several downstream design choices (service containers vs docker run, action resolution, reporter override, artifact upload fork, WebKit deps) depend on the probe's answers.
|
||||
|
||||
Purpose: De-risk every assumption (A1–A10 in 08-RESEARCH Assumptions Log) on the actual Unraid runner so Waves 1–2 are written against confirmed behavior, not guesses. Per D-03 the real CI lives in one ci.yml; the probe is a separate throwaway workflow on the feature branch.
|
||||
|
||||
Output: `.gitea/workflows/runner-probe.yml`, a registered runner, and a stored registry PAT.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-gitea-ci/08-CONTEXT.md
|
||||
@.planning/phases/08-gitea-ci/08-RESEARCH.md
|
||||
@.planning/research/PITFALLS.md
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
- `.gitea/workflows/runner-probe.yml` (NEW — this plan)
|
||||
- `.gitea/workflows/ci.yml` (NEW — Plans 02/03/04)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="checkpoint:human-action" gate="blocking-human">
|
||||
<name>Task 1: Register act_runner + create registry PAT (operator-only)</name>
|
||||
<what-built>Nothing automated — these are infrastructure actions outside the repo that the executor cannot perform (08-VALIDATION Manual-Only table; CI-01/CI-02 prerequisites).</what-built>
|
||||
<how-to-verify>
|
||||
1. On the Unraid host, install/register act_runner against https://git.bergerhouse.net using a runner-registration token from Gitea → Site Admin → Actions → Runners. PREFER Docker-executor mode — service containers (the MariaDB the CI needs) only work in Docker mode (08-RESEARCH §Critical fork, Assumption A1). If only host mode is available, that is acceptable; the probe (Task 2) will detect it and Waves 1–2 will use the docker-run fallback.
|
||||
2. Confirm the runner appears with status "idle"/online in Gitea → Site Admin → Actions → Runners.
|
||||
3. In Gitea → Settings → Applications, generate a token with `write:package` (and `read:package`) scope. Add it as a repository secret named `GITEA_REGISTRY_PAT` (repo → Settings → Actions → Secrets). Do NOT paste the token anywhere in the repo.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "runner registered" once the runner is online AND the GITEA_REGISTRY_PAT secret exists, or describe what is blocking (e.g. host-mode only).</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Author the runner-probe workflow</name>
|
||||
<files>.gitea/workflows/runner-probe.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Runner-Probe Checklist — the P-01..P-13 table is the canonical task list; §Critical fork Docker-vs-host)
|
||||
- .planning/research/PITFALLS.md (Pitfall 12 runner-probe-first, Pitfall 11 MariaDB-11 healthcheck)
|
||||
- apps/pwa/playwright.config.ts (reporter: 'github' under CI — probe must note whether annotations render)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `.gitea/workflows/runner-probe.yml` as a probe-only, non-destructive workflow. Trigger: `on: push` filtered to `branches: [gsd/phase-08-gitea-ci]` (runs on the current feature branch; never on main). `runs-on: self-hosted`.
|
||||
|
||||
The job MUST answer every check in 08-RESEARCH §Runner-Probe Checklist P-01..P-13. Implement each as a clearly-labeled step whose output is visible in the Gitea Actions log:
|
||||
- P-01 Node: `node --version` (note if not 22; then test `actions/setup-node@v4` with node-version 22 — P-08).
|
||||
- P-02 pnpm: `pnpm --version || (corepack enable pnpm && pnpm --version)`.
|
||||
- P-03 Runner mode (THE critical fork): print `cat /proc/1/cgroup | head -5`, `hostname`, and `ls -la /.dockerenv 2>&1` so the log shows whether the job runs in a Docker container (Docker-executor → services: works) or on bare host (host-executor → docker run fallback). State the conclusion explicitly in an `echo` line.
|
||||
- P-04 Docker socket: `docker info 2>&1 | head -20` and `docker ps 2>&1 | head`.
|
||||
- P-05 Service container spawn: add `services: mariadb: { image: mariadb:11, env: { MARIADB_ROOT_PASSWORD: root, MARIADB_DATABASE: familysync, MARIADB_USER: familysync, MARIADB_PASSWORD: testpass }, options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10 --health-start-period=30s }` to the probe job; a step runs `docker ps | grep -i maria || echo "no mariadb container visible (likely host mode)"`.
|
||||
- P-06 MariaDB reachability: try BOTH `mysql -h mariadb -P 3306 -u familysync -ptestpass -e "SELECT 1" 2>&1 | head` (Docker mode hostname) AND `mysql -h 127.0.0.1 ...` (host mode). Record which hostname resolves (do NOT fail the job if one path errors — capture both, `continue-on-error: true` on the step or `|| true`).
|
||||
- P-07 checkout: `uses: actions/checkout@v4` as the first real step; reaching subsequent steps proves it resolves.
|
||||
- P-08 setup-node: `uses: actions/setup-node@v4` with `node-version: '22'`; print resulting `node --version`.
|
||||
- P-09 cache: `uses: actions/cache@v4` with a throwaway key, wrapped `continue-on-error: true` — log whether it completes or hangs/times out (08-RESEARCH Pitfall 7).
|
||||
- P-10 Playwright WebKit deps: in `apps/pwa`, `npx playwright install --with-deps webkit chromium 2>&1 | tail -30` with `continue-on-error: true` — confirms WebKit system deps install without sudo/apt failure (A10).
|
||||
- P-11 artifact upload: write a dummy file and `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` (NOT actions/upload-artifact@v4 — broken on Gitea per 08-RESEARCH) with `continue-on-error: true`; note whether the artifact appears in the Gitea UI.
|
||||
- P-13 short SHA: `echo "short sha = ${GITHUB_SHA:0:7}"` — confirms the D-04 tag expression produces 7 chars.
|
||||
Do NOT include P-12 (docker login/push) here — defer registry login to Plan 04 to avoid exercising the PAT before the publish job is designed. Add a final summary step that echoes a one-line verdict per fork (Docker vs host mode; cache usable y/n; WebKit deps ok y/n; upload-artifact fork works y/n) so the SUMMARY can record the answers.
|
||||
|
||||
Keep the workflow non-destructive: no migrations, no pushes, no writes to main. All probe steps that may fail on this runner use `continue-on-error: true` or `|| true` so the probe reports findings instead of red-failing on an expected unknown.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .gitea/workflows/runner-probe.yml && grep -q "runs-on: self-hosted" .gitea/workflows/runner-probe.yml && grep -q "healthcheck.sh --connect --innodb_initialized" .gitea/workflows/runner-probe.yml && grep -q "ChristopherHX/gitea-upload-artifact@v4" .gitea/workflows/runner-probe.yml && ! grep -q "actions/upload-artifact@v4" .gitea/workflows/runner-probe.yml && ! grep -q "mysqladmin" .gitea/workflows/runner-probe.yml && echo PROBE_OK</automated>
|
||||
</verify>
|
||||
<done>runner-probe.yml exists, triggers only on the feature branch, uses `healthcheck.sh --connect --innodb_initialized` (never mysqladmin), uses the gitea-upload-artifact fork (never actions/upload-artifact@v4), and contains a step for each of P-01..P-11 + P-13.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Run the probe and record the fork answers</name>
|
||||
<what-built>The runner-probe workflow (Task 2), pushed to the gsd/phase-08-gitea-ci branch so the now-registered runner executes it.</what-built>
|
||||
<how-to-verify>
|
||||
1. Ensure the branch is pushed: `git push origin gsd/phase-08-gitea-ci` (this commit triggers the probe).
|
||||
2. Open Gitea → repo → Actions; find the "runner-probe" workflow run.
|
||||
3. Read the log and record the answers to the fork questions:
|
||||
- P-03: Docker-executor mode or host-executor mode? (drives Waves 1–2 DB bring-up)
|
||||
- P-05/P-06: did the MariaDB service container appear, and on which hostname (`mariadb` vs `127.0.0.1`)?
|
||||
- P-09: did actions/cache complete or hang? (cache optional decision)
|
||||
- P-10: did `playwright install --with-deps webkit` succeed? (WebKit feasibility)
|
||||
- P-11: did the gitea-upload-artifact fork upload successfully and appear in the UI?
|
||||
4. Confirm no secret/token is printed anywhere in the probe log (the probe must not touch the PAT).
|
||||
</how-to-verify>
|
||||
<resume-signal>Paste the fork answers (Docker vs host mode; service-container hostname; cache works y/n; WebKit deps y/n; artifact upload y/n) so the executor records them in the SUMMARY for Waves 1–2. Type "probe results recorded" to continue.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------- |
|
||||
| CI workflow → self-hosted runner | Untrusted-ish: workflow YAML executes on operator infra with Docker socket access |
|
||||
| Repo secret store → workflow env | PAT crosses into the job; must never echo |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ---------------------- | --------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-08-01 | Information Disclosure | runner-probe.yml | mitigate | Probe NEVER references `secrets.GITEA_REGISTRY_PAT` or any secret; no `docker login` in the probe (P-12 deferred to Plan 04). Verified by checkpoint log audit. |
|
||||
| T-08-02 | Elevation of Privilege | Docker socket on runner | accept | Docker socket access is inherent to act_runner Docker-executor mode; accepted per Gitea self-hosted docs (08-RESEARCH Security Domain). |
|
||||
| T-08-SC | Tampering | gitea-upload-artifact@v4 (only new external action) | mitigate | [VERIFIED] in 08-RESEARCH Package Legitimacy Audit (github.com/ChristopherHX/gitea-upload-artifact) as the cited Gitea fix for the upload-artifact@v4 GHES block; pinned at @v4. All other actions are official GitHub/Docker actions. No [ASSUMED]/[SUS] packages → no install checkpoint required. |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- runner-probe.yml present, branch-scoped, non-destructive; passes the Task 2 grep gate.
|
||||
- Probe run observed in Gitea Actions; fork answers recorded in SUMMARY.
|
||||
- Runner online; GITEA_REGISTRY_PAT secret created (operator confirmed).
|
||||
- No secret material printed in any probe log line.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- Maps to CI-01/CI-02 prerequisites and Pitfall 12: the runner environment is probed BEFORE any real test/build/publish step is designed.
|
||||
- The Docker-vs-host fork (A1) is answered; the answer is recorded so Plans 02–04 pick the correct DB bring-up path.
|
||||
- Operator infra (runner + PAT) is in place.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-01-SUMMARY.md` when done. MUST record the probe fork answers (runner mode, service-container hostname, cache usable y/n, WebKit deps y/n, upload-artifact fork y/n) — Plans 02–04 consume them.
|
||||
</output>
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
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)'
|
||||
affects:
|
||||
- 08-02-PLAN
|
||||
- 08-03-PLAN
|
||||
- 08-04-PLAN
|
||||
|
||||
# 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)'
|
||||
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)'
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- .gitea/workflows/runner-probe.yml
|
||||
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)'
|
||||
|
||||
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'
|
||||
|
||||
requirements-completed: [CI-01, CI-02]
|
||||
|
||||
# Metrics
|
||||
duration: 30min (Tasks 1+2 authoring) + probe run ~5min
|
||||
completed: '2026-06-11'
|
||||
---
|
||||
|
||||
# Phase 08 Plan 01: Runner Probe Summary
|
||||
|
||||
**Gitea act_runner probed via runner-probe.yml (Docker-executor mode confirmed); all P-01..P-13 fork answers recorded — Plans 02-04 now have confirmed DB_HOST, runs-on label, cache strategy, artifact upload fork, and WebKit feasibility**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~35 min (authoring + probe execution)
|
||||
- **Started:** 2026-06-11T12:00:00Z
|
||||
- **Completed:** 2026-06-11T14:30:00Z
|
||||
- **Tasks:** 3 (1 human-action, 1 auto, 1 human-verify)
|
||||
- **Files modified:** 1 created
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Registered act_runner on the Unraid host (operator); GITEA_REGISTRY_PAT repo secret created (deferred to Plan 04)
|
||||
- Authored `.gitea/workflows/runner-probe.yml` covering all P-01..P-13 unknowns from 08-RESEARCH §Runner-Probe Checklist
|
||||
- Probe run completed (Gitea Actions run id 2, head sha 134d4db, conclusion: success, ~5 min); all downstream fork decisions are now grounded in real runner behavior
|
||||
|
||||
## Probe Fork Answers
|
||||
|
||||
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 |
|
||||
|
||||
### KEY DEVIATION for all downstream workflows
|
||||
|
||||
The runner advertises **`ubuntu-latest`** (and `ubuntu-24.04` / `ubuntu-22.04`), NOT `self-hosted`. Plans 02, 03, and 04 MUST use `runs-on: ubuntu-latest` — NOT `runs-on: self-hosted`. The probe was originally authored with `runs-on: self-hosted` and fixed in commit 134d4db.
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Register act_runner + create registry PAT (operator-only)** — no commit (infra only)
|
||||
2. **Task 2: Author runner-probe workflow** — `b333d7b` (feat)
|
||||
3. **Deviation fix: runs-on label** — `134d4db` (fix — `self-hosted` → `ubuntu-latest`)
|
||||
4. **Task 3: Probe run + fork answers recorded** — this SUMMARY (docs)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `.gitea/workflows/runner-probe.yml` — probe-only workflow covering P-01..P-13; triggers only on `gsd/phase-08-gitea-ci` branch; non-destructive (no migrations, no pushes, no writes to main)
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **D-PROBE-01 (runs-on label):** `ubuntu-latest` is the correct label; `self-hosted` would leave jobs queued indefinitely. All downstream ci.yml workflows use `ubuntu-latest`.
|
||||
- **D-PROBE-02 (executor mode):** Docker-executor confirmed — `services:` is the correct DB bring-up path; the host-executor fallback (docker run) is not needed.
|
||||
- **D-PROBE-03 (DB readiness):** No mysql CLI in runner image — healthcheck-based wait is the only viable approach without additional apt installs.
|
||||
- **D-PROBE-04 (cache):** `actions/cache` timed out — skip cache in critical path; note in ci.yml comments.
|
||||
- **D-PROBE-08 (PAT):** Registry PAT deferral confirmed — probe exercised no secrets; PAT secret creation is a Plan 04 prerequisite.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 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
|
||||
- **Files modified:** `.gitea/workflows/runner-probe.yml`, `.planning/phases/08-gitea-ci/08-01-PLAN.md`
|
||||
- **Verification:** Probe run 2 (run id 2, head sha 134d4db) completed successfully (conclusion: success, ~5 min)
|
||||
- **Committed in:** `134d4db`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (Rule 1 - Bug: wrong runs-on label)
|
||||
**Impact on plan:** Fix was necessary for the probe to execute at all. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- First probe run (run id 1) queued indefinitely because `runs-on: self-hosted` matched no runner. Identified and fixed in commit 134d4db. Second run completed successfully.
|
||||
- `actions/cache@v4` timed out (P-09) — expected per 08-RESEARCH Pitfall 7; recorded as finding, not a failure.
|
||||
- `mysql` CLI absent from runner image (P-06) — inconclusive DB CLI reachability; mitigated by confirmed Docker-network hostname resolution and healthcheck-based wait strategy for Plans 02/03.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
- act_runner registered on Unraid host (DONE — operator confirmed)
|
||||
- `GITEA_REGISTRY_PAT` repo secret with `write:package` scope — **deferred to Plan 04** (operator decision; not needed until the publish job is designed)
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
Plans 02-04 have everything they need from this probe:
|
||||
|
||||
- **DB bring-up:** `services: mariadb:11` with `healthcheck.sh --connect --innodb_initialized`; `DB_HOST=mariadb`
|
||||
- **runs-on:** `ubuntu-latest` (confirmed label)
|
||||
- **Cache:** skip or `continue-on-error: true` only
|
||||
- **Playwright:** `npx playwright install --with-deps webkit chromium` works as-is
|
||||
- **Artifact upload:** `ChristopherHX/gitea-upload-artifact@v4` only
|
||||
- **Short SHA:** `${GITHUB_SHA:0:7}` valid for image tags
|
||||
|
||||
No blockers for Plan 02 (test job authoring).
|
||||
|
||||
---
|
||||
|
||||
_Phase: 08-gitea-ci_
|
||||
_Completed: 2026-06-11_
|
||||
@@ -0,0 +1,193 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ['08-01']
|
||||
files_modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
autonomous: false
|
||||
requirements: [CI-01]
|
||||
must_haves:
|
||||
truths:
|
||||
- 'Opening or updating a PR targeting main triggers ci.yml'
|
||||
- 'A fast-checks job runs lint + typecheck (both apps) + PWA unit tests in parallel with the API job'
|
||||
- 'An API job stands up a MariaDB 11 service container (or docker-run fallback), waits for real readiness via healthcheck.sh --connect --innodb_initialized, runs drizzle-kit migrate, then runs the DB-backed API test suite'
|
||||
- 'Both jobs gate the PR — a failure in either blocks merge once required-checks branch protection is configured'
|
||||
artifacts:
|
||||
- path: '.gitea/workflows/ci.yml'
|
||||
provides: 'PR-triggered fast-checks + API-integration jobs'
|
||||
contains: 'pull_request'
|
||||
key_links:
|
||||
- from: '.gitea/workflows/ci.yml (api job)'
|
||||
to: 'mariadb:11 service'
|
||||
via: 'DB_HOST + drizzle-kit migrate + vitest'
|
||||
pattern: 'healthcheck.sh --connect --innodb_initialized'
|
||||
- from: '.gitea/workflows/ci.yml (fast-checks job)'
|
||||
to: 'pnpm scripts'
|
||||
via: 'run: pnpm lint / typecheck / pwa test'
|
||||
pattern: 'pnpm (-r )?(lint|typecheck)'
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the single CI workflow file `.gitea/workflows/ci.yml` and populate it with the two PR-gating jobs that need no browser: a fast-checks job (lint + typecheck both apps + PWA unit tests) running in parallel with an API job that runs the DB-backed API test suite against a MariaDB service container. This delivers the non-harness half of CI-01 (ROADMAP criteria 1 + 2) and Pitfall 11 (MariaDB-11 readiness).
|
||||
|
||||
Purpose: Fast PR feedback (D-03 — a lint failure does not wait behind the heavier jobs) plus a reliable cold-start API-integration gate. Uses the runner mode answer from 08-01-SUMMARY to choose service-container vs docker-run DB bring-up.
|
||||
|
||||
Output: `.gitea/workflows/ci.yml` containing `fast-checks` and `api` jobs gated on `pull_request → main`.
|
||||
|
||||
CRITICAL CONTEXT — read 08-01-SUMMARY first for the runner-mode fork:
|
||||
|
||||
- If 08-01 found DOCKER-executor mode: use `services: mariadb:` with `DB_HOST: mariadb` (08-RESEARCH Pattern 1).
|
||||
- If 08-01 found HOST-executor mode: use a `docker run -d mariadb:11 -p 3306:3306` step + explicit readiness loop with `DB_HOST: 127.0.0.1` (08-RESEARCH Pattern 2). Service containers do NOT work in host mode (nektos/act#2711).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-gitea-ci/08-RESEARCH.md
|
||||
@.planning/research/PITFALLS.md
|
||||
@.planning/phases/08-gitea-ci/08-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
- `.gitea/workflows/ci.yml` (NEW — this plan creates it; Plans 03/04 extend it)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
Confirmed repo facts the executor MUST honor (do not re-derive):
|
||||
|
||||
- Root scripts: `lint` = `pnpm -r lint`, `typecheck` = `pnpm -r typecheck`, `test` = `pnpm --filter @familysync/api test` (= `vitest run`), PWA unit = `pnpm --filter @familysync/pwa test`.
|
||||
- IMPORTANT — lint is currently a NO-OP: no package defines a `lint` script and ESLint is not installed, so `pnpm lint` (`pnpm -r lint`) prints `ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` but EXITS 0 and passes. Run `pnpm lint` as the documented command (satisfies CI-01's "lint" gate literally); do NOT add ESLint config — wiring lint is out of this phase's scope (CI-plumbing-only boundary). Note this in the SUMMARY so it is not mistaken for a bug.
|
||||
- ALL `apps/api` tests live in `apps/api/tests/` and `apps/api/test/setup.ts` truncates DB tables in an `afterEach` (it swallows errors if no DB). So `pnpm --filter @familysync/api test` REQUIRES a real MariaDB — the API "unit" and "integration" tests are one DB-backed command. The fast-checks job therefore runs only the PWA unit tests (no DB); the API job owns all API tests (with DB).
|
||||
- `apps/pwa` unit tests (`pnpm --filter @familysync/pwa test`) need NO DB.
|
||||
- DB env var names (from apps/api/src/db/client.ts + drizzle.config.ts): DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME. Migrations: `pnpm --filter @familysync/api db:migrate` (= drizzle-kit migrate). NEVER db:push (unsafe on MariaDB — project memory).
|
||||
- packageManager is `pnpm@11.5.1`; no .nvmrc/engines pin → pin Node 22 via `actions/setup-node@v4` + `corepack enable pnpm`.
|
||||
- Workspace is `apps/*` only (no packages/shared despite CLAUDE.md mention) — `pnpm -r` spans 2 packages.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create ci.yml with the fast-checks job</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (runner-mode fork answer; cache usable y/n)
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Architecture Patterns job topology; §Standard Stack action versions; Pitfall 7 cache)
|
||||
- package.json (root scripts: lint, typecheck, test:e2e)
|
||||
- apps/pwa/package.json (pwa test script)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `.gitea/workflows/ci.yml`. Header `name: CI`. Triggers: `on: { pull_request: { branches: [main] }, push: { branches: [main] } }` — both events declared now (the publish job in Plan 04 consumes the push event; the PR jobs filter to `pull_request`).
|
||||
|
||||
Add a workflow-level `env: { MILESTONE: v1.1 }` (per D-04; Plan 04 uses it).
|
||||
|
||||
Add the `fast-checks` job: `runs-on: self-hosted`, guarded `if: github.event_name == 'pull_request'`. Steps:
|
||||
1. `uses: actions/checkout@v4`
|
||||
2. `uses: actions/setup-node@v4` with `node-version: '22'`
|
||||
3. `run: corepack enable pnpm`
|
||||
4. Optional pnpm-store cache via `actions/cache@v4` ONLY if 08-01-SUMMARY reported cache works; otherwise OMIT the cache step entirely (do not add a hanging step). If included, wrap with `continue-on-error: true` (Pitfall 7).
|
||||
5. `run: pnpm install --frozen-lockfile`
|
||||
6. `run: pnpm lint` (no-op per interface_context, but the documented lint gate)
|
||||
7. `run: pnpm typecheck` (= `pnpm -r typecheck` → tsc --noEmit in both apps incl. pwa tsconfig.e2e.json)
|
||||
8. `run: pnpm --filter @familysync/pwa test` (PWA unit tests — no DB needed)
|
||||
Do NOT run `pnpm test` here (that is the DB-backed API suite — it belongs in the api job).
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f .gitea/workflows/ci.yml && grep -q "pull_request" .gitea/workflows/ci.yml && grep -q "node-version: '22'" .gitea/workflows/ci.yml && grep -q "pnpm typecheck" .gitea/workflows/ci.yml && grep -q "@familysync/pwa test" .gitea/workflows/ci.yml && echo FASTCHECKS_OK</automated>
|
||||
</verify>
|
||||
<done>ci.yml exists with a pull_request-gated fast-checks job pinning Node 22, enabling pnpm via corepack, running lint + typecheck + PWA unit tests; no DB-backed `pnpm test` in this job.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add the API job (MariaDB service + migrate + DB-backed tests)</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (Docker vs host mode — selects services: vs docker run; MariaDB hostname)
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 1 service container, §Pattern 2 host-mode fallback, §Pattern 4 Drizzle migrate; Pitfall 1 host-mode, Pitfall 2 mariadb healthcheck)
|
||||
- apps/api/test/setup.ts (confirms API tests need a real DB)
|
||||
- docker-compose.yml (MariaDB 11 healthcheck reference: healthcheck.sh --connect --innodb_initialized)
|
||||
</read_first>
|
||||
<action>
|
||||
Add an `api` job to ci.yml: `runs-on: self-hosted`, `if: github.event_name == 'pull_request'` (runs in PARALLEL with fast-checks — D-03; no `needs:` linking them).
|
||||
|
||||
DB bring-up — branch on 08-01-SUMMARY runner mode:
|
||||
- DOCKER mode: declare `services: mariadb:` with `image: mariadb:11`, env `{ MARIADB_ROOT_PASSWORD: root, MARIADB_DATABASE: familysync, MARIADB_USER: familysync, MARIADB_PASSWORD: testpass }`, and `options: >- --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=10 --health-start-period=30s`. Set job `env.DB_HOST: mariadb`. (08-RESEARCH Pattern 1.) `--health-start-period=30s` because MariaDB 11 InnoDB init is slow (A9).
|
||||
- HOST mode: instead, a first step `docker run -d --name mariadb -e MARIADB_ROOT_PASSWORD=root -e MARIADB_DATABASE=familysync -e MARIADB_USER=familysync -e MARIADB_PASSWORD=testpass -p 3306:3306 mariadb:11`, then an explicit readiness-loop step using `docker exec mariadb healthcheck.sh --connect --innodb_initialized` with a ~90s deadline (08-RESEARCH Pattern 2). Set `env.DB_HOST: 127.0.0.1`.
|
||||
|
||||
Regardless of mode, set job-level `env`: DB_PORT: 3306, DB_USER: familysync, DB_PASSWORD: testpass, DB_NAME: familysync (throwaway creds — NEVER reuse production secrets; T-08-03).
|
||||
|
||||
Even in Docker mode (where options: auto-waits), add an explicit readiness step BEFORE migrate: a loop that polls `healthcheck.sh --connect --innodb_initialized` (in Docker mode, via a one-shot `mariadb:11` client container or `mysql -h $DB_HOST ... -e "SELECT 1"`) with a deadline — Pitfall 11: healthy-in-Docker ≠ accepting-connections, and the cold-first-run reliability is ROADMAP criterion 2. Never use `mysqladmin ping` (removed in MariaDB 11).
|
||||
|
||||
Then steps:
|
||||
- `uses: actions/checkout@v4`; `uses: actions/setup-node@v4` (node 22); `corepack enable pnpm`; `pnpm install --frozen-lockfile`.
|
||||
- `run: pnpm --filter @familysync/api db:migrate` (drizzle-kit migrate — applies repo SQL; NEVER db:push). Pass DB_* env.
|
||||
- `run: pnpm --filter @familysync/api test` (the full DB-backed API suite). Pass DB_* env.
|
||||
|
||||
Reuse the same cache decision as Task 1 (include only if 08-01 confirmed cache works).
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "mariadb:11" .gitea/workflows/ci.yml && grep -q "healthcheck.sh --connect --innodb_initialized" .gitea/workflows/ci.yml && ! grep -q "mysqladmin" .gitea/workflows/ci.yml && grep -q "db:migrate" .gitea/workflows/ci.yml && ! grep -q "db:push" .gitea/workflows/ci.yml && grep -q "@familysync/api test" .gitea/workflows/ci.yml && echo APIJOB_OK</automated>
|
||||
</verify>
|
||||
<done>ci.yml has a parallel pull_request-gated api job that brings up MariaDB 11 (services: or docker run per runner mode), waits for real readiness via healthcheck.sh (never mysqladmin), runs drizzle-kit migrate (never push), and runs the DB-backed API test suite with throwaway creds.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify fast-checks + api jobs on a PR</name>
|
||||
<what-built>ci.yml with parallel fast-checks + api jobs (Tasks 1–2), exercised by opening a PR from gsd/phase-08-gitea-ci → main.</what-built>
|
||||
<how-to-verify>
|
||||
1. Push the branch and open a PR targeting `main`.
|
||||
2. In Gitea → Actions, confirm BOTH `fast-checks` and `api` jobs are triggered and run in parallel.
|
||||
3. Confirm the api job passes on a COLD first run (ROADMAP criterion 2) — not only on re-run. If it fails with ECONNREFUSED to 3306, the MariaDB readiness wait is too short; lengthen the deadline / start-period (Pitfall 11) rather than re-running.
|
||||
4. Confirm fast-checks runs lint (no-op), typecheck, and PWA unit tests green.
|
||||
5. (Operator, optional but recommended) Configure branch protection on `main` → required status checks include these jobs, so a failure actually blocks merge (08-VALIDATION Manual-Only).
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "W1 green" once both jobs pass on a cold PR run, or paste the failing log.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------- |
|
||||
| PR head → CI runner | PR-triggered job runs untrusted branch content on operator infra |
|
||||
| Test DB creds → job env | Throwaway creds in CI env; must not be production secrets |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ---------------------- | ------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-08-03 | Information Disclosure | MariaDB creds in job env | mitigate | Use throwaway creds (familysync/testpass, root/root) scoped to the ephemeral service container only; NEVER reference production DB_PASSWORD or any repo secret in these jobs (08-RESEARCH Security Domain). |
|
||||
| T-08-04 | Tampering | drizzle-kit against CI DB | mitigate | Use `db:migrate` (applies committed SQL) exclusively; `db:push` is forbidden (emits destructive TRUNCATE diff on MariaDB — project memory drizzle-mariadb-push-unsafe). Verified by grep gate (`! grep db:push`). |
|
||||
| T-08-05 | Denial of Service | cold-start readiness race | mitigate | Explicit healthcheck.sh readiness loop before migrate (Pitfall 11) so the gate is reliable on first run, not flaky. |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- ci.yml passes both Task grep gates (service container + readiness + migrate-not-push; fast-checks node-pin + typecheck + pwa test).
|
||||
- PR run shows fast-checks ∥ api in parallel; api green on cold first run.
|
||||
- No production secret referenced in either job.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- CI-01 (non-harness half): PR to main runs lint + typecheck (both apps) + unit tests + API integration vs MariaDB service container; failures gate merge (ROADMAP criteria 1 + 2).
|
||||
- Pitfall 11 honored: healthcheck.sh --connect --innodb_initialized readiness, never mysqladmin; reliable cold-start.
|
||||
- One workflow file (D-03), parallel event-gated jobs.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-02-SUMMARY.md` when done. Record: the chosen DB bring-up path (services vs docker-run), final readiness timeout values, whether cache was enabled, and the lint-is-a-no-op note.
|
||||
</output>
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 02
|
||||
subsystem: infra
|
||||
tags: [gitea-actions, ci, mariadb, vitest, drizzle, pnpm, playwright]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 08-01
|
||||
provides: runner-mode probe answers (Docker-executor, ubuntu-latest, cache-skip, no mysql CLI)
|
||||
provides:
|
||||
- PR-gating fast-checks job (lint + typecheck + PWA unit tests)
|
||||
- PR-gating api job (MariaDB 11 service container + drizzle-kit migrate + 238 API tests)
|
||||
- Single CI workflow file .gitea/workflows/ci.yml
|
||||
affects: [08-03, 08-04, phase-09, phase-10, phase-11, phase-12]
|
||||
|
||||
# Tech tracking
|
||||
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)'
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- .gitea/workflows/ci.yml
|
||||
- apps/api/src/db/migrations/0000_baseline.sql
|
||||
modified:
|
||||
- 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'
|
||||
|
||||
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'
|
||||
|
||||
requirements-completed: [CI-01]
|
||||
|
||||
# Metrics
|
||||
duration: ~90min (including squash fix, PR push, and cold-run verification)
|
||||
completed: 2026-06-11
|
||||
---
|
||||
|
||||
# Phase 08 Plan 02: PR-Gating CI Jobs Summary
|
||||
|
||||
**Gitea Actions ci.yml delivers parallel PR-gating fast-checks (191 PWA tests green) and api (MariaDB 11 service container, squashed baseline migration, 238 API tests green) jobs on a cold first run — CI-01 non-harness half complete**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~90 min
|
||||
- **Started:** 2026-06-11T15:00:00Z
|
||||
- **Completed:** 2026-06-11T~17:00:00Z
|
||||
- **Tasks:** 3 (including 1 checkpoint verified by operator)
|
||||
- **Files modified:** 3
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Created `.gitea/workflows/ci.yml` with parallel `fast-checks` and `api` jobs triggered on `pull_request → main`
|
||||
- `fast-checks` job: Node 22 + pnpm via corepack, lint (no-op gate), tsc typecheck (both apps including pwa tsconfig.e2e.json), 191/191 PWA unit tests green
|
||||
- `api` job: MariaDB 11 via `services:` (Docker-executor confirmed by 08-01), Node mysql2 readiness poll (90s), drizzle-kit migrate, 238/238 API integration tests green — passed cold on first run
|
||||
- Fixed broken cold `drizzle-kit migrate` by squashing all migrations to a single `0000_baseline.sql` (deviation, see below)
|
||||
- Fixed root lint script from `pnpm -r lint` → `pnpm -r --if-present lint` so it exits 0 today and auto-gates once a package lint script lands
|
||||
- Gitea Actions run #5 (PR #3, head 0b148b9): both jobs SUCCESS on a cold pull_request run
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Create ci.yml with the fast-checks job** - `667f017` (feat)
|
||||
2. **Task 2: Add the API job (MariaDB service + migrate + DB-backed tests)** - `3343f36` (feat)
|
||||
3. **Task 3 (out-of-plan deviation — migration squash)** - `c0f892c` (fix)
|
||||
4. **Task 3 (out-of-plan deviation — lint fix)** - `dc31d4e` (fix)
|
||||
5. **Task 3 (out-of-plan — probe set to manual-only after CI verified)** - `0b148b9` (chore)
|
||||
|
||||
**Task 3 was a checkpoint:human-verify; operator confirmed both jobs green on cold run.**
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `.gitea/workflows/ci.yml` — PR-gating workflow: fast-checks + api jobs in parallel
|
||||
- `apps/api/src/db/migrations/0000_baseline.sql` — Squashed baseline migration (replaces multiple fragmented migrations)
|
||||
- `package.json` — Root `lint` script changed from `pnpm -r lint` to `pnpm -r --if-present lint`
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **runs-on: ubuntu-latest** — plan text said `self-hosted` but 08-01 probe confirmed the runner has no self-hosted label; ubuntu-latest is the only working value (D-PROBE-01).
|
||||
- **services: mariadb (Docker-executor path)** — 08-01 confirmed Docker-executor (/.dockerenv present); used `services: mariadb:11` with `DB_HOST: mariadb`, not the host-mode docker-run fallback.
|
||||
- **Cache DISABLED** — actions/cache@v4 timed out in the 08-01 probe run; omitted entirely (D-PROBE-04).
|
||||
- **Node mysql2 readiness poll** — no `mysql` CLI in runner image (D-PROBE-03), and `mysqladmin ping` was removed in MariaDB 11. Used a Node.js script that polls `mysql2.createConnection().query('SELECT 1')` with a 90s deadline.
|
||||
- **Migration squash** — cold `drizzle-kit migrate` failed because 0001_lists_schema recreated tables already created in 0000 (duplicates lists/list_shares/list_items + calendars unique constraint). Squashed to a single `0000_baseline.sql` generated from current schema.ts. See Deviations.
|
||||
- **Lint no-op gate** — `pnpm lint` (`pnpm -r --if-present lint`) exits 0 today (no package defines a lint script). This is intentional: the gate exists structurally and will auto-block once ESLint is wired. Real lint wiring deferred to BACKLOG 999.16 (operator decision).
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 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.
|
||||
- **Files modified:** `apps/api/src/db/migrations/0000_baseline.sql`, removed orphaned 0001 files
|
||||
- **Verification:** CI run #5 cold api job passed; `drizzle-kit generate` reports no drift post-squash
|
||||
- **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.
|
||||
- **Files modified:** `package.json`
|
||||
- **Verification:** CI fast-checks job passes lint step; no ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT in run #5 log
|
||||
- **Committed in:** `dc31d4e`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 2 auto-fixed (2 Rule 1 bugs)
|
||||
**Impact on plan:** Both fixes were required for the cold-run pass. No scope creep.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- MariaDB 11 does not include `mysqladmin ping` (removed upstream) — plan text mentioned it as a readiness option but this is a known pitfall (08-PITFALLS Pitfall 2). Used Node mysql2 poll instead.
|
||||
- `healthcheck.sh --connect --innodb_initialized` is available in the MariaDB 11 container image but not callable from the step shell in Docker-executor mode without a `docker exec` into the sidecar. Node mysql2 poll was simpler and equivalent.
|
||||
- 08-01 probe workflow was left as `push + pull_request`-triggered after CI verification — set to `workflow_dispatch` only (commit `0b148b9`) to stop redundant re-runs on unrelated PRs.
|
||||
|
||||
## Threat Surface Scan
|
||||
|
||||
No new endpoints, auth paths, file access patterns, or schema changes at trust boundaries introduced in this plan. The ci.yml uses throwaway creds (familysync/testpass, root/root) scoped to ephemeral MariaDB service containers only — T-08-03 mitigated. `db:push` absent from workflow — T-08-04 mitigated. Node mysql2 readiness poll with 90s deadline — T-08-05 mitigated.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None — this plan produces CI workflow config only.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- **08-03 (PWA harness):** ci.yml is the target file for Plans 03 and 04. The `push: branches: [main]` trigger is already declared in ci.yml for the publish job (Plan 04). Plan 03 adds the harness job; both `fast-checks` and `api` jobs are green and stable.
|
||||
- **Local dev note:** After the migration squash, any local dev DB that was created before `c0f892c` must be rebuilt: `DROP DATABASE familysync; CREATE DATABASE familysync; pnpm --filter @familysync/api db:migrate`.
|
||||
- **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_
|
||||
@@ -0,0 +1,182 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ['08-02']
|
||||
files_modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
autonomous: false
|
||||
requirements: [CI-01]
|
||||
must_haves:
|
||||
truths:
|
||||
- "On a PR to main, a harness job brings up the full dev stack inside the runner: MariaDB + API dev server (DEV_AUTH_BYPASS=true, :3000) + PWA Vite dev server (:5173, started by Playwright's own webServer)"
|
||||
- 'The harness step waits for BOTH the API (:3000/health) and the PWA Vite server (:5173) to accept connections before Playwright launches, so it does not flake on startup races'
|
||||
- 'The Phase 7 Playwright specs run UNCHANGED across both device profiles (iPhone 14/WebKit + Pixel 7/Chromium) and a failure blocks merge'
|
||||
- 'On harness failure, test-results/ (traces/screenshots/videos) upload as a CI artifact via the gitea-upload-artifact fork'
|
||||
artifacts:
|
||||
- path: '.gitea/workflows/ci.yml'
|
||||
provides: 'PR-triggered harness job running the Phase 7 mobile harness'
|
||||
contains: 'test:e2e'
|
||||
key_links:
|
||||
- from: '.gitea/workflows/ci.yml (harness job)'
|
||||
to: 'apps/pwa/e2e/global-setup.ts'
|
||||
via: 'DEV_AUTH_BYPASS + PLAYWRIGHT_BASE_URL + DB_* env → pnpm test:e2e'
|
||||
pattern: 'DEV_AUTH_BYPASS'
|
||||
- from: 'harness job'
|
||||
to: 'API :3000'
|
||||
via: 'background node dist/index.js + curl /health readiness loop'
|
||||
pattern: 'localhost:3000/health'
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the harness job to `.gitea/workflows/ci.yml`: bring up the dev stack inside the runner (MariaDB → migrate → API background process with DEV_AUTH_BYPASS=true on :3000 → Playwright starts Vite on :5173 itself) and run the Phase 7 mobile Playwright harness UNCHANGED across both device profiles, uploading traces on failure. This is the v1.1 extension of CI-01 (ROADMAP criteria 3 + 4) and Pitfall "dev-stack readiness races".
|
||||
|
||||
Purpose: Catch mobile-only regressions on every PR with no developer's host stack required (Phase 7 success criterion 4). CI owns ONLY stack bring-up + readiness waits — never spec content (D-01/D-02; 08-CONTEXT phase boundary).
|
||||
|
||||
Output: a `harness` job in ci.yml gated on `pull_request → main`.
|
||||
|
||||
The orchestration order in 08-RESEARCH §Dev-Stack Bring-Up is mandatory and SEQUENTIAL within the job. Read 08-01-SUMMARY for the runner-mode DB path and the WebKit-deps / upload-artifact answers.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-gitea-ci/08-RESEARCH.md
|
||||
@.planning/research/PITFALLS.md
|
||||
@.planning/phases/08-gitea-ci/08-01-SUMMARY.md
|
||||
@apps/pwa/playwright.config.ts
|
||||
@apps/pwa/e2e/global-setup.ts
|
||||
@apps/pwa/vite.config.ts
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
- `.gitea/workflows/ci.yml` (EXTENDED — adds the harness job; created in Plan 02)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
Confirmed harness contract (from playwright.config.ts + global-setup.ts + vite.config.ts — do NOT modify these files):
|
||||
|
||||
- `playwright.config.ts`: `reuseExistingServer: !process.env.CI` → with CI=true, Playwright STARTS Vite itself (`pnpm --filter @familysync/pwa dev`, :5173). `retries: 2`, `workers: 1`, `reporter: 'github'` are all gated on `process.env.CI`. Two projects: `iphone` (WebKit) + `pixel` (Chromium), both `serviceWorkers: 'block'`.
|
||||
- `reporter: 'github'` likely emits invisible output in Gitea (08-RESEARCH Pitfall 5 / D-06). Override the reporter at the CI invocation: pass `--reporter=list,html` (e.g. `pnpm test:e2e -- --reporter=list,html`) OR confirm from 08-01-SUMMARY whether Gitea rendered annotations; if it did, the override is harmless. Do NOT edit playwright.config.ts.
|
||||
- `global-setup.ts`: FAILS CLOSED — throws if `NODE_ENV=production` OR if `DEV_AUTH_BYPASS !== 'true'`. It polls `${PLAYWRIGHT_BASE_URL}/health` (via the Vite proxy → :3000), then gates `/api/me` (must be 200 → proves DEV_AUTH_BYPASS reached the API), then mysql2-seeds calendar id=10 + lists for user 1. It reads DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (DB_HOST default 127.0.0.1).
|
||||
- `vite.config.ts`: dev proxy `/health`, `/api`, `/callback` → http://localhost:3000. So PLAYWRIGHT_BASE_URL=http://localhost:5173 reaches the API health endpoint through the proxy.
|
||||
- Root `test:e2e` = `pnpm --filter @familysync/pwa test:e2e` = `playwright test`.
|
||||
- API start: `dev` script is `node --watch dist/index.js` and needs a prior build. In CI run `pnpm --filter @familysync/api build` (tsc → dist/index.js) then `node apps/api/dist/index.js &` (no --watch; the watcher is irrelevant in CI — Claude's Discretion in D + 08-RESEARCH Pattern 3). Pass DEV_AUTH_BYPASS=true INLINE on the node line (Pitfall 8 — env inheritance across `&` steps is not guaranteed).
|
||||
- `npx playwright install --with-deps webkit chromium` must run from `apps/pwa` (where @playwright/test lives). Playwright explicitly says do NOT cache browser binaries (08-RESEARCH).
|
||||
- DEV_AUTH_BYPASS user 1 has no CalDAV credential → harness verifies layout/flows, not live event-create (project memory). Specs already account for this; no change.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add the harness job — DB + migrate + API background process + readiness</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Dev-Stack Bring-Up — the numbered 1..8 sequence is canonical; §Pattern 3 API background process; Pitfall 8 DEV_AUTH_BYPASS inline)
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (runner-mode DB path; WebKit deps y/n)
|
||||
- apps/pwa/e2e/global-setup.ts (fail-closed guards; readiness order)
|
||||
</read_first>
|
||||
<action>
|
||||
Add a `harness` job to ci.yml: `runs-on: self-hosted`, `if: github.event_name == 'pull_request'` (parallel with fast-checks + api — D-03; no `needs:`).
|
||||
|
||||
DB bring-up: SAME runner-mode branch as Plan 02's api job (services: mariadb: for Docker mode with DB_HOST=mariadb, or `docker run -d` + readiness loop for host mode with DB_HOST=127.0.0.1). Set job env: DB_PORT 3306, DB_USER familysync, DB_PASSWORD testpass, DB_NAME familysync (throwaway creds). Include the explicit healthcheck.sh readiness loop before migrate (Pitfall 11; never mysqladmin).
|
||||
|
||||
Steps, in this exact order (08-RESEARCH §Dev-Stack Bring-Up 1..6):
|
||||
1. `uses: actions/checkout@v4`; `uses: actions/setup-node@v4` (node 22); `corepack enable pnpm`.
|
||||
2. `run: pnpm install --frozen-lockfile`.
|
||||
3. (after DB ready) `run: pnpm --filter @familysync/api db:migrate` with DB_* env (drizzle-kit migrate; never push).
|
||||
4. `run: pnpm --filter @familysync/api build` (produces dist/index.js — Pitfall 4).
|
||||
5. Start API as a background process with DEV_AUTH_BYPASS INLINE:
|
||||
`NODE_ENV=development DEV_AUTH_BYPASS=true DB_HOST=$DB_HOST DB_PORT=3306 DB_USER=familysync DB_PASSWORD=testpass DB_NAME=familysync node apps/api/dist/index.js & echo $! > /tmp/api.pid` (Pitfall 8). NODE_ENV must be `development` (not production — global-setup refuses; not test — dev-bypass activation checks development per 08-RESEARCH note).
|
||||
6. Wait for API :3000: a curl retry loop `until curl -sf http://localhost:3000/health` with a ~60s deadline; on timeout, `kill $(cat /tmp/api.pid)` and `exit 1`. This step-level wait (D-02) ensures the API is up BEFORE Playwright starts Vite — separate from and earlier than global-setup's own poll.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "harness" .gitea/workflows/ci.yml && grep -q "node apps/api/dist/index.js" .gitea/workflows/ci.yml && grep -q "DEV_AUTH_BYPASS=true node" .gitea/workflows/ci.yml && grep -q "localhost:3000/health" .gitea/workflows/ci.yml && grep -q "db:migrate" .gitea/workflows/ci.yml && ! grep -q "db:push" .gitea/workflows/ci.yml && echo HARNESS_STACK_OK</automated>
|
||||
</verify>
|
||||
<done>The harness job brings up MariaDB (per runner mode), migrates, builds the API, starts it as a background process with DEV_AUTH_BYPASS=true passed inline on the node line, and waits for :3000/health before continuing.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Playwright install + run (both profiles) + artifact upload on failure</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- apps/pwa/playwright.config.ts (CI gating: reuseExistingServer, reporter:'github', both projects)
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 5 Playwright harness; Pitfall 5 reporter override; Pitfall 6 upload-artifact fork)
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (WebKit deps y/n; upload-artifact fork y/n; did Gitea render 'github' reporter annotations?)
|
||||
</read_first>
|
||||
<action>
|
||||
Continue the `harness` job (08-RESEARCH §Dev-Stack Bring-Up 7..8):
|
||||
7. Install browsers: `run: npx playwright install --with-deps webkit chromium` with `working-directory: apps/pwa`. (If 08-01-SUMMARY showed WebKit deps cannot install on this runner, record that as a phase blocker in the SUMMARY — do NOT silently drop the iphone profile; D-05 requires BOTH profiles. WebKit feasibility is a hard CI-01 input.)
|
||||
8. Run the harness:
|
||||
`run: pnpm test:e2e -- --reporter=list,html` (the `--reporter=list,html` overrides the config's CI `'github'` reporter which renders invisibly in Gitea — Pitfall 5; skip the override only if 08-01-SUMMARY confirmed Gitea renders 'github' annotations, in which case it is harmless to keep).
|
||||
env on this step: `CI: 'true'`, `PLAYWRIGHT_BASE_URL: http://localhost:5173`, `DEV_AUTH_BYPASS: 'true'`, `NODE_ENV: development`, plus DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME (global-setup seeds the DB directly via mysql2). CI=true makes Playwright start Vite itself (:5173) and use retries:2/workers:1; the run covers both `iphone` and `pixel` projects by default (no --project filter).
|
||||
9. Upload artifacts on failure: a final step `if: failure()` `uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4` (NEVER actions/upload-artifact@v4 — GHES-blocked on Gitea, Pitfall 6) with `name: playwright-traces-${{ github.run_id }}`, `path: apps/pwa/test-results/`, `retention-days: 14` (D-06). Add a final `if: always()` step to `kill $(cat /tmp/api.pid) 2>/dev/null || true` to clean up the API background process.
|
||||
|
||||
Do NOT modify playwright.config.ts, global-setup.ts, vite.config.ts, or any spec — CI owns bring-up only (D-01/D-02; phase boundary).
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "playwright install --with-deps webkit chromium" .gitea/workflows/ci.yml && grep -q "test:e2e" .gitea/workflows/ci.yml && grep -q "PLAYWRIGHT_BASE_URL: http://localhost:5173" .gitea/workflows/ci.yml && grep -q "ChristopherHX/gitea-upload-artifact@v4" .gitea/workflows/ci.yml && ! grep -q "actions/upload-artifact@v4" .gitea/workflows/ci.yml && git diff --quiet -- apps/pwa/playwright.config.ts apps/pwa/e2e/global-setup.ts apps/pwa/vite.config.ts && echo HARNESS_RUN_OK</automated>
|
||||
</verify>
|
||||
<done>The harness job installs webkit+chromium with deps, runs pnpm test:e2e (CI=true, DEV_AUTH_BYPASS=true, base URL :5173, DB env) across both profiles with a list,html reporter override, and uploads test-results/ on failure via the gitea fork. No Phase 7 harness file is modified.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verify the harness job on the PR</name>
|
||||
<what-built>The harness job (Tasks 1–2) added to ci.yml, exercised on the open PR to main.</what-built>
|
||||
<how-to-verify>
|
||||
1. Push the branch; on the PR, confirm the `harness` job runs alongside fast-checks + api.
|
||||
2. Confirm it brings up MariaDB → migrate → API (:3000) → Playwright starts Vite (:5173) → both `iphone` and `pixel` projects execute and pass (ROADMAP criteria 3 + 4).
|
||||
3. Confirm the readiness waits prevented a startup race (no "/api/me did not return 200" or ECONNREFUSED from global-setup on a cold run). If global-setup throws the DEV_AUTH_BYPASS error, the API was started without the inline flag (Pitfall 8) — fix the node invocation, do not re-run.
|
||||
4. Deliberately break a spec or seed once (or inspect a prior failure) to confirm test-results/ uploads as a downloadable artifact in the Gitea UI (D-06). Revert the break.
|
||||
5. Confirm test output is readable in the Gitea log (list reporter), not invisible 'github' annotations.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "harness green" once both device profiles pass against the CI-brought-up stack and artifact upload is confirmed, or paste the failing log.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
| --------------------- | --------------------------------------------------------------------------------- |
|
||||
| DEV_AUTH_BYPASS in CI | Bypass auth flag active in the harness job only; must never reach the publish job |
|
||||
| CI test DB → seed | global-setup TRUNCATEs tables; fail-closed guards protect against prod DB |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ----------------- | ----------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-08-06 | Spoofing | DEV_AUTH_BYPASS=true in harness job | mitigate | Bypass is set ONLY in the harness job env, against throwaway DB creds; it never appears in the publish job (Plan 04). global-setup.ts fails closed on NODE_ENV=production and on missing DEV_AUTH_BYPASS, so it cannot wipe/seed an unconfirmed DB (08-RESEARCH Security Domain). |
|
||||
| T-08-07 | Tampering | drizzle migrate against CI DB | mitigate | db:migrate only; db:push forbidden (grep gate). Throwaway creds, ephemeral container. |
|
||||
| T-08-08 | Denial of Service | dev-server startup race | mitigate | Explicit :3000/health curl loop before Playwright (D-02) on top of global-setup's :5173/health + /api/me gates; MariaDB healthcheck.sh readiness loop before migrate (Pitfall 11). |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- ci.yml passes both Task grep gates (background API + inline bypass + :3000 readiness; both browsers + base URL + gitea upload fork; no harness-file edits).
|
||||
- PR run shows the harness job green across iphone + pixel on a cold run.
|
||||
- Artifact upload confirmed on a forced failure; output legible via list reporter.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- CI-01 (harness half): PR to main brings up API + PWA dev servers + MariaDB with DEV_AUTH_BYPASS and runs the Phase 7 specs headlessly; failure gates merge (ROADMAP criterion 3).
|
||||
- Readiness: waits for both :3000 and :5173 before Playwright (ROADMAP criterion 4; Pitfall dev-stack races).
|
||||
- Phase 7 specs reused UNCHANGED (phase boundary); both device profiles run (D-05); traces upload on failure (D-06).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-03-SUMMARY.md` when done. Record: final API readiness timeout, whether the reporter override was needed, WebKit-deps install outcome on the runner, and confirmation no Phase 7 harness file was modified.
|
||||
</output>
|
||||
@@ -0,0 +1,180 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 03
|
||||
subsystem: testing
|
||||
tags: [playwright, ci, gitea, mariadb, webkit, chromium, dev-auth-bypass]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 07-mobile-test-harness
|
||||
provides: Phase 7 Playwright specs (both device profiles) run unchanged in CI
|
||||
- phase: 08-02
|
||||
provides: ci.yml with fast-checks + api jobs; runner-mode (ubuntu-latest, Docker-executor, services:)
|
||||
provides:
|
||||
- harness job in .gitea/workflows/ci.yml bringing up the full dev stack in CI and running the Phase 7 mobile specs across both device profiles
|
||||
- four infrastructure fixes resolving API-reap, IPv6/IPv4 mismatch, missing dev-user seed, and double-pnpm reporter forwarding
|
||||
affects: [08-04, phase-09, phase-10, phase-11, phase-12]
|
||||
|
||||
# Tech tracking
|
||||
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'
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
|
||||
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-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)'
|
||||
- "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'
|
||||
- "Idempotent user seed step: INSERT IGNORE + upsert pattern for DEV_AUTH_BYPASS user before global-setup's FK-dependent seeds"
|
||||
|
||||
requirements-completed: [CI-01]
|
||||
|
||||
# Metrics
|
||||
duration: ~2h (including CI iteration across 4 infrastructure fixes)
|
||||
completed: 2026-06-11
|
||||
---
|
||||
|
||||
# Phase 08 Plan 03: Harness CI Job Summary
|
||||
|
||||
**Gitea Actions harness job brings up MariaDB + API (DEV_AUTH_BYPASS) + Playwright Vite on every PR and runs 58 Phase 7 specs across iPhone/WebKit + Pixel/Chromium in 1.6 min — four infrastructure fixes required, no harness file modified**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** ~2h (task authoring + 4 CI fix iterations)
|
||||
- **Started:** 2026-06-11
|
||||
- **Completed:** 2026-06-11
|
||||
- **Tasks:** 2 auto + 1 checkpoint (human-verified)
|
||||
- **Files modified:** 1 (.gitea/workflows/ci.yml)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Harness job added to ci.yml: MariaDB 11 service → migrate → seed dev user → API background (DEV_AUTH_BYPASS=true, :3000) → Playwright starts Vite (:5173) → both iphone (WebKit) + pixel (Chromium) profiles → traces upload on failure
|
||||
- 58 Phase 7 specs passed green on Gitea Actions run #11 (PR #3, pull_request) — cold CI stack, 1.6 min
|
||||
- All four CI-side infrastructure fixes resolved without touching any Phase 7 harness file (phase boundary D-01/D-02 held)
|
||||
- Artifact upload confirmed working: playwright-traces-10 downloaded from Gitea UI on run #10
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Add harness job — DB + migrate + API background + :3000 readiness** - `d55e347` (feat)
|
||||
2. **Task 2: Add Playwright install + run (both profiles) + artifact upload** - `71c8909` (feat)
|
||||
3. **Fix 1: Keep API alive during harness — start API + run e2e in one step** - `53a989c` (fix)
|
||||
4. **Fix 2: Harness uses 127.0.0.1 + ipv4first — Vite is IPv4-only** - `7389740` (fix)
|
||||
5. **Fix 3: Seed dev user id=1 — global-setup assumes it exists** - `e486c6b` (fix)
|
||||
6. **Fix 4: Call pwa test:e2e directly so --reporter forwards cleanly** - `03e8088` (fix)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `.gitea/workflows/ci.yml` — harness job added; fast-checks + api jobs unchanged
|
||||
|
||||
## Decisions Made
|
||||
|
||||
**D-08-03-COMBINE-STEP:** API start + readiness wait + `pnpm test:e2e` run combined into a single CI step. When the API was started with `node &` in a standalone step, the backgrounded process was reaped when that step exited — the multi-minute browser install that followed caused the API to die before the test step. Confirmed the API does not self-crash when left as a background child of the test shell.
|
||||
|
||||
**D-08-03-IPV4FIRST:** `PLAYWRIGHT_BASE_URL=http://127.0.0.1:5173` and `NODE_OPTIONS=--dns-result-order=ipv4first` set on the harness step. The Gitea runner resolves `localhost` to `::1` (IPv6) first; Vite binds IPv4-only (`127.0.0.1:5173`); Node `fetch` does not fall back to IPv4 unlike `curl`. Proven: `[::1]:5173 ECONNREFUSED` vs `127.0.0.1:5173 200`. The API is dual-stack so its `localhost:3000` references were unaffected.
|
||||
|
||||
**D-08-03-SEED-USER:** An idempotent "Seed dev user (id=1)" step runs after `db:migrate` and before the API starts. `DEV_AUTH_BYPASS` in `devBypass.ts` injects the user entirely in-memory — on a fresh CI database there is no `users` row, so the `global-setup.ts` `INSERT IGNORE INTO calendars` silently fails on the FK constraint and calendar id=10 is absent, causing a cascade FK error on `calendar_events`. The seed is `INSERT IGNORE INTO users (id, oidc_iss, oidc_sub, display_name, color) VALUES (1, 'dev', 'dev-user', 'Dev User', '#4A90D9')`.
|
||||
|
||||
**D-08-03-REPORTER-FORWARD:** The root `test:e2e` script is `pnpm --filter @familysync/pwa test:e2e`. Calling `pnpm test:e2e -- --reporter=list,html` from the root passes `--` through two pnpm layers, resulting in `playwright test -- --reporter=list,html` where `--reporter=list,html` is treated as a test-file path filter — Playwright finds no tests. Fix: call `pnpm --filter @familysync/pwa test:e2e --reporter=list,html` directly. Validated: 58 specs listed vs 0 with the broken invocation.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 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.
|
||||
- **Files modified:** .gitea/workflows/ci.yml
|
||||
- **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.
|
||||
- **Files modified:** .gitea/workflows/ci.yml
|
||||
- **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'`.
|
||||
- **Files modified:** .gitea/workflows/ci.yml
|
||||
- **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.
|
||||
- **Files modified:** .gitea/workflows/ci.yml
|
||||
- **Committed in:** 03e8088
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 4 auto-fixed (all Rule 3 — blocking CI failures). All were infrastructure/orchestration issues. No Phase 7 harness files (playwright.config.ts, global-setup.ts, vite.config.ts, or any spec) were modified.
|
||||
|
||||
## Verified CI Result
|
||||
|
||||
**Gitea Actions run #11** (PR #3, `pull_request` event) — conclusion **SUCCESS**
|
||||
|
||||
- **Harness result:** 58 passed in 1.6 min
|
||||
- **Profiles:** iphone (WebKit) + pixel (Chromium), both passing
|
||||
- **Co-running jobs:** fast-checks (191 PWA tests) + api (238 API tests) — all green in the same run
|
||||
- **Artifact upload:** Confirmed working on run #10 — `playwright-traces-10` uploaded with a download URL via `ChristopherHX/gitea-upload-artifact@v4`
|
||||
- **Phase boundary:** Zero Phase 7 files modified — confirmed via `git diff --quiet -- apps/pwa/playwright.config.ts apps/pwa/e2e/global-setup.ts apps/pwa/vite.config.ts`
|
||||
|
||||
## CI Stack Bring-Up Order (confirmed working)
|
||||
|
||||
1. `services: mariadb:11` container (DB_HOST=mariadb, Docker-executor mode)
|
||||
2. `actions/checkout@v4` + `setup-node@v4` (Node 22) + `corepack enable pnpm`
|
||||
3. `pnpm install --frozen-lockfile`
|
||||
4. mysql2 readiness loop until mariadb port 3306 accepts connections
|
||||
5. `pnpm --filter @familysync/api db:migrate` (never push — verified no `db:push` in ci.yml)
|
||||
6. Seed dev user id=1 (INSERT IGNORE — idempotent)
|
||||
7. `pnpm --filter @familysync/api build` → dist/index.js
|
||||
8. `npx playwright install --with-deps webkit chromium` (from apps/pwa working-directory)
|
||||
9. Combined step: `NODE_ENV=development DEV_AUTH_BYPASS=true ... node apps/api/dist/index.js &` → curl :3000/health readiness loop → `pnpm --filter @familysync/pwa test:e2e --reporter=list,html`
|
||||
10. `if: failure()` — artifact upload via `ChristopherHX/gitea-upload-artifact@v4`
|
||||
11. `if: always()` — kill API background process
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
WebKit deps install: clean exit 0 — confirmed on this runner (D-PROBE-05 from plan 01, re-verified here). No issues encountered.
|
||||
|
||||
Reporter legibility: `list` reporter produced readable per-test output in the Gitea log; `html` report built but is only accessible via artifact download.
|
||||
|
||||
## Known Stubs
|
||||
|
||||
None.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None — no new network endpoints or auth paths introduced. The `DEV_AUTH_BYPASS=true` flag is scoped to the harness job only; it does not appear in the publish job (Plan 04). Threat mitigations T-08-06, T-08-07, T-08-08 confirmed implemented.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Plan 04 (publish job) is unblocked: harness green, CI-01 harness half complete
|
||||
- ROADMAP CI-01 criteria 3 (failure gates merge) and 4 (readiness waits) satisfied
|
||||
- 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_
|
||||
@@ -0,0 +1,158 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ['08-03']
|
||||
files_modified:
|
||||
- .gitea/workflows/ci.yml
|
||||
autonomous: false
|
||||
requirements: [CI-02]
|
||||
user_setup:
|
||||
- service: gitea-registry-pat
|
||||
why: 'Publish job authenticates to the Gitea container registry; created in Plan 01'
|
||||
env_vars:
|
||||
- name: GITEA_REGISTRY_PAT
|
||||
source: 'Repo secret created in Plan 01 (write:package scope)'
|
||||
must_haves:
|
||||
truths:
|
||||
- 'A merge (push) to main triggers a publish job that builds the API Docker production image and pushes it to the Gitea container registry'
|
||||
- 'The image is pushed under two tags: :latest and :<milestone>-<shortsha> (e.g. v1.1-<7charsha>)'
|
||||
- 'Registry authentication uses docker login --password-stdin with the PAT piped from a repo secret — the token never appears in plaintext in the CI log'
|
||||
- 'The publish job runs only on push to main, never on pull_request, and never carries DEV_AUTH_BYPASS'
|
||||
artifacts:
|
||||
- path: '.gitea/workflows/ci.yml'
|
||||
provides: 'push-to-main publish job (CI-02)'
|
||||
contains: 'docker push'
|
||||
key_links:
|
||||
- from: '.gitea/workflows/ci.yml (publish job)'
|
||||
to: 'git.bergerhouse.net registry'
|
||||
via: 'docker login --password-stdin + docker build --target production + docker push'
|
||||
pattern: '--password-stdin'
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the publish job to `.gitea/workflows/ci.yml`: on merge (push) to `main`, build the API Docker `production` image and push it to the Gitea container registry under `:latest` and `:<milestone>-<shortsha>`, authenticating with the operator PAT via `--password-stdin` so the credential never hits the log. This delivers CI-02 (ROADMAP criteria 5 + 6) and Pitfall 13 (--password-stdin).
|
||||
|
||||
Purpose: Every merge to main produces an immutable, traceable image (D-04) plus a moving :latest pointer, with zero credential exposure (ROADMAP criterion 6 is a hard requirement).
|
||||
|
||||
Output: a `publish` job in ci.yml gated on `push → main`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/08-gitea-ci/08-RESEARCH.md
|
||||
@.planning/research/PITFALLS.md
|
||||
@.planning/phases/08-gitea-ci/08-01-SUMMARY.md
|
||||
@apps/api/Dockerfile
|
||||
</context>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
|
||||
- `.gitea/workflows/ci.yml` (EXTENDED — adds the publish job; closes the phase)
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<interface_context>
|
||||
Confirmed facts (do not re-derive):
|
||||
|
||||
- Git remote: `https://git.bergerhouse.net/luckberg/familysync.git` → registry host `git.bergerhouse.net`, owner `luckberg`. Image: `git.bergerhouse.net/luckberg/familysync-api` (08-RESEARCH §Registry Details).
|
||||
- Dockerfile is multi-stage with a `production` target that builds API + PWA and serves both on :3000. It MUST be built from the REPO ROOT with `-f apps/api/Dockerfile .` (the Dockerfile header and 08-RESEARCH §Dockerfile Build Context say so — building from apps/api/ fails because it copies the root workspace manifest + lockfile).
|
||||
- Milestone = `v1.1` (PROJECT.md "Current Milestone"). Per D-04, expose it as the workflow-level `env.MILESTONE` (already added in Plan 02) rather than hardcoding inline; update at milestone boundaries.
|
||||
- Short SHA = `${GITHUB_SHA:0:7}` (CONFIRMED available in Gitea Actions; 08-RESEARCH). If 08-01-SUMMARY found GITHUB_SHA unavailable, fall back to `git rev-parse --short=7 HEAD`.
|
||||
- Registry auth: PAT with write:package in repo secret `GITEA_REGISTRY_PAT` (created Plan 01). `GITHUB_TOKEN`/`GITEA_TOKEN` CANNOT push packages (08-RESEARCH; Gitea forum) — must use the PAT. Username = `luckberg`.
|
||||
- Pitfall 13: NEVER `docker login -p $TOKEN` (token leaks to the log / process list). ALWAYS `echo "$PAT" | docker login git.bergerhouse.net -u luckberg --password-stdin`.
|
||||
</interface_context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add the publish job (build + tag + login --password-stdin + push)</name>
|
||||
<files>.gitea/workflows/ci.yml</files>
|
||||
<read_first>
|
||||
- .planning/phases/08-gitea-ci/08-RESEARCH.md (§Pattern 6 Docker publish; §Docker Registry Push; §Image Tag Strategy D-04; Pitfall on GITHUB_TOKEN)
|
||||
- .planning/research/PITFALLS.md (Pitfall 13 --password-stdin)
|
||||
- apps/api/Dockerfile (production target; build-from-root requirement)
|
||||
- .planning/phases/08-gitea-ci/08-01-SUMMARY.md (docker socket access confirmed; GITHUB_SHA availability)
|
||||
</read_first>
|
||||
<action>
|
||||
Add a `publish` job to ci.yml: `runs-on: self-hosted`, guarded `if: github.event_name == 'push' && github.ref == 'refs/heads/main'` (push-to-main ONLY — never pull_request; D-03). It runs independently of the PR jobs (those are pull_request-gated and won't fire on push). Do NOT set DEV_AUTH_BYPASS anywhere in this job (T-08-06 boundary).
|
||||
|
||||
Steps:
|
||||
1. `uses: actions/checkout@v4`.
|
||||
2. Compute tags (id: tags). Derive `SHORT_SHA=${GITHUB_SHA:0:7}` (fallback `git rev-parse --short=7 HEAD` if 08-01 flagged GITHUB_SHA missing). Use the workflow-level `${{ env.MILESTONE }}` (= v1.1). Emit two outputs:
|
||||
`latest=git.bergerhouse.net/luckberg/familysync-api:latest`
|
||||
`sha_tag=git.bergerhouse.net/luckberg/familysync-api:${MILESTONE}-${SHORT_SHA}`
|
||||
(write to `$GITHUB_OUTPUT`).
|
||||
3. Docker login via stdin (Pitfall 13 — the load-bearing security step):
|
||||
`echo "${{ secrets.GITEA_REGISTRY_PAT }}" | docker login git.bergerhouse.net --username luckberg --password-stdin`
|
||||
NEVER use `-p`/`--password` with the token as an argument. Do not `echo` the secret anywhere else; do not set it as a plain env var.
|
||||
4. Build + push from REPO ROOT:
|
||||
`docker build --target production -f apps/api/Dockerfile -t <latest> -t <sha_tag> .`
|
||||
then `docker push <latest>` and `docker push <sha_tag>`.
|
||||
5. Final `if: always()` step: `docker logout git.bergerhouse.net || true` to drop the stored credential from the runner after push.
|
||||
|
||||
Use `docker/login-action`/`docker/build-push-action` ONLY if 08-01-SUMMARY confirmed they resolve AND you prefer them; the shell `docker login --password-stdin` + `docker build`/`docker push` form is the safer first iteration (08-RESEARCH §Pattern 6 note) and is the recommended path.
|
||||
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "github.event_name == 'push'" .gitea/workflows/ci.yml && grep -q "refs/heads/main" .gitea/workflows/ci.yml && grep -q -- "--password-stdin" .gitea/workflows/ci.yml && ! grep -E "docker login.*(-p |--password )[^-]" .gitea/workflows/ci.yml && grep -q "docker build --target production" .gitea/workflows/ci.yml && grep -q "familysync-api:latest" .gitea/workflows/ci.yml && grep -q 'familysync-api:${MILESTONE}' .gitea/workflows/ci.yml && grep -q "docker push" .gitea/workflows/ci.yml && ! grep -qi "DEV_AUTH_BYPASS" <(awk '/publish:/,0' .gitea/workflows/ci.yml) && echo PUBLISH_OK</automated>
|
||||
</verify>
|
||||
<done>The publish job runs only on push to main, logs in with --password-stdin (never -p), builds the production target from repo root, pushes :latest and :${MILESTONE}-<shortsha>, logs out, and never sets DEV_AUTH_BYPASS.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 2: Merge, audit the publish log, and verify both tags</name>
|
||||
<what-built>The publish job (Task 1). Verifying it requires merging the PR to main and auditing the resulting CI log + registry — the executor cannot merge a protected branch.</what-built>
|
||||
<how-to-verify>
|
||||
1. Merge the PR (all PR jobs green) into `main` — the push triggers the publish job.
|
||||
2. In Gitea → Actions, open the publish job log and AUDIT it line by line: the PAT must NOT appear in plaintext anywhere (ROADMAP criterion 6 — hard requirement). The `docker login` line should show `--password-stdin`, never the token. If the token is visible, STOP — rotate the PAT and fix before anything else.
|
||||
3. Confirm the build used `--target production -f apps/api/Dockerfile .` and succeeded.
|
||||
4. In Gitea → repo → Packages, confirm `familysync-api` exists with BOTH tags: `latest` and `v1.1-<7charsha>` matching the merge commit.
|
||||
5. (Optional) `docker pull git.bergerhouse.net/luckberg/familysync-api:latest` from a machine with registry access to confirm the image is pullable.
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "publish verified" once both tags exist in the registry AND the log audit confirms no plaintext PAT, or describe the failure.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------------- |
|
||||
| Repo secret → docker login | PAT crosses into the job; the single highest-value secret in this phase |
|
||||
| publish job → registry | Authenticated push to the package registry |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
| --------- | ---------------------- | ---------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| T-08-PAT | Information Disclosure | GITEA_REGISTRY_PAT in publish job | mitigate | `docker login --password-stdin` exclusively — token piped via stdin, never an `-p`/`--password` argument (Pitfall 13). Token referenced only as `${{ secrets.GITEA_REGISTRY_PAT }}` (Gitea masks registered secrets in logs); never echoed elsewhere; `docker logout` after push. Grep gate forbids `-p`/`--password` forms. Checkpoint requires a line-by-line log audit (ROADMAP criterion 6). This is the load-bearing mitigation for the phase. |
|
||||
| T-08-09 | Spoofing | DEV_AUTH_BYPASS bleed into publish | mitigate | Publish job never sets DEV_AUTH_BYPASS (grep gate scoped to the publish: block); the bypass is confined to the harness job (Plan 03). |
|
||||
| T-08-10 | Tampering | wrong build context | mitigate | Build from repo root with `-f apps/api/Dockerfile .` (Dockerfile requires root context for the workspace manifest + lockfile); building from apps/api/ would fail or produce a broken image. |
|
||||
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- ci.yml passes the Task grep gate (push-to-main guard, --password-stdin, no -p, production target from root, both tags, no DEV_AUTH_BYPASS in publish block).
|
||||
- After merge: both tags present in the Gitea registry; log audit shows no plaintext PAT.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
- CI-02: on merge to main, the API production image is built and pushed to the Gitea registry under :latest + :v1.1-<shortsha> (ROADMAP criterion 5; D-04).
|
||||
- Registry credentials never appear in plaintext in the CI logs (ROADMAP criterion 6; Pitfall 13) — the load-bearing security outcome of the phase.
|
||||
- Publish runs only on push to main; DEV_AUTH_BYPASS never bleeds into it.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/08-gitea-ci/08-04-SUMMARY.md` when done. Record: the final image name + both tags pushed, confirmation the log audit found no plaintext PAT, and whether the shell or docker/* action form was used.
|
||||
</output>
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
plan: '04'
|
||||
subsystem: infra
|
||||
tags: [gitea, docker, ci, registry, publish, security]
|
||||
|
||||
requires:
|
||||
- phase: 08-03
|
||||
provides: harness job in ci.yml; dev-stack bring-up confirmed in CI
|
||||
|
||||
provides:
|
||||
- publish job in .gitea/workflows/ci.yml (push to main → build + push API Docker image)
|
||||
- familysync-api:latest and familysync-api:v1.1-<7charsha> tags in Gitea registry
|
||||
- --password-stdin docker login pattern (REGISTRY_PAT secret, never plaintext)
|
||||
|
||||
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)'
|
||||
|
||||
key-files:
|
||||
created: []
|
||||
modified:
|
||||
- .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).'
|
||||
|
||||
requirements-completed: [CI-02]
|
||||
|
||||
duration: checkpoint-verified
|
||||
completed: '2026-06-11'
|
||||
---
|
||||
|
||||
# Phase 08 Plan 04: Publish Job Summary
|
||||
|
||||
**Publish job wired to push-to-main: API Docker production image pushed to Gitea registry under :latest and :v1.1-<7charsha>, PAT authenticated via --password-stdin (never plaintext), security audit passed — CI-02 delivered.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** multi-step (Task 1 executed, Task 2 was a human-verify checkpoint — verified green on run #14)
|
||||
- **Started:** 2026-06-11
|
||||
- **Completed:** 2026-06-11
|
||||
- **Tasks:** 2 (1 auto + 1 human-verify checkpoint)
|
||||
- **Files modified:** 1
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- 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).
|
||||
|
||||
## Task Commits
|
||||
|
||||
1. **Task 1: Add the publish job** - `ebcc38d` (feat)
|
||||
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
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `.gitea/workflows/ci.yml` — publish job added (push-to-main guard, --password-stdin docker login, build --target production from repo root, dual push :latest + :v1.1-<sha>, docker logout always())
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- **REGISTRY_PAT naming (D-PAT-NAMING):** Gitea reserves the `GITEA_` prefix for built-in variables and silently strips any user-defined secret with that prefix from the runner environment. The original plan named the secret `GITEA_REGISTRY_PAT`; the fix renames it to `REGISTRY_PAT` (commit 73eecf7). Downstream plans must use `REGISTRY_PAT` if they add registry operations.
|
||||
- **Shell form over marketplace actions:** `echo "${{ secrets.REGISTRY_PAT }}" | docker login ... --password-stdin` + `docker build` + `docker push` rather than `docker/login-action` / `docker/build-push-action`. This matches the 08-RESEARCH §Pattern 6 preferred path; no external action marketplace dependency.
|
||||
- **Dual push:** two separate `docker push` calls (`:latest` + `:<milestone>-<sha>`) — same digest, two references. `:latest` is the moving pointer; `:<milestone>-<sha>` is the immutable traceability tag per D-04.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 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).
|
||||
- **Files modified:** `.gitea/workflows/ci.yml`
|
||||
- **Verification:** Run #14 succeeded — "Login Succeeded" in log; both tags pushed; PAT masked.
|
||||
- **Committed in:** `73eecf7`
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
|
||||
**Impact on plan:** Required for the publish job to function at all. No scope creep. Security outcome (no plaintext PAT) unchanged.
|
||||
|
||||
## 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
|
||||
|
||||
- Phase 8 (Gitea CI) is fully complete — all six ROADMAP success criteria met (see 08-VERIFICATION.md).
|
||||
- CI-01 (PR regression: lint + typecheck + unit + API integration + mobile harness) delivered by plans 02 + 03.
|
||||
- CI-02 (push-to-main publish) delivered by this plan (04).
|
||||
- The Gitea registry now has a pullable `familysync-api:latest` image. Future phases can reference it for deployment documentation or smoke tests.
|
||||
- Phase 9 (Faster Write-Back) is fully independent and can start immediately.
|
||||
|
||||
---
|
||||
|
||||
_Phase: 08-gitea-ci_
|
||||
_Completed: 2026-06-11_
|
||||
@@ -0,0 +1,125 @@
|
||||
# Phase 8: Gitea CI - Context
|
||||
|
||||
**Gathered:** 2026-06-11
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Phase 8 adds CI on the existing self-hosted **Gitea Actions** runner. Two outcomes:
|
||||
|
||||
1. **PR regression gate** — every PR targeting `main` runs lint, typecheck (both apps), unit tests, API-integration tests against a MariaDB service container, **and the Phase 7 mobile Playwright harness** (against a CI-brought-up dev stack with `DEV_AUTH_BYPASS=true`). Any failure blocks the merge.
|
||||
2. **Publish on merge** — a push to `main` builds and publishes the API Docker image to the Gitea container registry.
|
||||
|
||||
This phase owns only the CI plumbing: workflow files, dev-stack bring-up + readiness waits, image build/push. It does **not** modify the Phase 7 harness specs (CI reuses them unchanged), the Dockerfile (already multi-stage, builds API + PWA), or application code. Requirements: **CI-01, CI-02**.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Dev-stack bring-up in CI (for the harness step)
|
||||
|
||||
- **D-01:** Bring up the stack with **bare background processes + a MariaDB service container** — NOT docker compose, NOT a production image.
|
||||
- MariaDB runs as a Gitea **service container** (the same one the API-integration job needs; `DB_HOST=127.0.0.1`, service creds).
|
||||
- The **API** runs as a background process via `pnpm dev:api` (or equivalent) with `DEV_AUTH_BYPASS=true` and `DB_HOST=127.0.0.1`, listening on `:3000`.
|
||||
- The **PWA** Vite dev server is started by **Playwright's own `webServer`** config (already present; `reuseExistingServer: !process.env.CI`), on `:5173`. Vite proxies `/api`, `/health`, `/callback` → `:3000`.
|
||||
- Rationale: no docker-in-docker on the self-hosted runner; matches the Phase 7 dev-server harness contract exactly; reuses the MariaDB service container already required for integration tests.
|
||||
- **D-02:** The harness step MUST wait for **both** the API (`:3000`) and the PWA Vite server (`:5173`) to accept connections before Playwright launches. The harness already polls `baseURL/health` (proxied to the API) in `global-setup.ts`; CI must additionally ensure the API process is up first. This is on top of the MariaDB-11 readiness loop (Pitfall 11 — `healthcheck.sh --connect --innodb_initialized`, never `mysqladmin ping`).
|
||||
|
||||
### Workflow topology & jobs
|
||||
|
||||
- **D-03:** **One workflow file with parallel, event-gated jobs.**
|
||||
- `pull_request` → `main`: fast-checks job (lint + typecheck both apps + unit tests) runs **in parallel** with the heavier API-integration job and the harness job. Fast feedback — a lint failure does not wait behind the harness.
|
||||
- `push` → `main` (merge): build-and-publish job runs.
|
||||
- Single file so the whole regression + publish story lives in one place; accept the minor setup duplication (checkout, pnpm cache, Node-22 pin) across jobs.
|
||||
|
||||
### Docker image tag strategy (CI-02)
|
||||
|
||||
- **D-04:** On merge to `main`, publish the API image with **two tags**: `:latest` (moving pointer) **and** `:<milestone>-<shortsha>` (immutable, e.g. `v1.1-4303a1b`).
|
||||
- The milestone string (e.g. `v1.1`) is read from PROJECT.md / ROADMAP.md, not hardcoded inline if avoidable.
|
||||
- `<shortsha>` is the short commit SHA of the merge commit.
|
||||
- Rationale: `:latest` for easy pulls; the milestone-prefixed SHA tag groups builds by release line and stays immutable for rollback/traceability.
|
||||
|
||||
### Failure artifacts & browser matrix
|
||||
|
||||
- **D-05:** Run **both** device profiles in CI — iPhone 14/WebKit **and** Pixel 7/Chromium (the full Phase 7 matrix). Install whatever system deps WebKit needs on the runner (probe in the runner-probe step).
|
||||
- **D-06:** On harness **failure**, upload Playwright **traces / screenshots / videos** as CI artifacts for debugging. The config already emits `trace`/`video` `on-first-retry` and `screenshot: only-on-failure`; CI must upload the `test-results/` output. Note the config's `reporter: 'github'` may not render natively in Gitea Actions — verify during the runner probe and fall back to `list`/`html` if annotations don't surface.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Exact job names, step ordering within a job, pnpm store cache key strategy, and whether fast-checks is one job or split — planner/executor decide.
|
||||
- Whether the API background process is launched with `pnpm dev:api` vs a built `node dist` — pick whatever gives reliable `:3000` readiness under `DEV_AUTH_BYPASS`; the harness only needs the authed PWA reachable (Dev User 1 has no CalDAV creds, so verify layout/flows, not live event-create).
|
||||
- Registry hostname / image repository path under the Gitea registry.
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
### Phase scope & requirements
|
||||
|
||||
- `.planning/ROADMAP.md` §"Phase 8: Gitea CI" — goal, 6 success criteria, pitfalls this phase owns.
|
||||
- `.planning/REQUIREMENTS.md` — CI-01 (PR regression incl. harness), CI-02 (publish image on merge).
|
||||
- `.planning/PITFALLS.md` — Pitfalls 11 (MariaDB-11 readiness), 12 (runner-probe first), 13 (`--password-stdin`), 15 (SW block, harness side).
|
||||
|
||||
### Harness the CI step runs (reused unchanged from Phase 7)
|
||||
|
||||
- `apps/pwa/playwright.config.ts` — device matrix, `serviceWorkers: 'block'`, `webServer` (Vite-only, `reuseExistingServer: !CI`), `retries`/`workers`/`reporter` under `CI`, env-driven `PLAYWRIGHT_BASE_URL`.
|
||||
- `apps/pwa/e2e/global-setup.ts` — `/health` readiness poll, `/api/me` DEV_AUTH_BYPASS reachability gate, fail-closed env guard (refuses `NODE_ENV=production` or missing `DEV_AUTH_BYPASS`), mysql2 truncate-and-seed (calendar id 10, lists/items for user 1).
|
||||
- `.planning/phases/07-mobile-test-harness/07-CONTEXT.md` — Phase 7 decisions D-01..D-10 (auth strategy, SW block, env baseURL, compose-managed backend).
|
||||
|
||||
### Infra the CI builds/runs against
|
||||
|
||||
- `apps/api/Dockerfile` — multi-stage: `builder` (API), `pwa-builder` (PWA dist → `./public`), `production` target. CI publishes the `production` target.
|
||||
- `docker-compose.yml` / `docker-compose.dev.yml` — service shape, MariaDB 11 healthcheck (`healthcheck.sh --connect --innodb_initialized`), dev override exposing 3306, API `dev` build target.
|
||||
- `apps/pwa/vite.config.ts` — dev proxy (`/api`, `/health`, `/callback` → `:3000`) the harness depends on.
|
||||
- `package.json` (root) — scripts: `dev:api`, `dev:pwa`, `test`, `test:e2e`, `lint`, `typecheck`.
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
|
||||
- **Playwright config + global-setup (Phase 7):** ready to run headlessly in CI. `retries: 2`, `workers: 1`, `reporter: 'github'` already gated on `process.env.CI`. CI sets `CI=true` and `PLAYWRIGHT_BASE_URL` and the harness behaves correctly. No spec changes.
|
||||
- **MariaDB service-container pattern:** API-integration tests already require a real MariaDB with `DB_HOST=127.0.0.1` + service creds + Drizzle `generate`+`migrate` for schema. The harness's `global-setup` seeds the same DB directly via mysql2. One MariaDB service container can back both the integration job and the harness job.
|
||||
- **Multi-stage Dockerfile:** `production` target already builds API + PWA and serves both on `:3000`. CI build/push is a thin wrapper (`docker build --target production` + `docker login --password-stdin` + `docker push`).
|
||||
|
||||
### Established Patterns
|
||||
|
||||
- **Gitea, not GitHub:** origin is self-hosted Gitea; `main` is protected (PRs only). Gitea Actions is GitHub-Actions-syntax-compatible but **do not assume `actions/setup-node` behaves identically** — runner-probe first (Pitfall 12), pin Node 22 explicitly.
|
||||
- **node-cron lesson (long-running process):** not directly relevant to CI, but the API in CI is short-lived/background — no scheduler concerns.
|
||||
|
||||
### Integration Points
|
||||
|
||||
- CI orchestrates, in order, for the harness job: MariaDB service container (readiness loop) → Drizzle generate+migrate → API background process (`DEV_AUTH_BYPASS=true`, `:3000`, readiness wait) → Playwright (`webServer` starts Vite `:5173`, `global-setup` polls `/health` + `/api/me`) → specs → upload artifacts on failure.
|
||||
- Publish job depends on `apps/api/Dockerfile` `production` target + Gitea registry credentials (PAT with `write:package`, piped via `--password-stdin`).
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Image tag format locked to `:latest` + `:v1.1-<shortsha>` (milestone prefix + short SHA). Example: `v1.1-4303a1b`.
|
||||
- Start the very first CI iteration as a **runner-probe** only: `node --version` / `pnpm --version` / Docker access / WebKit dep availability on the `self-hosted` runner — before any real test/build steps are designed.
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
- **ROADMAP status fix:** ROADMAP.md line 29 marks Phase 8 "completed 2026-06-11" while line 204 says "Not started" and no Phase 8 artifacts exist. This is a bookkeeping error to correct (Phase 8 is being started now) — a docs/roadmap cleanup, not Phase 8 scope.
|
||||
- **Desktop e2e coverage:** moved to backlog **Phase 999.15** — Phase 8 CI gates mobile only; adding a Desktop Playwright profile + desktop-safe specs is out of CI-plumbing scope.
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
_Phase: 8-Gitea CI_
|
||||
_Context gathered: 2026-06-11_
|
||||
@@ -0,0 +1,74 @@
|
||||
# Phase 8: Gitea CI - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** 2026-06-11
|
||||
**Phase:** 8-Gitea CI
|
||||
**Areas discussed:** Dev-stack bring-up in CI, Workflow topology & jobs, Docker image tag strategy, Failure artifacts & browser matrix
|
||||
|
||||
---
|
||||
|
||||
## Dev-stack bring-up in CI
|
||||
|
||||
| Option | Description | Selected |
|
||||
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||
| Bare processes + service MariaDB | MariaDB as a Gitea service container; API as background `pnpm dev:api` with DEV_AUTH_BYPASS; Playwright webServer starts Vite. No docker-in-docker. | ✓ |
|
||||
| docker compose dev override | Run docker-compose.yml + dev override inside the runner; adds image-build time + docker-in-docker. | |
|
||||
| Production image for harness | Build prod image, run harness against API-served PWA on :3000; diverges from Phase 7 dev-server contract. | |
|
||||
|
||||
**User's choice:** Bare processes + service MariaDB
|
||||
**Notes:** Reuses the MariaDB service container already needed by the API-integration tests; matches the Phase 7 harness contract exactly.
|
||||
|
||||
---
|
||||
|
||||
## Workflow topology & jobs
|
||||
|
||||
| Option | Description | Selected |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------ | -------- |
|
||||
| One file, parallel jobs | Single workflow; PR→main runs fast-checks in parallel with integration + harness; push→main publishes. | ✓ |
|
||||
| Two files | Separate ci.yml + publish.yml; cleaner split, duplicated setup. | |
|
||||
| One file, linear job | Single sequential job; simplest, slowest feedback. | |
|
||||
|
||||
**User's choice:** One file, parallel jobs
|
||||
**Notes:** Fast feedback prioritized; minor setup duplication across jobs accepted.
|
||||
|
||||
---
|
||||
|
||||
## Docker image tag strategy
|
||||
|
||||
| Option | Description | Selected |
|
||||
| ------------------------------------- | ---------------------------------------------------- | -------- |
|
||||
| latest + short SHA | :latest + :<short-sha> | |
|
||||
| Short SHA only | Immutable per-commit only | |
|
||||
| latest only | Single moving tag | |
|
||||
| semver from package.json | Version field + latest | |
|
||||
| **latest + milestone short (custom)** | :latest + :<milestone>-<shortsha>, e.g. v1.1-4303a1b | ✓ |
|
||||
|
||||
**User's choice:** latest + `v1.1-4303a1b` (milestone prefix + short SHA), confirmed in follow-up over a 3-tag variant and a no-milestone variant.
|
||||
**Notes:** Milestone string read from PROJECT.md/ROADMAP, not hardcoded; SHA tag immutable for rollback.
|
||||
|
||||
---
|
||||
|
||||
## Failure artifacts & browser matrix
|
||||
|
||||
| Option | Description | Selected |
|
||||
| --------------------------------- | ---------------------------------------------------------------------------- | -------- |
|
||||
| Upload on failure + both profiles | iPhone/WebKit + Pixel/Chromium; upload traces/screenshots/videos on failure. | ✓ |
|
||||
| Upload on failure + Chromium only | Pixel/Chromium only; faster, loses iOS-engine coverage. | |
|
||||
| Both profiles, no artifacts | Full matrix, log-only failures. | |
|
||||
|
||||
**User's choice:** Upload on failure + both profiles
|
||||
**Notes:** Full mobile coverage + debuggable failures. `reporter: 'github'` may not render in Gitea — verify in runner probe, fall back if needed.
|
||||
|
||||
---
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
- Exact job names/step ordering, pnpm cache key, fast-checks split.
|
||||
- API launch mechanism (`pnpm dev:api` vs built `node dist`) as long as `:3000` is reliably ready under DEV_AUTH_BYPASS.
|
||||
- Registry hostname / image repo path.
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
- ROADMAP status conflict (line 29 "completed" vs line 204 "Not started", no artifacts) — bookkeeping fix, not Phase 8 scope.
|
||||
@@ -0,0 +1,798 @@
|
||||
# Phase 8: Gitea CI — Research
|
||||
|
||||
**Researched:** 2026-06-11
|
||||
**Domain:** Gitea Actions / act_runner, GitHub Actions service containers, Playwright CI, Docker registry
|
||||
**Confidence:** MEDIUM — the runner does not yet exist on the Unraid host (0 runners registered); all runner-mode and service-container behaviour is inferred from Gitea/act docs and community reports and must be confirmed via the runner-probe task.
|
||||
|
||||
---
|
||||
|
||||
<user_constraints>
|
||||
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
### Locked Decisions
|
||||
|
||||
**D-01** — Dev-stack bring-up: bare background processes + MariaDB service container. No docker compose, no production image. MariaDB = Gitea service container (shared with integration job). API = background `pnpm dev:api` (needs build first; `dev` script is `node --watch dist/index.js`). PWA Vite dev server = started by Playwright's own `webServer` config. `reuseExistingServer: !process.env.CI` means Playwright WILL start Vite itself when `CI=true`.
|
||||
|
||||
**D-02** — Harness step must wait for both `:3000` (API) and `:5173` (Vite) to be ready before Playwright launches. The harness `global-setup.ts` already polls `baseURL/health` (proxied to the API) and gates `/api/me` for DEV_AUTH_BYPASS. CI must additionally ensure the API process is up before `global-setup` runs. MariaDB readiness uses `healthcheck.sh --connect --innodb_initialized` (never `mysqladmin ping` — removed in MariaDB 11).
|
||||
|
||||
**D-03** — One workflow file with parallel, event-gated jobs. `pull_request → main`: fast-checks (lint + typecheck + unit tests) in parallel with API-integration job and harness job. `push → main` (merge): build-and-publish job.
|
||||
|
||||
**D-04** — Two tags on merge: `:latest` + `:<milestone>-<shortsha>` (e.g. `v1.1-4303a1b`). Milestone string read from PROJECT.md/ROADMAP.md (currently `v1.1`), not hardcoded inline. Short SHA = first 7 chars of `GITHUB_SHA`.
|
||||
|
||||
**D-05** — Full Phase 7 device matrix in CI: iPhone 14/WebKit AND Pixel 7/Chromium. Install WebKit system deps on runner.
|
||||
|
||||
**D-06** — On harness failure, upload `test-results/` (traces/screenshots/videos) as CI artifacts. Reporter `'github'` in playwright.config.ts may not render annotations in Gitea — verify and fall back to `list`+`html` if so.
|
||||
|
||||
### Claude's Discretion
|
||||
|
||||
- Exact job names, step ordering within a job, pnpm store cache key strategy, whether fast-checks is one job or split.
|
||||
- Whether API background process is `pnpm dev:api` vs a built `node dist/index.js` — pick whatever gives reliable `:3000` readiness under `DEV_AUTH_BYPASS`.
|
||||
- Registry hostname / image repository path under the Gitea registry.
|
||||
|
||||
### 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>
|
||||
|
||||
<phase_requirements>
|
||||
|
||||
## Phase Requirements
|
||||
|
||||
| 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. |
|
||||
|
||||
</phase_requirements>
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 8 adds a single `.gitea/workflows/ci.yml` file that delivers a PR regression gate and a merge-triggered publish job. The technical unknowns cluster around three areas that all require a runner-probe task before anything else is trusted: (1) which GitHub Actions marketplace actions resolve on this self-hosted act_runner and in what runner mode it operates; (2) whether the `services:` key starts MariaDB when the runner runs jobs in Docker-container mode (the recommended mode), and what hostname the job container uses to reach it; (3) whether `actions/upload-artifact@v4` works on Gitea 1.26 or whether the `gitea-upload-artifact` fork is required.
|
||||
|
||||
The single most important finding: **service containers (`services:`) work when act_runner runs jobs in Docker-container mode (the default), but are NOT supported when the runner is configured for host-executor mode.** The runner-probe's first task is to determine which mode the Unraid runner is in. If the runner is in host mode, the plan must pivot: either spin up MariaDB via a `docker run` step in the workflow (instead of `services:`), or request that the runner be reconfigured to Docker mode.
|
||||
|
||||
For the publish job, the Gitea container registry path is `git.bergerhouse.net/luckberg/<image>`. The built-in `GITHUB_TOKEN` does NOT work for Gitea package registry pushes — a PAT with `write:package` scope stored as a repository secret is required. `docker/login-action@v3` + `docker/build-push-action@v6` resolve from GitHub by default (via `DEFAULT_ACTIONS_URL`) and appear to work in most Gitea installations; the runner-probe confirms.
|
||||
|
||||
**Primary recommendation:** Write the workflow in three phases — (W0) a runner-probe-only workflow that prints Node/pnpm/Docker versions and tests the key assumptions; (W1) the fast-checks + integration jobs; (W2) the harness job + publish job. Each wave is committed only after the previous wave's probe confirms the assumptions it depends on.
|
||||
|
||||
---
|
||||
|
||||
## 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) |
|
||||
|
||||
---
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### 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. |
|
||||
|
||||
### No `pnpm/action-setup` needed
|
||||
|
||||
The repo root `package.json` declares `"packageManager": "pnpm@11.5.1"`. With Node.js installed via `actions/setup-node`, enabling corepack via `corepack enable pnpm` in a step is sufficient. [ASSUMED] — probe confirms pnpm is resolvable this way.
|
||||
|
||||
### Workflow file location
|
||||
|
||||
`.gitea/workflows/ci.yml` — Gitea primarily reads `.gitea/workflows/`. Both `.gitea/` and `.github/` are supported, but having files in `.gitea/` takes precedence. [CITED: docs.gitea.com/usage/actions/quickstart]
|
||||
|
||||
---
|
||||
|
||||
## Package Legitimacy Audit
|
||||
|
||||
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 |
|
||||
|
||||
**Packages removed due to SLOP verdict:** none
|
||||
**Packages flagged as suspicious SUS:** none
|
||||
|
||||
---
|
||||
|
||||
## Runner-Probe Checklist
|
||||
|
||||
This is the single most important planning output for Phase 8. Every runner assumption MUST be confirmed by running a minimal probe workflow before the real CI steps are designed.
|
||||
|
||||
The runner-probe workflow lives at `.gitea/workflows/runner-probe.yml`, runs only on a named test branch (e.g. `gsd/phase-08-gitea-ci`), and does nothing destructive.
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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
|
||||
- Explicit readiness loop step required (no `options:` health-check auto-wait)
|
||||
- This is the fallback path; probe determines if it applies
|
||||
|
||||
---
|
||||
|
||||
## 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] |
|
||||
|
||||
---
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture Diagram
|
||||
|
||||
```
|
||||
PR opened / push to main
|
||||
│
|
||||
▼
|
||||
.gitea/workflows/ci.yml
|
||||
│
|
||||
├─── on: pull_request ──────────────────────────────────────────────┐
|
||||
│ │ │
|
||||
│ ┌────▼──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ fast-checks job │ │ api-integration │ │
|
||||
│ │ (parallel) │ │ job (parallel) │ │
|
||||
│ │ • pnpm install │ │ • MariaDB service │ │
|
||||
│ │ • lint │ │ • pnpm install │ │
|
||||
│ │ • typecheck api+pwa │ │ • drizzle migrate │ │
|
||||
│ │ • vitest unit tests │ │ • vitest run │ │
|
||||
│ └───────────────────────┘ │ (api only) │ │
|
||||
│ └──────────────────┘ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ harness job (parallel) │ │
|
||||
│ │ • MariaDB service container (shared need, same pattern) │ │
|
||||
│ │ • pnpm install │ │
|
||||
│ │ • drizzle generate + migrate │ │
|
||||
│ │ • pnpm build (api) → node dist/index.js & │ │
|
||||
│ │ • wait :3000 /health │ │
|
||||
│ │ • DEV_AUTH_BYPASS=true CI=true PLAYWRIGHT_BASE_URL=... │ │
|
||||
│ │ • pnpm test:e2e (Playwright starts Vite :5173 itself) │ │
|
||||
│ │ • upload test-results/ on failure │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─── on: push (main) ───────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────▼──────────────┐
|
||||
│ publish job │
|
||||
│ • docker login (PAT) │
|
||||
│ • docker build │
|
||||
│ --target production │
|
||||
│ • docker push :latest │
|
||||
│ • docker push :v1.1-sha│
|
||||
└────────────────────────┘
|
||||
```
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
.gitea/
|
||||
└── workflows/
|
||||
├── runner-probe.yml # Wave 0: probe only, runs on feature branch
|
||||
└── ci.yml # Waves 1-2: real CI after probe passes
|
||||
```
|
||||
|
||||
### Pattern 1: MariaDB Service Container (Docker-mode runner)
|
||||
|
||||
**What:** Declare MariaDB as a `services:` entry; act_runner starts it as a sibling container on the same Docker network as the job container. Job reaches it by service label hostname.
|
||||
|
||||
**When to use:** Runner probe P-03 confirms the job runs in a Docker container (Docker mode).
|
||||
|
||||
```yaml
|
||||
# Source: [ASSUMED from GitHub Actions docs + MariaDB docs]
|
||||
jobs:
|
||||
api-integration:
|
||||
runs-on: self-hosted
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
env:
|
||||
MARIADB_ROOT_PASSWORD: root
|
||||
MARIADB_DATABASE: familysync
|
||||
MARIADB_USER: familysync
|
||||
MARIADB_PASSWORD: testpass
|
||||
options: >-
|
||||
--health-cmd="healthcheck.sh --connect --innodb_initialized"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=10
|
||||
--health-start-period=30s
|
||||
env:
|
||||
DB_HOST: mariadb # service label name — Docker mode only
|
||||
DB_PORT: 3306
|
||||
DB_USER: familysync
|
||||
DB_PASSWORD: testpass
|
||||
DB_NAME: familysync
|
||||
```
|
||||
|
||||
**Critical note on `--health-start-period`:** MariaDB 11 takes longer to initialize InnoDB than older versions. Set `--health-start-period=30s` to avoid premature health-check failures during container startup. [ASSUMED based on MariaDB 11 init time; tune in probe]
|
||||
|
||||
### Pattern 2: MariaDB Without Service Containers (host-mode runner fallback)
|
||||
|
||||
**What:** If P-03 shows host mode, start MariaDB manually with `docker run -d` in a step and do an explicit readiness loop.
|
||||
|
||||
**When to use:** Runner probe P-03 shows job runs directly on host (host mode).
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Pattern 3: API Background Process
|
||||
|
||||
**What:** Build the API, start it as a background process, wait for `:3000/health`.
|
||||
|
||||
**When to use:** Harness job only (CI-01 harness step).
|
||||
|
||||
```yaml
|
||||
# Source: [ASSUMED — standard CI background-process pattern]
|
||||
- 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: 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.
|
||||
|
||||
### Pattern 4: Drizzle Migration in CI
|
||||
|
||||
**What:** Run `drizzle-kit generate` (idempotent, generates SQL from schema if needed) then `drizzle-kit migrate` against the service container. Do NOT use `db:push` (documented as unsafe on MariaDB — project memory `drizzle-mariadb-push-unsafe`).
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### Pattern 5: Playwright Harness in CI
|
||||
|
||||
**What:** Run the full Phase 7 harness against the CI-brought-up dev stack. Playwright's `webServer` starts Vite (`:5173`) automatically when `CI=true` (because `reuseExistingServer: !process.env.CI` is `false`). The `global-setup.ts` handles the DB seed and the `/health` + `/api/me` readiness gates.
|
||||
|
||||
```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: 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
|
||||
```
|
||||
|
||||
**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]
|
||||
|
||||
### Pattern 6: Docker Image Publish
|
||||
|
||||
```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
|
||||
|
||||
- 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: 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.
|
||||
|
||||
**Why not `docker/login-action`:** `--password-stdin` via a direct `docker login` step is simpler to verify on a self-hosted runner and avoids a dependency on the action resolving. The action is an option but the shell form is safer as a first iteration.
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
|
||||
- **`mysqladmin ping` in MariaDB 11 health check:** The `mysqladmin` binary is not in the `mariadb:11` image. Use `healthcheck.sh --connect --innodb_initialized`. [CONFIRMED: mage-os/github-actions issue]
|
||||
- **`drizzle-kit push` in CI:** Documented as unsafe on MariaDB — emits false destructive diff that TRUNCATEs tables. Always use `generate` + `migrate`. [CONFIRMED: project memory]
|
||||
- **`secrets.GITHUB_TOKEN` for Gitea registry push:** Returns `unauthorized: reqPackageAccess`. Use a PAT. [CONFIRMED: Gitea forum]
|
||||
- **`actions/upload-artifact@v4` natively on Gitea:** Fails with GHES detection. Use `ChristopherHX/gitea-upload-artifact@v4`. [CONFIRMED: Gitea issue #31256]
|
||||
- **`reporter: 'github'` assumed to render in Gitea:** Gitea does not implement the GitHub workflow-command protocol for annotations. The reporter setting in `playwright.config.ts` currently hardcodes `'github'` when `CI=true`. The planner must add a step that overrides reporter to `['list', 'html']` OR passes `--reporter=list` to the `playwright test` invocation. [ASSUMED — verify in probe]
|
||||
- **Starting API with `pnpm dev:api` without building first:** `pnpm dev:api` is `pnpm --filter @familysync/api dev` = `node --watch dist/index.js`, which requires `dist/` to exist. In CI, `dist/` does not exist until `pnpm --filter @familysync/api build` (`tsc`) runs. Build first.
|
||||
- **`actions/cache` without confirming it works:** The cache action has a known networking issue in act_runner Docker mode — the cache server runs in the runner container but the job container is on a different network, causing socket hang-up. Do not assume cache works; probe first and make it optional.
|
||||
- **`node-cron` in the API background process:** Not applicable to CI (short-lived process), but confirming the API uses `setInterval` (fixed in project) — no concern for CI.
|
||||
|
||||
---
|
||||
|
||||
## Dev-Stack Bring-Up for the Harness Job
|
||||
|
||||
The orchestration order is critical. All of the following must be sequential within the harness job (not parallelizable):
|
||||
|
||||
```
|
||||
1. MariaDB service container starts (via `services:` or `docker run -d` step)
|
||||
└── Wait: options health-check (Docker mode) OR explicit loop (host mode)
|
||||
Target: `healthcheck.sh --connect --innodb_initialized`
|
||||
Timeout: up to 90s (MariaDB 11 init is slower than 10)
|
||||
|
||||
2. pnpm install (workspace)
|
||||
|
||||
3. drizzle-kit migrate (DB_HOST = mariadb or 127.0.0.1 per runner mode)
|
||||
|
||||
4. pnpm --filter @familysync/api build (produces dist/index.js)
|
||||
|
||||
5. Start API background process:
|
||||
NODE_ENV=development DEV_AUTH_BYPASS=true node apps/api/dist/index.js &
|
||||
|
||||
6. Wait for :3000/health (curl retry loop, 60s timeout)
|
||||
This is SEPARATE from global-setup.ts's poll — global-setup runs AFTER
|
||||
Playwright starts, and it polls the Vite proxy. The step-level wait ensures
|
||||
the API is up before Playwright even attempts to start Vite.
|
||||
|
||||
7. Playwright invocation (pnpm test:e2e):
|
||||
a. Playwright webServer starts Vite :5173 (reuseExistingServer=false in CI)
|
||||
b. global-setup.ts polls baseURL/health (proxied to :3000) — already up from step 6
|
||||
c. global-setup.ts gates /api/me for DEV_AUTH_BYPASS confirmation
|
||||
d. global-setup.ts seeds DB via mysql2 direct connection (DB_HOST, etc.)
|
||||
e. Specs run against both iPhone 14/WebKit and Pixel 7/Chromium
|
||||
|
||||
8. On failure: upload test-results/ via gitea-upload-artifact
|
||||
```
|
||||
|
||||
**Note on `PLAYWRIGHT_BASE_URL`:** Set to `http://localhost:5173`. The Vite dev server proxies `/health`, `/api`, `/callback` → `http://localhost:3000`. This is how `global-setup` reaches the API health endpoint through the Vite proxy URL.
|
||||
|
||||
**Note on `NODE_ENV`:** The `global-setup.ts` refuses to run if `NODE_ENV=production`. In CI, set `NODE_ENV=development` (or leave unset; the guard only blocks `production`). Do NOT set `NODE_ENV=test` — the API checks `NODE_ENV=development` for dev-bypass activation confirmation.
|
||||
|
||||
**Note on both MariaDB connections:** The API (via Drizzle/mysql2) and `global-setup.ts` (via mysql2 direct) both use the same `DB_HOST` / `DB_PORT` / `DB_USER` / `DB_PASSWORD` / `DB_NAME` env vars. Set them once at job level and they propagate to both.
|
||||
|
||||
---
|
||||
|
||||
## Docker Registry Push (CI-02)
|
||||
|
||||
### 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] |
|
||||
|
||||
### Image Tag Strategy (D-04)
|
||||
|
||||
| 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.
|
||||
|
||||
Short SHA: `${GITHUB_SHA:0:7}` — confirmed available in Gitea Actions. [CITED: forum.gitea.com]
|
||||
|
||||
### Dockerfile Build Context
|
||||
|
||||
The `apps/api/Dockerfile` must be built from the **repo root** (not `apps/api/`), as documented in the Dockerfile header:
|
||||
|
||||
```bash
|
||||
docker build --target production -f apps/api/Dockerfile .
|
||||
```
|
||||
|
||||
This is because the Dockerfile copies the pnpm workspace manifest and lockfile from the repo root. Building from `apps/api/` would fail.
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Service Containers Don't Start (Host Mode Runner)
|
||||
|
||||
**What goes wrong:** `services:` in the workflow YAML is silently ignored; MariaDB container never appears in `docker ps`. API integration tests fail with `ECONNREFUSED` to DB.
|
||||
|
||||
**Why it happens:** act_runner in host-executor mode does not implement service container lifecycle [CITED: github.com/nektos/act/issues/2711]. The act_runner runs jobs directly on the host OS and has no mechanism to start sidecar containers.
|
||||
|
||||
**How to avoid:** Probe P-03 detects the runner mode. If host mode: use `docker run -d mariadb:11` in a step instead of `services:`.
|
||||
|
||||
**Warning signs:** P-05 shows MariaDB container not in `docker ps`.
|
||||
|
||||
### Pitfall 2: MariaDB 11 Health Check With mysqladmin (Pitfall 11)
|
||||
|
||||
**What goes wrong:** `--health-cmd="mysqladmin ping"` in `options:` causes the health check to always fail; the job times out waiting for the service to become healthy.
|
||||
|
||||
**Why it happens:** `mysqladmin` binary was removed from the official `mariadb:11` Docker image.
|
||||
|
||||
**How to avoid:** Use `--health-cmd="healthcheck.sh --connect --innodb_initialized"` exclusively. [CONFIRMED: mariadb.com docs]
|
||||
|
||||
**Warning signs:** Job hangs at service startup; `docker inspect` shows container in `unhealthy` state.
|
||||
|
||||
### Pitfall 3: Drizzle-Kit Push in CI
|
||||
|
||||
**What goes wrong:** `drizzle-kit push` emits a destructive diff (TRUNCATEs tables) on populated MariaDB. The CI DB has just been seeded by `global-setup.ts`; running push afterwards would wipe it.
|
||||
|
||||
**Why it happens:** MariaDB metadata misread by Drizzle's mysql dialect (project memory: `drizzle-mariadb-push-unsafe`).
|
||||
|
||||
**How to avoid:** Always `drizzle-kit migrate` in CI (applies existing SQL migration files). Never `drizzle-kit push`.
|
||||
|
||||
### Pitfall 4: API Started Without Building First
|
||||
|
||||
**What goes wrong:** `node apps/api/dist/index.js` fails with `MODULE_NOT_FOUND` because `dist/` does not exist in CI.
|
||||
|
||||
**Why it happens:** `dist/` is gitignored; the repo checkout has no compiled output.
|
||||
|
||||
**How to avoid:** Always run `pnpm --filter @familysync/api build` (= `tsc`) before starting the API process.
|
||||
|
||||
### Pitfall 5: Reporter `'github'` Emits Invisible Annotations in Gitea (Pitfall from D-06)
|
||||
|
||||
**What goes wrong:** `playwright.config.ts` sets `reporter: 'github'` when `CI=true`. This emits `::error::` GitHub workflow commands, which Gitea Actions does not render as UI annotations. Test failures appear in raw log output only, with no visual callout in the PR.
|
||||
|
||||
**Why it happens:** Gitea Actions does not implement GitHub's workflow command annotation protocol.
|
||||
|
||||
**How to avoid:** The `CI` env var triggers the `'github'` reporter. Override with `--reporter=list,html` on the `playwright test` invocation in CI, OR modify the harness job step to set `PLAYWRIGHT_REPORTER=list` if Playwright honours that env var. The planner should add a `PLAYWRIGHT_REPORTER` override. Runner probe P-06 (effectively) confirms this.
|
||||
|
||||
**Warning signs:** PR shows no inline annotation for a test failure; only the raw job log shows the failure.
|
||||
|
||||
### Pitfall 6: `actions/upload-artifact@v4` GHES Detection
|
||||
|
||||
**What goes wrong:** Upload step fails with `Error: This version of upload-artifact is not supported. Only GHES version X.Y.Z and above is supported.`
|
||||
|
||||
**Why it happens:** Gitea is detected as GitHub Enterprise Server; `actions/upload-artifact@v4` has a version gate that rejects GHES below a certain version.
|
||||
|
||||
**How to avoid:** Use `https://github.com/ChristopherHX/gitea-upload-artifact@v4` instead. [CONFIRMED: Gitea issue #28853 + #31256]
|
||||
|
||||
### Pitfall 7: `actions/cache` Socket Hang-Up in Docker Mode
|
||||
|
||||
**What goes wrong:** Cache step hangs and eventually times out with `socket hang up`. This may only appear intermittently.
|
||||
|
||||
**Why it happens:** act_runner's cache server runs in the runner container; the job container is on a different Docker network and cannot reach the runner's cache server by its configured address. [CITED: docs.gitea.com/usage/actions/act-runner — cache section]
|
||||
|
||||
**How to avoid:** Probe P-09 tests this. If cache consistently fails, skip it — pnpm install without cache on a fast network takes ~30s. Accept it.
|
||||
|
||||
### Pitfall 8: `DEV_AUTH_BYPASS` Not Propagated to API Process
|
||||
|
||||
**What goes wrong:** API starts, `/health` returns 200, but `/api/me` returns 302 redirect to Authelia. `global-setup.ts`'s DEV_AUTH_BYPASS gate (WR-01) throws a clear error, but the root cause is that `DEV_AUTH_BYPASS=true` was not exported into the API background process environment.
|
||||
|
||||
**Why it happens:** If the env var is set at the step level but the `node` process is launched with `&` in a separate `run:` step, environment inheritance between steps is not guaranteed in all runner modes.
|
||||
|
||||
**How to avoid:** Pass `DEV_AUTH_BYPASS=true` inline on the same line as the `node` invocation (`DEV_AUTH_BYPASS=true node apps/api/dist/index.js &`) rather than relying on inherited step env.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
---
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
### 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` |
|
||||
|
||||
### 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 |
|
||||
|
||||
### Sampling Rate
|
||||
|
||||
- **Per task commit (Wave 0):** Run runner-probe workflow manually on branch; check Gitea Actions logs
|
||||
- **Per wave:** Confirm all jobs in that wave pass on a test PR
|
||||
- **Phase gate:** Full CI green on a real PR before `/gsd-verify-work`
|
||||
|
||||
### Wave 0 Gaps
|
||||
|
||||
- [ ] `.gitea/workflows/runner-probe.yml` — runner probe workflow (new file; Wave 0 task)
|
||||
- [ ] `.gitea/workflows/ci.yml` — main CI workflow (new file; Waves 1-2)
|
||||
|
||||
---
|
||||
|
||||
## Security Domain
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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` |
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Runner mode: Docker vs host?**
|
||||
- What we know: 0 runners are currently registered; no configuration is visible from outside
|
||||
- What's unclear: Whether the Unraid act_runner is/will be configured with Docker mode (service containers work) or host mode (service containers don't work)
|
||||
- Recommendation: Runner-probe task P-03 answers this definitively; plan must handle both branches
|
||||
|
||||
2. **Unraid Docker socket access from act_runner container?**
|
||||
- What we know: act_runner typically mounts `/var/run/docker.sock` to spawn job containers
|
||||
- What's unclear: Whether the Unraid act_runner installation (likely via Unraid Community Applications template) has the socket mount configured
|
||||
- Recommendation: Probe P-04 (`docker info`) answers this
|
||||
|
||||
3. **`actions/cache` networking on this runner?**
|
||||
- What we know: Known issue with act_runner's cache server networking in Docker mode
|
||||
- What's unclear: Whether the Gitea 1.26.2 + current act_runner release has fixed this
|
||||
- Recommendation: Probe P-09; design the cache step as `continue-on-error: true` or skip entirely
|
||||
|
||||
4. **Playwright `reporter: 'github'` in Gitea — truly invisible?**
|
||||
- What we know: Gitea does not document GitHub workflow command support
|
||||
- What's unclear: Whether Gitea 1.26.2 partially supports `::error::` annotation commands
|
||||
- Recommendation: Probe P-11 (upload artifact test) can also test reporter output; plan to override reporter to `['list', 'html']` as default
|
||||
|
||||
5. **Milestone string automation — read from PROJECT.md or hardcode?**
|
||||
- What we know: `PROJECT.md` says "Current Milestone: v1.1"; D-04 says "read from PROJECT.md if avoidable"
|
||||
- What's unclear: Whether the executor wants a `grep` step to extract `v1.1` dynamically
|
||||
- Recommendation: Hardcode `v1.1` as a workflow-level env var (`MILESTONE: v1.1`) for Wave 2; update it manually at milestone boundaries. Simpler than parsing.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- [MariaDB healthcheck.sh docs](https://mariadb.com/docs/server/server-management/automated-mariadb-deployment-and-administration/docker-and-mariadb/using-healthcheck-sh) — `--connect --innodb_initialized` options
|
||||
- [ChristopherHX/gitea-upload-artifact README](https://github.com/ChristopherHX/gitea-upload-artifact/blob/main/README.md) — Gitea-compatible upload-artifact v4 fork
|
||||
- [Gitea issue #31256: upload-artifact@v4 not available](https://github.com/go-gitea/gitea/issues/31256) — confirmed GHES detection block
|
||||
- [GitHub Actions: Communicating with service containers](https://docs.github.com/actions/tutorials/communicating-with-docker-service-containers) — host-mode vs container-mode networking semantics
|
||||
- [nektos/act issue #2711: service containers in host mode](https://github.com/nektos/act/issues/2711) — host executor does NOT support service containers
|
||||
- [Gitea forum: proper container registry procedure](https://forum.gitea.com/t/proper-container-registry-procedure/8987) — GITHUB_TOKEN fails; PAT required
|
||||
- [Gitea forum: GITHUB_SHA in Gitea Actions](https://forum.gitea.com/t/using-github-sha-or-gitea-sha-in-gitea-actions/7800) — GITHUB_SHA confirmed, ${hash::10} syntax confirmed
|
||||
- [mage-os issue: mysqladmin removed from mariadb:11](https://github.com/mage-os/github-actions/issues/365) — confirmed mysqladmin absent from mariadb:11 image
|
||||
- [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
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
- Registry push / PAT auth: HIGH — confirmed in Gitea docs and forum
|
||||
- `actions/upload-artifact` block on Gitea: HIGH — confirmed in Gitea issue tracker
|
||||
- `actions/cache` networking: MEDIUM — known issue; unclear if fixed in current act_runner
|
||||
- Playwright CI: HIGH — official Playwright docs are clear
|
||||
- Short SHA syntax: HIGH — confirmed in Gitea forum
|
||||
|
||||
**Research date:** 2026-06-11
|
||||
**Valid until:** 2026-09-11 (stable CI/tooling area; 90 days)
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
phase: 08-gitea-ci
|
||||
reviewed: 2026-06-11T00:00:00Z
|
||||
depth: deep
|
||||
files_reviewed: 3
|
||||
files_reviewed_list:
|
||||
- .gitea/workflows/ci.yml
|
||||
- .gitea/workflows/publish.yml
|
||||
- apps/api/src/db/migrations/0000_baseline.sql
|
||||
findings:
|
||||
critical: 0
|
||||
warning: 5
|
||||
info: 2
|
||||
total: 7
|
||||
status: fixes_applied
|
||||
fix_summary:
|
||||
fixed: [WR-02, WR-03, WR-04, WR-05, IN-01]
|
||||
deferred: [IN-02] # ESLint gate is Phase 13's deliverable
|
||||
fixed_at: 2026-06-11
|
||||
---
|
||||
|
||||
# Phase 8: Code Review Report (Re-Review, Post-Split)
|
||||
|
||||
**Reviewed:** 2026-06-11
|
||||
**Depth:** deep
|
||||
**Files Reviewed:** 3
|
||||
**Status:** issues_found
|
||||
|
||||
## Summary
|
||||
|
||||
Re-review of Phase 8 (Gitea CI) against the post-split state: `publish` is now its own
|
||||
`publish.yml` triggered on `push: branches: [main]`, and `ci.yml` holds the three PR-gated
|
||||
jobs (`fast-checks`, `api`, `harness`).
|
||||
|
||||
**Resolved since prior review:**
|
||||
- **WR-01 (publish ran without test gating / orphaned pending status)** — RESOLVED. Publish
|
||||
is split into `publish.yml`, triggered only on `push` to `main`. PR jobs gate the merge via
|
||||
required status checks + branch protection (direct/force push blocked). No `needs:` is needed
|
||||
because publish never shares a workflow invocation with the test jobs. Architecture confirmed
|
||||
intentional; not re-raised.
|
||||
|
||||
**Still open (re-located to the split files):**
|
||||
- WR-02 (harness HTML report built then discarded) — still open, now in `ci.yml`.
|
||||
- WR-03 (unguarded `${GITHUB_SHA:0:7}` → malformed tag) — still open, now in `publish.yml`.
|
||||
- WR-04 (non-atomic two-push, `:latest` before immutable tag) — still open, now in `publish.yml`.
|
||||
- IN-01 (>3072-byte UNIQUE indexes, MariaDB-only) — still open in `0000_baseline.sql`.
|
||||
|
||||
**New findings from the deep pass:**
|
||||
- WR-05 (secret interpolated into `run:` script body via `echo` instead of `env:`) — new.
|
||||
- IN-02 (`lint` step is a documented no-op that masks lint failures) — new.
|
||||
|
||||
No Critical findings. The publish flow has no test `needs:` by design (gated by branch
|
||||
protection), so it is not flagged. The dominant theme is robustness/observability gaps in the
|
||||
publish + harness steps and MariaDB-specific schema portability.
|
||||
|
||||
## Narrative Findings (AI reviewer)
|
||||
|
||||
## Warnings
|
||||
|
||||
### WR-02: Harness HTML report is generated but never uploaded — FIXED (commit 44a9c30)
|
||||
|
||||
> Resolution: extended the existing `ChristopherHX/gitea-upload-artifact@v4` failure step's
|
||||
> `path:` to a multi-line list uploading both `apps/pwa/test-results/` and
|
||||
> `apps/pwa/playwright-report/`. Reporter unchanged; step still `if: failure()`.
|
||||
|
||||
|
||||
**File:** `.gitea/workflows/ci.yml:293` (report generation) and `:301-307` (upload step)
|
||||
**Issue:** The harness runs Playwright with `--reporter=list,html`. The `html` reporter writes
|
||||
its output to `apps/pwa/playwright-report/` (Playwright's default `outputFolder`, not overridden
|
||||
in `playwright.config.ts`). The failure-artifact upload step (`if: failure()`) only uploads
|
||||
`path: apps/pwa/test-results/`. The HTML report — the most useful artifact for triaging a remote
|
||||
CI failure — is built on every run and then discarded when the runner is torn down. `test-results/`
|
||||
contains traces/screenshots/videos but not the navigable HTML report.
|
||||
**Fix:** Either drop `html` from the reporter (saves build time if it is genuinely unwanted), or
|
||||
upload it. Preferred — add the report to the existing upload, or a second upload step:
|
||||
```yaml
|
||||
- name: Upload Playwright HTML report
|
||||
if: failure()
|
||||
uses: https://github.com/ChristopherHX/gitea-upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report-${{ github.run_id }}
|
||||
path: apps/pwa/playwright-report/
|
||||
retention-days: 14
|
||||
```
|
||||
(Or set `path: |` with both `apps/pwa/test-results/` and `apps/pwa/playwright-report/` on the
|
||||
existing step.)
|
||||
|
||||
### WR-03: Unguarded `${GITHUB_SHA:0:7}` can emit a malformed image tag — FIXED (commit 6bcf867)
|
||||
|
||||
> Resolution: added `set -euo pipefail` + a `: "${GITHUB_SHA:?...}"` fail-closed guard
|
||||
> before `SHORT_SHA=${GITHUB_SHA:0:7}`, so an empty SHA aborts the step instead of
|
||||
> producing `:v1.1-`.
|
||||
|
||||
|
||||
**File:** `.gitea/workflows/publish.yml:44`
|
||||
**Issue:** `SHORT_SHA=${GITHUB_SHA:0:7}` has no guard for an empty/unset `GITHUB_SHA`. If the
|
||||
runner does not populate `GITHUB_SHA` (Gitea Actions env parity is not guaranteed across runner
|
||||
versions; the comment only asserts it via probe P-13, not a runtime check), `SHORT_SHA` becomes
|
||||
empty and the immutable tag silently degrades to `git.bergerhouse.net/luckberg/familysync-api:v1.1-`
|
||||
— a valid-but-wrong tag that overwrites the milestone pointer and destroys rollback traceability.
|
||||
Because the default Actions shell runs with `pipefail`/`-e` but NOT `-u`, the empty expansion does
|
||||
not error; it proceeds.
|
||||
**Fix:** Fail closed when the SHA is missing:
|
||||
```bash
|
||||
set -euo pipefail
|
||||
if [ -z "${GITHUB_SHA:-}" ]; then
|
||||
echo "GITHUB_SHA is empty — cannot compute immutable tag" >&2
|
||||
exit 1
|
||||
fi
|
||||
SHORT_SHA=${GITHUB_SHA:0:7}
|
||||
```
|
||||
|
||||
### WR-04: Build-and-push is non-atomic and pushes `:latest` before the immutable tag — FIXED (commit 4001cd5)
|
||||
|
||||
> Resolution: reordered the pushes so the immutable `:<milestone>-<sha>` tag goes first,
|
||||
> `:latest` second; added `set -euo pipefail` so the step stops on the first failed push.
|
||||
|
||||
|
||||
**File:** `.gitea/workflows/publish.yml:64-72`
|
||||
**Issue:** The step runs `docker build` then two sequential `docker push` calls. `:latest` is
|
||||
pushed first (line 71), then `:<milestone>-<sha>` (line 72). If the second push fails (registry
|
||||
hiccup, auth expiry, network), `:latest` already moved to the new image while the immutable,
|
||||
rollback-traceable tag was never published — the exact tag operators would reach for to roll back
|
||||
does not exist, but `:latest` already advanced. Although the Actions default shell injects `-e`
|
||||
(so a failed first command aborts the step), ordering still means a partial-failure window leaves
|
||||
`:latest` ahead of the immutable record. Push order should be immutable-first.
|
||||
**Fix:** Push the immutable tag first, then `:latest`, and make the shell strict explicitly:
|
||||
```bash
|
||||
set -euo pipefail
|
||||
docker build --target production \
|
||||
-f apps/api/Dockerfile \
|
||||
-t ${{ steps.tags.outputs.latest }} \
|
||||
-t ${{ steps.tags.outputs.sha_tag }} \
|
||||
.
|
||||
docker push ${{ steps.tags.outputs.sha_tag }} # immutable first
|
||||
docker push ${{ steps.tags.outputs.latest }} # move pointer only after immutable lands
|
||||
```
|
||||
|
||||
### WR-05: Registry PAT is interpolated into the `run:` script body instead of passed via `env:` — FIXED (commit 58861d9)
|
||||
|
||||
> Resolution: bound `REGISTRY_PAT` through step-level `env:` and replaced the
|
||||
> `echo "${{ secrets.REGISTRY_PAT }}" | ...` pipe with `printf '%s' "$REGISTRY_PAT" | ...`.
|
||||
> Existing PAT-via-stdin and REGISTRY_PAT-naming comments preserved.
|
||||
|
||||
|
||||
**File:** `.gitea/workflows/publish.yml:55-60`
|
||||
**Issue:** `echo "${{ secrets.REGISTRY_PAT }}" | docker login ... --password-stdin` interpolates
|
||||
the secret into the shell script text at template-expansion time. Two problems:
|
||||
(1) Robustness — if the PAT ever contains a shell-significant character or a trailing newline,
|
||||
`echo` may mangle or split it (`echo` is not safe for arbitrary strings; `printf %s` is). A
|
||||
mangled-but-nonempty password produces a confusing `unauthorized` rather than a clear failure.
|
||||
(2) Surface — template-substituting a secret into the script body is the documented anti-pattern
|
||||
versus binding it through `env:` (the script then references `$REGISTRY_PAT`), which keeps the
|
||||
secret out of the rendered command line / step definition and is the recommended pattern for
|
||||
Actions-compatible runners. Gitea's log scrubber masks it either way, so this is a robustness/
|
||||
hardening WARNING, not a leak.
|
||||
**Fix:**
|
||||
```yaml
|
||||
- name: Docker login
|
||||
env:
|
||||
REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
printf '%s' "$REGISTRY_PAT" | docker login git.bergerhouse.net \
|
||||
--username luckberg --password-stdin
|
||||
```
|
||||
|
||||
## Info
|
||||
|
||||
### IN-01: Two UNIQUE constraints exceed the 3072-byte index limit (MariaDB-only) — FIXED, docs-only (commit bf09110)
|
||||
|
||||
> Resolution: added explanatory SQL comments above `uniq_calendar_user_url` and
|
||||
> `uniq_push_endpoint` in the already-applied `0000_baseline.sql`, documenting the
|
||||
> over-length-index dependency on MariaDB 11's long-unique HASH behavior. NO schema,
|
||||
> column, or index altered (migration is live on main/production).
|
||||
|
||||
|
||||
**File:** `apps/api/src/db/migrations/0000_baseline.sql:100` (`uniq_push_endpoint`) and `:48`
|
||||
(`uniq_calendar_user_url`)
|
||||
**Issue:** With the default `utf8mb4` charset (4 bytes/char):
|
||||
- `uniq_push_endpoint UNIQUE(endpoint)` where `endpoint varchar(2048)` → 2048 × 4 = 8192 bytes.
|
||||
- `uniq_calendar_user_url UNIQUE(user_id, url)` where `url varchar(1024)` → 4 + 1024 × 4 = 4100 bytes.
|
||||
Both exceed InnoDB's 3072-byte index-key limit. They succeed on MariaDB 11.x (the CI service image
|
||||
`mariadb:11`) because MariaDB silently builds over-length UNIQUE constraints as long-unique HASH
|
||||
indexes. The same DDL fails hard on MySQL 8 and on MariaDB configured with
|
||||
`innodb_large_prefix` semantics disabled or a stricter SQL mode. This is engine-pinned, not a bug
|
||||
on the current target — the project hard-constrains to MariaDB (no PostgreSQL/MySQL) — so it is INFO.
|
||||
**Fix:** No change required while MariaDB is the only target. If portability is ever wanted, either
|
||||
(a) shorten the columns (e.g. `url varchar(768)`, `endpoint` hashed to a `char(64)` digest column
|
||||
with the UNIQUE on the digest), or (b) add an explicit comment in `schema.ts` documenting the
|
||||
MariaDB long-unique-HASH dependency so a future MySQL migration is not silently broken. Schema.ts
|
||||
already carries a CR-02 note on the `endpoint(2048)` width; extend it to record the index-limit
|
||||
caveat.
|
||||
|
||||
### IN-02: `Lint` step is a no-op that will mask real lint failures once ESLint is wired — DEFERRED to Phase 13
|
||||
|
||||
> Not fixed this phase. Wiring a real ESLint gate (`pnpm -r --if-present lint`) is Phase 13's
|
||||
> deliverable. `ci.yml`'s lint step left untouched intentionally.
|
||||
|
||||
|
||||
**File:** `.gitea/workflows/ci.yml:31-32` (`pnpm lint`)
|
||||
**Issue:** Per the in-file comment, no package defines a `lint` script, so `pnpm lint` (root) prints
|
||||
`ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT` but exits 0 — the step is green regardless. This is acceptable
|
||||
for the current phase (lint wiring is explicitly out of scope), but it is a latent trap: when a
|
||||
`lint` script is later added to one package, `pnpm lint` at the root still will not run it unless
|
||||
the invocation is `pnpm -r lint`, and even then `--if-present` semantics differ. The step gives a
|
||||
false sense that linting is enforced.
|
||||
**Fix:** When lint is wired, switch to `pnpm -r --if-present lint` (runs lint only in packages that
|
||||
define it, fails the job on real lint errors) and remove the no-op comment. No action this phase;
|
||||
tracked so the green-but-empty step is not mistaken for working lint enforcement.
|
||||
|
||||
---
|
||||
|
||||
_Reviewed: 2026-06-11_
|
||||
_Reviewer: Claude (gsd-code-reviewer)_
|
||||
_Depth: deep_
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
phase: 8
|
||||
slug: gitea-ci
|
||||
status: planned
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: false
|
||||
created: 2026-06-11
|
||||
---
|
||||
|
||||
# Phase 8 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
| ---------------------- | -------------------------------------------------------------------- |
|
||||
| **Framework** | Vitest 4.1.x (API + PWA unit), @playwright/test 1.60.0 (E2E harness) |
|
||||
| **Config file** | `apps/api/vitest.config.ts`, `apps/pwa/playwright.config.ts` |
|
||||
| **Quick run command** | `pnpm lint && pnpm typecheck` |
|
||||
| **Full suite command** | `pnpm test` then `pnpm test:e2e` (CI=true, dev stack up) |
|
||||
| **Estimated runtime** | unit ~tens of seconds; harness several minutes (2 device profiles) |
|
||||
|
||||
**Note:** Phase 8 delivers CI plumbing (`.gitea/workflows/*.yml`). The "tests" for this phase are the CI workflow runs themselves — validation is observed by triggering the workflow on a branch/PR and reading Gitea Actions logs, not by a local unit-test file per task.
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** YAML lint / `act_runner` dry-validate where possible; push branch and observe the probe/CI run in Gitea Actions
|
||||
- **After every plan wave:** Confirm all jobs in that wave pass on a test PR (W0 probe green → W1 fast-checks + integration green → W2 harness + publish green)
|
||||
- **Before `/gsd-verify-work`:** Full CI green on a real PR targeting `main`
|
||||
- **Max feedback latency:** one CI run (minutes), bounded by the harness job
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
| ------------------------------------ | ----- | ---- | ----------- | -------------- | ------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------- | ---------- |
|
||||
| P01-T1 register runner + PAT | 08-01 | 1 | CI-01/CI-02 | T-08-PAT | runner online; PAT secret stored, never in repo | operator/manual | Gitea Actions runner list + repo secret present | ❌ operator | ⬜ pending |
|
||||
| P01-T2 author runner-probe.yml | 08-01 | 1 | CI-01 | T-08-01 | probe is non-destructive; touches no secret | grep gate | `grep` healthcheck.sh + gitea-upload fork; `! grep` mysqladmin/upload-artifact@v4 | ❌ W0 | ⬜ pending |
|
||||
| P01-T3 run probe, record forks | 08-01 | 1 | CI-01 | T-08-01 | runner mode / cache / WebKit / artifact answers captured | workflow run | observe runner-probe run in Gitea Actions | ❌ W0 | ⬜ pending |
|
||||
| P02-T1 fast-checks job | 08-02 | 2 | CI-01 | — | lint+typecheck+PWA-unit gate the PR | grep gate + workflow run | `grep` node-pin/typecheck/pwa test; PR run green | ✅ scripts | ⬜ pending |
|
||||
| P02-T2 api job (MariaDB+migrate) | 08-02 | 2 | CI-01 | T-08-03/04/05 | DB-backed API tests pass cold; migrate-not-push; throwaway creds | grep gate + workflow run | `grep` healthcheck.sh+db:migrate; `! grep` mysqladmin/db:push; cold PR run green | ✅ scripts | ⬜ pending |
|
||||
| P02-T3 verify PR jobs | 08-02 | 2 | CI-01 | — | both jobs run parallel, api green cold | workflow run | observe fast-checks ∥ api on a PR | ❌ W1 | ⬜ pending |
|
||||
| P03-T1 harness stack bring-up | 08-03 | 3 | CI-01 | T-08-06/07/08 | API bg w/ inline DEV_AUTH_BYPASS; :3000 readiness before Playwright | grep gate + workflow run | `grep` inline-bypass+:3000/health+db:migrate; `! grep` db:push | ✅ Phase 7 specs | ⬜ pending |
|
||||
| P03-T2 playwright run + artifacts | 08-03 | 3 | CI-01 | T-08-06 | both profiles run; traces upload on failure; no spec edits | grep gate + workflow run | `grep` webkit+chromium+base-url+gitea-upload; `git diff --quiet` harness files | ✅ Phase 7 specs | ⬜ pending |
|
||||
| P03-T3 verify harness on PR | 08-03 | 3 | CI-01 | — | iphone+pixel green vs CI dev stack; artifact confirmed | workflow run | observe harness job on a PR | ❌ W2 | ⬜ pending |
|
||||
| P04-T1 publish job | 08-04 | 4 | CI-02 | T-08-PAT/09/10 | --password-stdin only; both tags; production target from root; no bypass | grep gate | `grep` --password-stdin+target production+both tags; `! grep` -p/--password/DEV_AUTH_BYPASS-in-publish | ❌ W2 | ⬜ pending |
|
||||
| P04-T2 merge, audit log, verify tags | 08-04 | 4 | CI-02 | T-08-PAT | no plaintext PAT in log; :latest + :v1.1-<sha> in registry | workflow run + log audit | merge → audit publish log + check Packages | ❌ W2 | ⬜ pending |
|
||||
|
||||
_Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky — planner expands one row per task._
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `.gitea/workflows/runner-probe.yml` — runner-probe workflow (new file; Wave 0 task). Must answer: runner mode (Docker vs host), Docker socket access, `actions/*` resolution, Node 22 / pnpm availability, internet access, WebKit dep installability.
|
||||
- [ ] `.gitea/workflows/ci.yml` — main CI workflow scaffolding (new file; Waves 1–2).
|
||||
|
||||
_Existing unit/integration/E2E infrastructure (Vitest + Playwright) is reused unchanged; no new local test framework is installed._
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
| ------------------------------------ | ----------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| Runner registered on Unraid | CI-01/CI-02 | Operator infra action outside the repo; 0 runners currently registered | Install/register `act_runner` on Unraid against `git.bergerhouse.net`; confirm it appears in Gitea Actions runners list |
|
||||
| Gitea registry PAT created | CI-02 | Secret creation is an operator action; cannot be scripted in-repo | Create PAT with `write:package`; store as repo secret (e.g. `GITEA_REGISTRY_PAT`); confirm publish job authenticates |
|
||||
| PR merge actually blocked on failure | CI-01 | Requires branch-protection "required status checks" config in Gitea | Configure required checks on `main`; open a failing PR; confirm merge button is blocked |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [x] All tasks have an observable CI-run verification or Wave 0 dependency
|
||||
- [x] Sampling continuity: every wave has a green-gate before the next (W0 probe → W1 PR jobs → W2 harness → W3 publish, each gated by a checkpoint)
|
||||
- [x] Wave 0 covers all MISSING references (runner-probe answers all unknowns)
|
||||
- [x] No watch-mode flags
|
||||
- [x] `nyquist_compliant: true` set in frontmatter (after planner expands the map)
|
||||
|
||||
**Approval:** planned 2026-06-11 — map expanded, nyquist_compliant=true
|
||||
@@ -0,0 +1,50 @@
|
||||
# Phase 8: Gitea CI — Verification
|
||||
|
||||
**Status: PASSED**
|
||||
**Verified:** 2026-06-11
|
||||
**Evidence basis:** Gitea Actions run log + Gitea Packages API inspection by operator
|
||||
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
Every PR to `main` runs a full regression that gates the merge — lint, typecheck, unit, API-integration against a MariaDB service container, and the Phase 7 mobile Playwright harness as a UI-regression step — and a merge to `main` builds and publishes the API Docker image. All on the existing self-hosted Gitea Actions runner.
|
||||
|
||||
## 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). |
|
||||
|
||||
## 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). |
|
||||
| 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)
|
||||
|
||||
1. **squashed migration baseline** (commit c0f892c) — single `0000_baseline.sql` replaces the incremental migration history; eliminates the false drizzle-kit destructive diff that aborted the first cold CI run.
|
||||
2. **API-reap + ipv4first** — `pkill -f "tsx watch"` reap guard + `--dns-result-order=ipv4first` flag ensures the API dev server binds on 127.0.0.1 (not ::1), matching the harness readiness poll.
|
||||
3. **dev-user seed** — global-setup inserts the DEV_AUTH_BYPASS user (id=1) before Playwright launches, so the authed bypass session is valid on first spec.
|
||||
4. **direct test:e2e** — `pnpm --filter @familysync/pwa test:e2e` called directly (not via Makefile) to avoid shell-expansion issues in the runner.
|
||||
5. **REGISTRY_PAT naming** (commit 73eecf7) — Gitea runner silently drops secrets whose names begin with `GITEA_`; secret renamed from `GITEA_REGISTRY_PAT` to `REGISTRY_PAT`.
|
||||
|
||||
## 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** |
|
||||
|
||||
## Phase 8 Outcome
|
||||
|
||||
Phase 8 is complete. The Gitea CI pipeline is fully operational on the self-hosted runner. Future merges to `main` will automatically run the full regression gate and publish a new image — no manual steps required.
|
||||
Reference in New Issue
Block a user