docs: refresh CI + lint/format docs (Phase 8 + Phase 13) #9
@@ -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; ~1–2 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)
|
||||
|
||||
+265
-977
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+21927
-32077
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user