From 213aeba347554ee2675879ea8301be5fdc96fb35 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 11 Jun 2026 22:21:23 -0400 Subject: [PATCH] docs: refresh CI + lint/format docs (Phase 8 + Phase 13) --- README.md | 19 +++++- docs/CONFIGURATION.md | 52 ++++++++++++++++ docs/DEVELOPMENT.md | 139 +++++++++++++++++++++++++++++------------- docs/TESTING.md | 121 +++++++++++++++++++++++++++++++++--- docs/deployment.md | 107 +++++++++++++++++++++++++++++--- 5 files changed, 378 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 6144dd6..f96ba68 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7c2bb9e..fbe25dc 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index dcf0673..9c4d5f4 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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`. - ## 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`). diff --git a/docs/TESTING.md b/docs/TESTING.md index b54036b..e15d3e3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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. diff --git a/docs/deployment.md b/docs/deployment.md index edebc12..b5dd161 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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` | +| `:-` | `: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 `:-` 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 +``` + + + +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 && 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: