# 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)
