---
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_
