docs: refresh CI + lint/format docs (Phase 8 + Phase 13) #9

Merged
luckberg merged 3 commits from gsd/docs-ci-lint-refresh into main 2026-06-12 08:54:18 -04:00
10 changed files with 22595 additions and 33421 deletions
+25
View File
@@ -462,3 +462,28 @@ Plans:
Plans:
- [ ] TBD (promote with /gsd-review-backlog when ready)
### Phase 999.17: CI — let doc-only PRs skip the slow api + harness jobs (BACKLOG)
**Goal:** [Captured for future planning] Make doc-only PRs to `main` mergeable without running the slow `harness` (Playwright e2e + dev-stack bring-up, ~5 min) and `api` (MariaDB integration) jobs, while keeping `fast-checks` (which must run on docs — Phase 13's `format:check` is `prettier --check .` and covers markdown; ~12 min).
**The footgun:** branch protection currently requires three contexts — `CI / fast-checks`, `CI / api`, `CI / harness`. The naive fix (`paths-ignore` on the workflow, or path-filtering `harness`) **deadlocks**: on a docs-only PR the required `harness`/`api` contexts never report, so the PR can never merge. A required check that never reports is worse than a slow one.
**Solution (Option A — aggregate gate):**
- Add a cheap `changes` detector job: `git diff --name-only base...HEAD`, set `code=true` unless every changed path matches `docs/` or `*.md`.
- Gate the heavy jobs: `api` and `harness` get `needs: changes` + `if: needs.changes.outputs.code == 'true'`.
- Add an always-running `gate` job: `needs: [fast-checks, api, harness]`, `if: always()`, passes when each dependency `result` is `success` OR `skipped`.
- Change Gitea branch protection to require `CI / fast-checks` + `CI / gate` (drop the direct `api`/`harness` requirements).
- Result: docs PR → `api`/`harness` skip, `gate` passes; code PR → all run, `gate` passes only if they succeed. No deadlock because `gate` always reports.
**Gitea caveat:** historically Gitea may not emit a commit-status for a `skipped` job — the always-running `gate` sidesteps that entirely (don't rely on marking `api`/`harness` themselves as skipped-but-required).
**Files:** `.gitea/workflows/ci.yml` + Gitea branch-protection settings.
**Context:** Surfaced 2026-06-12 while opening the docs-only PR #9, which had to run the full harness. Tags: ci, gitea, branch-protection, docs, performance.
**Requirements:** TBD
**Plans:** 0 plans
Plans:
- [ ] TBD (promote with /gsd-review-backlog when ready)
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+21927 -32077
View File
File diff suppressed because it is too large Load Diff
+17 -2
View File
@@ -90,9 +90,12 @@ docker-compose.dev.yml Dev overrides (bind-mount src/, expose DB/Redis ports)
| `pnpm dev:api` | Start API in watch mode (`dist/` must be built first) |
| `pnpm dev:pwa` | Start Vite dev server with HMR |
| `pnpm build` | Build both api and pwa |
| `pnpm test` | Run API test suite (vitest) |
| `pnpm lint` | Lint all workspaces |
| `pnpm test` | Run API test suite (vitest + real MariaDB) |
| `pnpm test:e2e` | Run Playwright harness (iPhone + Pixel profiles) |
| `pnpm lint` | ESLint across all workspaces (flat config, TS-aware) |
| `pnpm typecheck` | Type-check all workspaces |
| `pnpm format` | Reformat all files with Prettier |
| `pnpm format:check` | Check formatting without writing (used in CI) |
| `pnpm --filter @familysync/api db:generate` | Generate Drizzle migration from schema changes |
| `pnpm --filter @familysync/api db:migrate` | Apply pending migrations to MariaDB |
@@ -123,6 +126,18 @@ Store the app password in the database via the `/me` endpoint after first login.
See [`docs/deployment.md`](docs/deployment.md) for Unraid/Docker Compose deployment notes including the Pangolin/Newt tunnel configuration.
## CI
Every PR to `main` must pass three required checks before it can merge:
| Job | What it runs |
| ------------------ | -------------------------------------------------------------------------- |
| `CI / fast-checks` | `pnpm lint`, `pnpm format:check`, `pnpm typecheck`, PWA unit tests |
| `CI / api` | DB migrations + API test suite against a real MariaDB 11 service container |
| `CI / harness` | Playwright end-to-end harness (WebKit iPhone + Chromium Pixel) |
`fast-checks` and `api`/`harness` run in parallel. Defined in `.gitea/workflows/ci.yml`.
## Publishing / Releases
Publishing happens automatically on every push to `main` — i.e. when a PR merges. The `.gitea/workflows/publish.yml` workflow runs and builds + pushes the API image to the Gitea container registry.
+52
View File
@@ -165,6 +165,8 @@ See `docs/deployment.md` for the full `drizzle-kit migrate` command used to prep
## Config File Reference
### Application Config Files
There are no application-level JSON/YAML config files. The two config files that read environment variables at dev/build time are:
| File | Purpose |
@@ -173,3 +175,53 @@ There are no application-level JSON/YAML config files. The two config files that
| `apps/pwa/vite.config.ts` | Vite build config — no env var reads; proxy rules for dev server |
The PWA Vite dev server proxies `/health`, `/api`, and `/callback` to `http://localhost:3000` so the frontend and API can be developed without CORS configuration.
### Lint and Format Config Files
These files live at the repo root and apply to both `apps/api` and `apps/pwa`.
#### `eslint.config.js`
ESLint 9 flat config (ESM). Pinned to ESLint **9.39.4** — do not upgrade to ESLint 10 until `eslint-plugin-react` resolves the `getFilename is not a function` incompatibility.
Key layers (in order):
1. **Global ignores**`**/dist/**`, `**/node_modules/**`, `apps/api/src/db/migrations/**`, `pnpm-lock.yaml`.
2. **Base TS/TSX** (`apps/**/*.{ts,tsx}`) — `js.configs.recommended` + `tseslint.configs.recommendedTypeChecked` with `projectService: true` (auto-discovers all `tsconfig.json` files). `@typescript-eslint/no-unused-vars` allows `_`-prefixed names.
3. **React + Hooks** (`apps/pwa/**/*.{ts,tsx}` only) — `eslint-plugin-react` flat recommended + `eslint-plugin-react-hooks` flat recommended. React Compiler rules (immutability, purity, refs, etc.) are disabled — this codebase does not use the React Compiler.
4. **`disableTypeChecked` override** — applied to tool config files and test/e2e directories that are outside any `tsconfig` project (`apps/api/drizzle.config.ts`, `apps/api/vitest.config.ts`, `apps/pwa/vite.config.ts`, `apps/pwa/vitest.config.ts`, `apps/pwa/playwright.config.ts`, `apps/api/tests/**/*.ts`, `apps/pwa/e2e/**/*.ts`, `eslint.config.js`). Type-aware rules are disabled for these files; non-type-aware rules still apply.
5. **`eslint-config-prettier`** (last) — disables all ESLint formatting rules that conflict with Prettier.
Run lint: `pnpm lint` (delegates to `pnpm -r --if-present lint` across all workspaces).
#### `.prettierrc`
```json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100
}
```
Run formatter: `pnpm format` (write) or `pnpm format:check` (CI check, no writes).
#### `.prettierignore`
Excludes `dist/`, `node_modules/`, `.pnpm-store/`, `pnpm-lock.yaml`, `apps/api/src/db/migrations/`, `*.html`, and `.planning/` from formatting. The `.pnpm-store/` exclusion covers CI runners that have no persistent global pnpm store and land the content-addressable store inside the workspace.
---
## CI Secrets
The Gitea Actions workflows in `.gitea/workflows/` require one repository secret.
| Secret | Scope | Description |
| -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `REGISTRY_PAT` | `publish.yml` only | A Gitea user PAT with **`write:package`** scope. Used to authenticate `docker login` against the Gitea container registry before pushing the API image. **Must be named `REGISTRY_PAT`** — Gitea reserves the `GITEA_` prefix for built-in variables, so any `GITEA_`-prefixed secret name is rejected. `GITEA_TOKEN` and `GITHUB_TOKEN` do not have package-push permissions. |
### CI Database Credentials
The `ci.yml` `api` and `harness` jobs spin up a throwaway MariaDB service container with hardcoded credentials (`familysync` / `testpass`). These are ephemeral — scoped to a single job container — and are **not** production secrets. Do not reuse them outside CI.
+95 -44
View File
@@ -100,37 +100,104 @@ Vite serves the PWA with HMR on the configured dev port. The PWA's API calls tar
### Root workspace scripts
| Command | Description |
| ---------------- | ------------------------------------------------------------- |
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
| `pnpm test` | Run API test suite (`vitest run` in `apps/api`) |
| `pnpm lint` | Run lint in all workspaces (`pnpm -r lint`) |
| `pnpm typecheck` | Run `tsc --noEmit` in all workspaces |
| Command | Description |
| ------------------- | ------------------------------------------------------------- |
| `pnpm dev:api` | Start API dev watcher (`node --watch dist/index.js`) |
| `pnpm dev:pwa` | Start Vite dev server for the PWA |
| `pnpm build` | Build both `apps/api` (tsc) and `apps/pwa` (tsc + vite build) |
| `pnpm test` | Run API test suite (`vitest run` in `apps/api`) |
| `pnpm test:e2e` | Run Playwright e2e harness (`apps/pwa`) |
| `pnpm lint` | ESLint across all workspaces (`pnpm -r --if-present lint`) |
| `pnpm format` | Reformat all files with Prettier (`prettier --write .`) |
| `pnpm format:check` | Check formatting without writing (`prettier --check .`) |
| `pnpm typecheck` | `tsc --noEmit` in all workspaces |
### `apps/api` scripts
| Command | Description |
| ------------------------------------------- | ------------------------------------------- |
| `pnpm --filter @familysync/api build` | Compile TypeScript (`tsc`) → `dist/` |
| `pnpm --filter @familysync/api dev` | Start `node --watch dist/index.js` |
| `pnpm --filter @familysync/api start` | Start `node dist/index.js` (no watch) |
| `pnpm --filter @familysync/api test` | Run vitest once (`vitest run`) |
| `pnpm --filter @familysync/api test:watch` | Run vitest in watch mode |
| `pnpm --filter @familysync/api typecheck` | `tsc --noEmit` |
| `pnpm --filter @familysync/api db:generate` | Generate SQL migrations from schema changes |
| `pnpm --filter @familysync/api db:migrate` | Apply pending migrations to the database |
| Command | Description |
| ------------------------------------------- | ----------------------------------------------- |
| `pnpm --filter @familysync/api build` | Compile TypeScript (`tsc`) → `dist/` |
| `pnpm --filter @familysync/api dev` | Start `node --watch dist/index.js` |
| `pnpm --filter @familysync/api start` | Start `node dist/index.js` (no watch) |
| `pnpm --filter @familysync/api test` | Run vitest once (`vitest run`) |
| `pnpm --filter @familysync/api test:watch` | Run vitest in watch mode |
| `pnpm --filter @familysync/api lint` | ESLint `src/` and `tests/` (`--max-warnings 0`) |
| `pnpm --filter @familysync/api typecheck` | `tsc --noEmit` |
| `pnpm --filter @familysync/api db:generate` | Generate SQL migrations from schema changes |
| `pnpm --filter @familysync/api db:migrate` | Apply pending migrations to the database |
### `apps/pwa` scripts
| Command | Description |
| ----------------------------------------- | ---------------------------------- |
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server with HMR |
| `pnpm --filter @familysync/pwa build` | `tsc && vite build``dist/` |
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
| `pnpm --filter @familysync/pwa typecheck` | `tsc --noEmit` |
| `pnpm --filter @familysync/pwa test` | Run vitest once |
| Command | Description |
| ----------------------------------------------- | ----------------------------------------------------------- |
| `pnpm --filter @familysync/pwa dev` | Start Vite dev server with HMR |
| `pnpm --filter @familysync/pwa build` | `tsc && vite build``dist/` |
| `pnpm --filter @familysync/pwa preview` | Serve the production build locally |
| `pnpm --filter @familysync/pwa lint` | ESLint `src/` and `e2e/` (`--max-warnings 0`) |
| `pnpm --filter @familysync/pwa typecheck` | `tsc --noEmit` + `tsc --project tsconfig.e2e.json --noEmit` |
| `pnpm --filter @familysync/pwa test` | Run vitest once |
| `pnpm --filter @familysync/pwa test:e2e` | Run Playwright e2e tests |
| `pnpm --filter @familysync/pwa test:e2e:headed` | Playwright in headed mode (visible browser) |
| `pnpm --filter @familysync/pwa test:e2e:ui` | Playwright UI mode |
## Code Quality — Run Before Every Push
CI gates every PR to `main` on these checks. Run them locally before pushing to avoid a CI round-trip.
```bash
pnpm lint # ESLint --max-warnings 0 across apps/api (src/ + tests/) and apps/pwa (src/ + e2e/)
pnpm format:check # Prettier formatting check (use `pnpm format` to auto-fix)
pnpm typecheck # tsc --noEmit in both apps (includes apps/pwa tsconfig.e2e.json)
```
### ESLint
Config: `eslint.config.js` (root, flat ESLint 9 format). The config covers:
- **All `apps/**/\*.{ts,tsx}`** — `js.configs.recommended`+`tseslint.configs.recommendedTypeChecked`with`projectService: true`(type-aware rules, auto-discovers all`tsconfig.json` files)
- **`apps/pwa/**/\*.{ts,tsx}`additionally** —`eslint-plugin-react`+`eslint-plugin-react-hooks` (React 19 flat config; React Compiler rules disabled — this codebase does not use the Compiler)
- **Tool configs + test dirs** (`drizzle.config.ts`, `vitest.config.ts`, `apps/api/tests/**`, `apps/pwa/e2e/**`) — type-aware rules disabled via `disableTypeChecked` (these files are outside the main tsconfig projects)
- **Prettier integration**`eslint-config-prettier` last in the config disables all formatting rules that conflict with Prettier
`--max-warnings 0` is enforced: warnings count as failures. **Blanket `eslint-disable` comments are not permitted** — every suppression requires a justification comment.
### Prettier
Config: `.prettierrc` (root). Settings: `singleQuote: true`, `semi: true`, `tabWidth: 2`, `trailingComma: "all"`, `printWidth: 100`.
```bash
pnpm format # write fixes in place
pnpm format:check # check only (used in CI)
```
The `.prettierignore` file at the repo root excludes build output and generated files.
## TypeScript Strict Checks
Both workspaces use `"strict": true` in their `tsconfig.json`. The build step (`tsc`) catches type errors in `apps/api` (since it emits output). For `apps/pwa`, Vite uses esbuild to transpile and does not perform type checking — **vitest will pass even when there are type errors in the PWA**. Always run the typecheck script explicitly:
```bash
# Check both workspaces
pnpm typecheck
# Or individually
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck # also checks tsconfig.e2e.json
```
Run `pnpm typecheck` before opening a PR to catch errors that vitest and Vite builds will silently miss.
## CI Pipeline Overview
Every PR to `main` runs three parallel jobs (`.gitea/workflows/ci.yml`):
| Job | Checks |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| `fast-checks` | `pnpm lint``pnpm format:check``pnpm typecheck``pnpm --filter @familysync/pwa test` |
| `api` | DB migrations + `pnpm --filter @familysync/api test` (vitest against a MariaDB 11 service container) |
| `harness` | DB migrations + API build + Playwright e2e (WebKit + Chromium) with `DEV_AUTH_BYPASS=true` |
All three jobs must pass before a PR can merge. See [docs/TESTING.md](TESTING.md) for test suite details.
## Drizzle Migration Workflow
@@ -162,25 +229,6 @@ DB_HOST=127.0.0.1 pnpm --filter @familysync/api db:migrate
Migration files live in `apps/api/src/db/migrations/` and are committed to version control.
## TypeScript Strict Checks
Both workspaces use `"strict": true` in their `tsconfig.json`. The build step (`tsc`) catches type errors in `apps/api` (since it emits output). For `apps/pwa`, Vite uses esbuild to transpile and does not perform type checking — **vitest will pass even when there are type errors in the PWA**. Always run the typecheck script explicitly:
```bash
# Check both workspaces
pnpm typecheck
# Or individually
pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck
```
Run `pnpm typecheck` before opening a PR to catch errors that vitest and Vite builds will silently miss.
## Code Style
ESLint and Prettier are listed as the intended linting and formatting tools. Check for config files in each workspace and confirm the `lint` script is wired before running `pnpm lint`. <!-- VERIFY: ESLint and Prettier configs are present in apps/api and apps/pwa -->
## Docker Compose Dev Stack
```bash
@@ -216,3 +264,6 @@ The vitest config sets `fileParallelism: false` to prevent concurrent test files
**TypeScript errors missed during development**
Vite/esbuild strips types; type errors will not surface in `vitest run` or `vite build` output. Run `pnpm typecheck` explicitly to catch them.
**`pnpm lint` warns about ESLint version**
ESLint is pinned to 9.39.4. Do not upgrade to ESLint 10 until `eslint-plugin-react` resolves the `getFilename is not a function` incompatibility (`jsx-eslint#3977`).
+114 -7
View File
@@ -19,6 +19,8 @@ No additional install step is needed beyond the normal `pnpm install` at the rep
## Running tests
### Unit and integration tests
**All API tests (from repo root):**
```bash
@@ -27,7 +29,7 @@ pnpm --filter @familysync/api test
This is also the command run by `pnpm test` at the root.
**All PWA tests:**
**All PWA unit tests:**
```bash
pnpm --filter @familysync/pwa test
@@ -45,7 +47,41 @@ pnpm --filter @familysync/api test:watch
pnpm --filter @familysync/api exec vitest run tests/routes/lists.test.ts
```
**Type checking (separate from tests — required):**
### End-to-end tests (Playwright)
The PWA has a Playwright harness configured in `apps/pwa/playwright.config.ts` with two device profiles:
| Profile | Viewport | Engine | User-Agent |
| -------- | -------- | -------- | ------------------------- |
| `iphone` | 390×844 | WebKit | Mobile Safari (iPhone 14) |
| `pixel` | 412×915 | Chromium | Chrome Android (Pixel 7) |
Both profiles block the service worker (`serviceWorkers: 'block'`) so the Workbox SW does not intercept requests during tests. Auth is handled via `DEV_AUTH_BYPASS=true` on the API — never via stored browser state.
**Run all e2e tests (both profiles):**
```bash
pnpm test:e2e
# or
pnpm --filter @familysync/pwa test:e2e
```
**Run a single profile:**
```bash
pnpm --filter @familysync/pwa exec playwright test --project=pixel
pnpm --filter @familysync/pwa exec playwright test --project=iphone
```
**Interactive UI mode:**
```bash
pnpm --filter @familysync/pwa test:e2e:ui
```
The `baseURL` is driven by `PLAYWRIGHT_BASE_URL` (default: `http://localhost:5173`). In local mode the config reuses a running Vite dev server; in CI it starts Vite itself. The API, MariaDB, and Redis must already be running via Docker Compose before launching e2e tests locally — see `docs/DEVELOPMENT.md`.
### Type checking (separate from tests — required)
Vitest uses esbuild, which strips TypeScript types at runtime. A test run can pass while `tsc` reports errors. Always run type checks separately:
@@ -55,6 +91,39 @@ pnpm --filter @familysync/api typecheck
pnpm --filter @familysync/pwa typecheck
```
The PWA typecheck also covers the e2e spec files: `tsc --project tsconfig.e2e.json --noEmit`.
## Quality gate
The full local quality gate before opening a PR:
```bash
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test
```
Add e2e when changing PWA behaviour:
```bash
pnpm test:e2e
```
| Step | Command | What it checks |
| ---------------- | ------------------------------------ | -------------------------------------------------------- |
| Lint | `pnpm lint` | ESLint `--max-warnings 0` across both apps (type-aware) |
| Format check | `pnpm format:check` | Prettier — fails on any unformatted file |
| Typecheck | `pnpm typecheck` | `tsc --noEmit` across both apps (including e2e tsconfig) |
| Unit / API tests | `pnpm test` | API integration tests via Vitest |
| PWA unit tests | `pnpm --filter @familysync/pwa test` | Component and logic tests in jsdom |
| E2E | `pnpm test:e2e` | Playwright iphone + pixel profiles |
A deliberate ESLint violation makes `pnpm lint` exit non-zero; a formatting deviation makes `pnpm format:check` exit non-zero. Both block the PR in CI.
To auto-fix formatting locally:
```bash
pnpm format # prettier --write .
```
## Integration tests requiring a real database
Several API tests in `apps/api/tests/lib/` and `apps/api/tests/routes/` connect to the real dev MariaDB rather than mocking the DB layer. These tests require the dev Docker stack to be running with port 3306 exposed.
@@ -89,6 +158,7 @@ Pure-logic tests (e.g. `apps/api/tests/broker/expand.test.ts`, `apps/api/tests/l
| ---------- | ------------------------------------- | ------------------------------------ |
| `apps/api` | `apps/api/tests/{category}/*.test.ts` | `apps/api/tests/routes/push.test.ts` |
| `apps/pwa` | co-located `*.test.ts` / `*.test.tsx` | `src/components/AppNav.test.tsx` |
| `apps/pwa` | e2e specs | `e2e/*.spec.ts` |
Test categories for `apps/api`:
@@ -114,10 +184,47 @@ No coverage thresholds are configured in either `vitest.config.ts`. There is no
## CI integration
No CI pipeline is configured in this repository. Tests are run manually by developers before opening pull requests against the self-hosted Gitea remote.
CI runs on a self-hosted Gitea Actions runner and triggers on every pull request targeting `main` (`.gitea/workflows/ci.yml`). Three jobs run in parallel:
The recommended pre-PR gate is:
### `fast-checks`
```bash
pnpm test && pnpm typecheck
```
Runs lint, format check, typecheck, and PWA unit tests — no external services required.
| Step | Command |
| -------------- | ------------------------------------ |
| Lint | `pnpm lint` |
| Format check | `pnpm format:check` |
| Typecheck | `pnpm typecheck` |
| PWA unit tests | `pnpm --filter @familysync/pwa test` |
### `api`
Runs the full API test suite against a `mariadb:11` service container.
| Step | Detail |
| ----------------- | ---------------------------------------------------------- |
| MariaDB service | `mariadb:11` container; `DB_HOST=mariadb`, `DB_PORT=3306` |
| Readiness poll | Node script via `mysql2` driver (no `mysql` CLI in runner) |
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
| Tests | `pnpm --filter @familysync/api test` |
The throwaway credentials (`DB_USER=familysync`, `DB_PASSWORD=testpass`) are scoped to the ephemeral CI container and are never production secrets.
`actions/cache@v4` is intentionally omitted — the cache server times out on this runner (socket hang-up). `pnpm install` without cache takes ~30 s and is acceptable.
### `harness`
Runs the Playwright mobile e2e harness (iphone + pixel) against a runner-hosted dev stack.
| Step | Detail |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| MariaDB service | Same `mariadb:11` setup as the `api` job |
| Schema migrations | `pnpm --filter @familysync/api db:migrate` |
| Dev user seed | Inserts `users` row id=1 (`INSERT IGNORE`) for `DEV_AUTH_BYPASS` |
| API build | `pnpm --filter @familysync/api build` (dist/ is gitignored) |
| Playwright install | `npx playwright install --with-deps webkit chromium` (no cache) |
| API start + tests | API started as a background process in the same step as `playwright test` to survive the step boundary; `DEV_AUTH_BYPASS=true`, `NODE_ENV=development` |
| Base URL | `http://127.0.0.1:5173` (not `localhost` — runner resolves `localhost` to `::1` but Vite binds IPv4-only) |
| Artifacts on fail | Traces, screenshots, videos, and HTML report uploaded via `ChristopherHX/gitea-upload-artifact@v4` (standard `upload-artifact` aborts on Gitea) |
The API process is started and the Playwright suite invoked within a single CI step. Starting the API in an earlier step causes it to be reaped at the step boundary before Playwright runs.
+100 -7
View File
@@ -24,6 +24,61 @@ The production compose file brings up three services:
---
## CI/CD Pipeline
FamilySync uses a self-hosted Gitea Actions runner. Two workflows govern the release path.
### PR gate — `.gitea/workflows/ci.yml`
Triggered on every pull request targeting `main`. Three jobs run in parallel; all three must pass before the PR can be merged:
| Job | What it checks |
| ------------- | --------------------------------------------------------------------------------- |
| `fast-checks` | Lint (`pnpm lint`), format check (`pnpm format:check`), typecheck, PWA unit tests |
| `api` | DB migrations + API integration tests against a live MariaDB service container |
| `harness` | Full Playwright E2E suite (iPhone + Pixel profiles) against the compiled API |
A PR with lint or format violations is blocked from merging by the `fast-checks` job.
Branch protection on `main` blocks direct push and force push. Only PRs with all three required checks (`CI / fast-checks`, `CI / api`, `CI / harness`) passing can merge.
### Publish — `.gitea/workflows/publish.yml`
Triggered on push to `main` (i.e., when any PR merges). Builds the `apps/api` Docker image and pushes it to the Gitea container registry.
**Registry:** `git.bergerhouse.net/luckberg/familysync-api`
**Image tags produced per merge:**
| Tag | Example | Purpose |
| ------------------------- | --------------- | ------------------------------------------ |
| `:latest` | `:latest` | Moving pointer for easy `docker pull` |
| `:<milestone>-<shortsha>` | `:v1.1-98acff8` | Immutable, rollback-traceable (7-char SHA) |
The current milestone prefix (`v1.1`) is set in the `MILESTONE` env var at the top of `publish.yml`. Update it at milestone boundaries.
The immutable `:<milestone>-<sha>` tag is pushed first. `:latest` is only moved after the immutable tag has landed, so a failed second push can never leave `:latest` advanced without a corresponding rollback tag.
**Authentication — `REGISTRY_PAT` secret:**
The workflow authenticates with the Gitea container registry using a PAT stored in the `REGISTRY_PAT` Actions secret. The secret must have `write:package` scope. It is named `REGISTRY_PAT` — not `GITEA_REGISTRY_PAT` or any `GITEA_`-prefixed name, because Gitea reserves the `GITEA_` prefix and will reject those names at secret-creation time. `GITEA_TOKEN` and `GITHUB_TOKEN` cannot push packages.
The PAT is passed via `--password-stdin` (never via `-p`/`--password`) and is bound through `env:` so it is never interpolated into the script body:
```yaml
env:
REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
run: |
printf '%s' "$REGISTRY_PAT" | \
docker login git.bergerhouse.net \
--username luckberg \
--password-stdin
```
The credential is purged from the runner with `docker logout` in an `if: always()` step after every push.
---
## Prerequisites
- Docker and Docker Compose available on the Unraid host.
@@ -163,7 +218,40 @@ Ensure `OIDC_AUTH_EXTERNAL_URL` and `OIDC_REDIRECT_URI` in `.env` match the publ
---
## Build and Start
## Pulling the Published Image on Unraid
After a PR merges, `publish.yml` pushes two tags to the Gitea registry. To deploy the latest build on the Unraid host:
```bash
# Pull the moving :latest pointer
docker pull git.bergerhouse.net/luckberg/familysync-api:latest
# Or pin to a specific immutable tag (recommended for production)
docker pull git.bergerhouse.net/luckberg/familysync-api:v1.1-98acff8
```
<!-- VERIFY: Confirm the Gitea registry requires authentication for pulls on the Unraid host (docker login git.bergerhouse.net) -->
Update `docker-compose.yml` to reference the pre-built image instead of building locally:
```yaml
services:
api:
image: git.bergerhouse.net/luckberg/familysync-api:latest
# remove the build: block when using the published image
```
Then restart the service:
```bash
docker compose pull api && docker compose up -d api
```
---
## Build and Start (local build)
If you need to build locally rather than pull from the registry:
```bash
# From the repo root — builds both the API and the React PWA into one image
@@ -206,13 +294,18 @@ A `503` response (`{"ok":false,"db":"down"}`) means the API cannot reach MariaDB
## Rollback
There is no automated rollback pipeline. To revert to a previous build:
To revert to a specific prior build, use the immutable image tag produced by `publish.yml`:
1. Identify the prior working Git commit.
2. Stop the API: `docker compose stop api`.
3. Rebuild from the target commit: `git checkout <commit> && docker compose build api`.
4. Start: `docker compose up -d api`.
5. If the rollback crosses a schema migration boundary, restore the MariaDB volume from a backup — schema downgrades are not supported by Drizzle Kit's migrate command.
```bash
# Identify the immutable tag from the Gitea registry or CI run history
# e.g. git.bergerhouse.net/luckberg/familysync-api:v1.1-98acff8
docker compose stop api
# Update docker-compose.yml image: line to the target immutable tag, then:
docker compose pull api && docker compose up -d api
```
If the rollback crosses a schema migration boundary, restore the MariaDB volume from a backup — schema downgrades are not supported by Drizzle Kit's migrate command.
Take a MariaDB dump before every deployment that includes a migration: