From b1851f475edea5660c2d56c37dccbb2e127dad35 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 16:17:53 -0400 Subject: [PATCH] =?UTF-8?q?docs(08):=20SUMMARY=20+=20VERIFICATION=20?= =?UTF-8?q?=E2=80=94=20phase=208=20complete,=20CI-01=20+=20CI-02=20deliver?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 08-04-SUMMARY.md: publish job verified green (run #14), both tags pushed, security audit pass (PAT masked, --password-stdin), REGISTRY_PAT naming note - 08-VERIFICATION.md: all six phase-8 ROADMAP criteria passed with evidence table - REQUIREMENTS.md: CI-01 + CI-02 marked complete (checkboxes + traceability table) - ROADMAP.md: phase 8 marked [x] complete (2026-06-11), 08-04 plan ticked - STATE.md: phase complete, D-PAT-NAMING decision recorded, operator next steps updated --- .planning/REQUIREMENTS.md | 8 +- .planning/ROADMAP.md | 6 +- .planning/STATE.md | 33 +++--- .planning/phases/08-gitea-ci/08-04-SUMMARY.md | 111 ++++++++++++++++++ .../phases/08-gitea-ci/08-VERIFICATION.md | 50 ++++++++ 5 files changed, 185 insertions(+), 23 deletions(-) create mode 100644 .planning/phases/08-gitea-ci/08-04-SUMMARY.md create mode 100644 .planning/phases/08-gitea-ci/08-VERIFICATION.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index fc9ba16..141a477 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -39,8 +39,8 @@ Each requirement maps to exactly one roadmap phase (see Traceability). ### CI — Gitea continuous integration -- [ ] **CI-01**: Every pull request 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 as a UI-regression step (CI brings up the API + PWA dev servers + MariaDB service container with `DEV_AUTH_BYPASS` in the runner and runs the harness specs headlessly against the authed PWA)** — and the result gates the merge. -- [ ] **CI-02**: On merge to `main`, the API Docker image is built and published to the Gitea container registry. +- [x] **CI-01**: Every pull request 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 as a UI-regression step (CI brings up the API + PWA dev servers + MariaDB service container with `DEV_AUTH_BYPASS` in the runner and runs the harness specs headlessly against the authed PWA)** — and the result gates the merge. +- [x] **CI-02**: On merge to `main`, the API Docker image is built and published to the Gitea container registry. ### Test — Mobile-emulated authed browser harness @@ -73,8 +73,8 @@ Maps each REQ-ID to its phase. v1.1 phases continue v1.0 numbering (v1.0 ended a |--------|-------|--------| | TEST-01 | Phase 7 (Mobile Test Harness) | Complete | | TEST-02 | Phase 7 (Mobile Test Harness) | Complete | -| CI-01 | Phase 8 (Gitea CI) | In progress | -| CI-02 | Phase 8 (Gitea CI) | Pending | +| CI-01 | Phase 8 (Gitea CI) | Complete | +| CI-02 | Phase 8 (Gitea CI) | Complete | | CAL-15 | Phase 9 (Faster Write-Back) | Pending | | ADMIN-01 | Phase 10 (Admin Role & Settings) | Pending | | ADMIN-02 | Phase 10 (Admin Role & Settings) | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d23dec0..1bf7b27 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -26,7 +26,7 @@ Full phase detail archived in [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROA Make FamilySync configurable, administrable, and maintainable for real multi-member use — without hand-editing env files or the database. The new critical path runs **mobile test harness → Gitea CI** (CI consumes the harness specs for UI regression), and the **admin role → reminders / setup wizard** chain (a single `/api/admin` + `/api/setup` route surface carrying the v1.1 DB migration). Faster write-back is a fully independent track. - [x] **Phase 7: Mobile Test Harness** - Mobile-emulated, authenticated PWA browser harness so the assistant (and CI) can catch mobile-only defects (completed 2026-06-11) -- [ ] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge +- [x] **Phase 8: Gitea CI** - Full regression on PR to main (lint/typecheck/unit/API-integration vs a MariaDB service container **+ the Phase 7 mobile harness as a UI-regression step against a CI-hosted dev stack**) + Docker image publish on merge (completed 2026-06-11) - [ ] **Phase 9: Faster Write-Back** - Event-driven outbox drain so edits land in ~1-2s instead of ~15s, preserving every outbox durability guarantee - [ ] **Phase 10: Admin Role & Settings** - DB foundation (is_admin / reminder_lead / app_config) + role-gated admin UI to rotate app passwords and designate the shared calendar - [ ] **Phase 11: Per-Event Reminders** - Reminder selector on the event form (incl. "None") serialized as VALARM, with a variable-lead scheduler that honors each event's choice @@ -109,7 +109,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem **Wave 4** *(blocked on Wave 3 completion)* -- [ ] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1- via --password-stdin) +- [x] 08-04-PLAN.md — ci.yml: publish job (build production image + push :latest + :v1.1- via --password-stdin) **UI hint**: yes @@ -217,7 +217,7 @@ Make FamilySync configurable, administrable, and maintainable for real multi-mem | 5. Web Push Notifications | v1.0 | 8/8 | Complete | 2026-06-10 | | 6. UX Polish | v1.0 | 6/6 | Complete | 2026-06-10 | | 7. Mobile Test Harness | v1.1 | 4/4 | Complete | 2026-06-11 | -| 8. Gitea CI | v1.1 | 3/4 | In Progress| | +| 8. Gitea CI | v1.1 | 4/4 | Complete | 2026-06-11 | | 9. Faster Write-Back | v1.1 | 0/? | Not started | - | | 10. Admin Role & Settings | v1.1 | 0/? | Not started | - | | 11. Per-Event Reminders | v1.1 | 0/? | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 80f24bb..daf7f10 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish -status: executing -stopped_at: 08-03 complete — advancing to 08-04 (Wave 4, publish job) -last_updated: "2026-06-11T20:00:00.000Z" -last_activity: "2026-06-11 -- 08-03 complete; harness job green on cold CI run (run #11): 58 specs passed across iphone/WebKit + pixel/Chromium in 1.6 min; 4 infrastructure fixes (API-reap, IPv4-first, dev-user seed, reporter forwarding); no Phase 7 harness file modified; advancing to 08-04" +status: phase-complete +stopped_at: Phase 08 complete — all 4 plans executed, CI-01 + CI-02 delivered, publish job verified green (run #14) +last_updated: "2026-06-11T22:00:00.000Z" +last_activity: "2026-06-11 -- 08-04 complete; publish job green (run #14, merge commit 98acff8): both image tags pushed (familysync-api:latest + :v1.1-98acff8), PAT masked, --password-stdin confirmed. REGISTRY_PAT naming fix (73eecf7). Phase 8 (Gitea CI) complete — all 6 ROADMAP criteria met." progress: total_phases: 16 - completed_phases: 1 + completed_phases: 2 total_plans: 8 - completed_plans: 6 - percent: 6 + completed_plans: 8 + percent: 12 --- # Project State @@ -25,10 +25,10 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position -Phase: 08 (gitea-ci) — EXECUTING -Plan: 4 of 4 (08-04 next — Wave 4, publish job) -Status: Executing — 08-03 complete, harness green (58 specs, both profiles, cold CI run) -Last activity: 2026-06-11 -- 08-03 complete; harness job green on cold CI run (run #11): 58 specs passed across iphone/WebKit + pixel/Chromium in 1.6 min; 4 infrastructure fixes (API-reap, IPv4-first, dev-user seed, reporter forwarding); no Phase 7 harness file modified; advancing to 08-04 +Phase: 08 (gitea-ci) — COMPLETE +Plan: 4 of 4 (08-04 complete) +Status: Phase 08 complete — all 4 plans executed, CI-01 + CI-02 delivered +Last activity: 2026-06-11 -- 08-04 complete; publish job green (run #14): both tags pushed, PAT masked, REGISTRY_PAT naming fix applied. Phase 8 fully complete. ## Performance Metrics @@ -98,6 +98,7 @@ Recent decisions affecting current work: - D-PROBE-06 (2026-06-11, 08-01): ChristopherHX/gitea-upload-artifact@v4 works — MUST use this fork; actions/upload-artifact@v4 broken on Gitea. - D-PROBE-07 (2026-06-11, 08-01): ${GITHUB_SHA:0:7} produces 7 chars — D-04 publish tag expression valid. - D-PROBE-08 (2026-06-11, 08-01): GITEA_REGISTRY_PAT deferred to Plan 04; PAT not exercised in probe. +- D-PAT-NAMING (2026-06-11, 08-04): Gitea runner silently drops secrets with the `GITEA_` prefix (reserved namespace). Secret renamed from GITEA_REGISTRY_PAT → REGISTRY_PAT in both repo secret and ci.yml (commit 73eecf7). Use REGISTRY_PAT in any future registry operations. - CAL-08 RESOLVED → GO (Phase 1): per-member Fastmail app password reaches all of that account's calendars; no cross-account ACL needed. Unified view stands; no shared-only fallback. See CAL-08-DECISION.md. - D-14 (2026-06-04): Phase 1 Gate 2 (live Authelia/Pangolin) deferred. SSE-over-Pangolin smoke = hard gate before Phase 4; live AUTH smoke incl. iOS standalone-PWA folded into Phase 3. Phases 2–3 build behind a dev-auth bypass. Tracked in 01-HUMAN-UAT.md + docs/deployment.md. - D-15 (2026-06-04): Validate real topology via local Newt connector + test subdomain through Pangolin (Mode A), not an Unraid deploy; Unraid reserved for go-live. @@ -199,11 +200,11 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-11T20:00:00.000Z -Stopped at: 08-03 complete — advancing to 08-04 (Wave 4, publish job) -Resume file: .planning/phases/08-gitea-ci/08-04-PLAN.md +Last session: 2026-06-11T22:00:00.000Z +Stopped at: Phase 08 complete — CI-01 + CI-02 delivered, publish job verified, SUMMARY + VERIFICATION written +Resume file: None — start Phase 09 with /gsd-plan-phase 9 ## Operator Next Steps -- Review the reordered v1.1 roadmap in `.planning/ROADMAP.md` (Phases 7–12). -- Plan the first phase with `/gsd-plan-phase 7` (Mobile Test Harness — independent, goes first; Phase 8 CI runs its specs). Alternatives that can start in parallel: `/gsd-plan-phase 9` (Faster Write-Back — independent, lowest risk) or `/gsd-plan-phase 10` (Admin Role & Settings — carries the DB migration that Phases 11 & 12 depend on). +- **Phase 8 is complete.** CI pipeline is fully operational on the self-hosted Gitea runner. +- Next: `/gsd-plan-phase 9` (Faster Write-Back — fully independent, lowest risk) or `/gsd-plan-phase 10` (Admin Role & Settings — carries the v1.1 DB migration that Phases 11 & 12 depend on). These can run in parallel once planned. diff --git a/.planning/phases/08-gitea-ci/08-04-SUMMARY.md b/.planning/phases/08-gitea-ci/08-04-SUMMARY.md new file mode 100644 index 0000000..ec8b1ce --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-04-SUMMARY.md @@ -0,0 +1,111 @@ +--- +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 + :- (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-; 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-, 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` + `:-`) — same digest, two references. `:latest` is the moving pointer; `:-` 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* diff --git a/.planning/phases/08-gitea-ci/08-VERIFICATION.md b/.planning/phases/08-gitea-ci/08-VERIFICATION.md new file mode 100644 index 0000000..d4b665f --- /dev/null +++ b/.planning/phases/08-gitea-ci/08-VERIFICATION.md @@ -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-, 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. -- 2.54.0