diff --git a/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md new file mode 100644 index 0000000..14d4ef7 --- /dev/null +++ b/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md @@ -0,0 +1,417 @@ +# Phase 15: Doc-Only CI Skip + Markdown Lint - Pattern Map + +**Mapped:** 2026-06-12 +**Files analyzed:** 4 (2 modified, 1 created, N docs fixed) +**Analogs found:** 3 / 4 (`.markdownlint-cli2.jsonc` has no role-match analog — closest is `.prettierrc`) + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match Quality | +|---|---|---|---|---| +| `.gitea/workflows/ci.yml` | CI workflow | event-driven | itself (existing job blocks) | exact — new jobs modeled on existing jobs in same file | +| `.markdownlint-cli2.jsonc` | tool config | — | `.prettierrc` (root JSON tool config) | structural-match (same layer, no content analog) | +| `package.json` (root) | package manifest | — | itself (existing `format:check` script + `prettier` devDep) | exact — same script/dep pattern | +| `docs/**/*.md`, `README.md`, `apps/*/README.md` | docs content | — | the violating files themselves | exact — in-place fixes | + +--- + +## Pattern Assignments + +### `.gitea/workflows/ci.yml` — `changes` job (new) + +**Analog:** The existing `fast-checks` job in the same file, lines 8–38. + +**Job skeleton pattern** (ci.yml lines 8–11 — `fast-checks` header as template): +```yaml + fast-checks: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: +``` + +**New `changes` job — copy this structure, replacing steps with the paths-filter action:** +```yaml + changes: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + pull-requests: read + outputs: + code: ${{ steps.filter.outputs.code }} + steps: + - uses: dorny/paths-filter@v4 + id: filter + with: + filters: | + code: + - '**/*.ts' + - '**/*.tsx' + - '**/*.js' + - '**/*.json' + - '**/*.yaml' + - '**/*.yml' + - 'apps/**' + - 'packages/**' + - 'pnpm-lock.yaml' + - 'Dockerfile' + - 'docker-compose*.yml' +``` + +**Key design notes:** +- No `actions/checkout` step — `dorny/paths-filter@v4` uses the Gitea/GitHub REST API on PR events (no fetch-depth needed). +- `permissions: pull-requests: read` is required by v4; declare it at job scope (not workflow scope). +- Define the positive `code` filter (not a `docs` filter) — `code == 'false'` means "only docs changed", and ambiguous new file types safely default to the full test run. +- Insert this job **before** `fast-checks` in the file so the job ordering in the UI is logical (changes → fast-checks / api / harness → gate). + +--- + +### `.gitea/workflows/ci.yml` — `api` job modification + +**Analog:** Existing `api` job, ci.yml lines 40–125. + +**Current header** (lines 40–44): +```yaml + api: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + # Runs in PARALLEL with fast-checks (D-03) — no needs: dependency. + services: +``` + +**Modified header — add `needs` and extend the `if` condition:** +```yaml + api: + runs-on: ubuntu-latest + needs: [changes] + if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' + # Runs in PARALLEL with fast-checks (D-03) — skipped for doc-only PRs. + services: +``` + +**Nothing else in the `api` job body changes.** The existing comment at line 43 should be updated to reflect conditional skip behavior. + +--- + +### `.gitea/workflows/ci.yml` — `harness` job modification + +**Analog:** Existing `harness` job, ci.yml lines 126–314. + +**Current header** (lines 126–129): +```yaml + harness: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + # Runs in PARALLEL with fast-checks + api (D-03) — no needs: dependency. + services: +``` + +**Modified header — same pattern as `api`:** +```yaml + harness: + runs-on: ubuntu-latest + needs: [changes] + if: github.event_name == 'pull_request' && needs.changes.outputs.code == 'true' + # Runs in PARALLEL with fast-checks (D-03) — skipped for doc-only PRs. + services: +``` + +**Nothing else in the `harness` job body changes.** + +--- + +### `.gitea/workflows/ci.yml` — `gate` job (new) + +**Analog:** `publish.yml` line 94 — `if: always()` pattern on the `Docker logout` step; same construct used at job scope here. + +**Insert after `harness` job, at end of file:** +```yaml + gate: + runs-on: ubuntu-latest + needs: [fast-checks, changes, api, harness] + if: always() + steps: + - name: Check all required jobs passed or were skipped + run: | + # fast-checks always runs — must be success + if [ "${{ needs.fast-checks.result }}" != "success" ]; then + echo "fast-checks: ${{ needs.fast-checks.result }}" + exit 1 + fi + # api and harness are conditionally skipped — success OR skipped are both acceptable + for result in "${{ needs.api.result }}" "${{ needs.harness.result }}"; do + if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then + echo "Heavy job failed or was cancelled: $result" + exit 1 + fi + done + echo "Gate passed." +``` + +**Critical Gitea 1.26.2 constraint:** Do NOT use `contains(needs.*.result, 'success')` — Gitea issue #31007 confirms this returns `false` even when jobs succeed. Individual `needs.X.result` string comparisons are the only reliable approach on this instance. + +**Why `if: always()`:** Without it, a skipped upstream job causes `gate` to also skip — branch protection waiting for `CI / gate` would deadlock. The `if: always()` deadlock bug (Gitea #27906) was fixed in 1.21.8; this instance runs 1.26.2. + +--- + +### `.gitea/workflows/ci.yml` — Markdown lint step in `fast-checks` + +**Analog:** Existing `Format check` step, ci.yml lines 31–32: +```yaml + - name: Format check + run: pnpm format:check +``` + +**New step — insert after `Format check`, before `Typecheck`:** +```yaml + - name: Markdown lint + run: pnpm md:lint +``` + +**Slot:** After line 32 (`run: pnpm format:check`), before line 34 (`- name: Typecheck`). One line of YAML mirrors the `format:check` step pattern exactly — a named step calling a root pnpm script. + +--- + +### `package.json` (root) — `md:lint` script + devDependency + +**Analog:** Existing `format:check` script and `prettier` devDependency, package.json lines 15 and 21. + +**Current scripts block** (lines 6–16): +```json + "scripts": { + "dev:api": "pnpm --filter @familysync/api dev", + "dev:pwa": "pnpm --filter @familysync/pwa dev", + "build": "pnpm --filter @familysync/api build && pnpm --filter @familysync/pwa build", + "test": "pnpm --filter @familysync/api test", + "test:e2e": "pnpm --filter @familysync/pwa test:e2e", + "lint": "pnpm -r --if-present lint", + "typecheck": "pnpm -r typecheck", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, +``` + +**Add `md:lint` after `format:check` (alphabetical/logical grouping matches existing order):** +```json + "format:check": "prettier --check .", + "md:lint": "markdownlint-cli2" +``` + +**Note:** No glob args on the CLI — globs and ignores live entirely in `.markdownlint-cli2.jsonc` (see Open Question #3 in RESEARCH.md resolved: config-file globs make `pnpm md:lint` and direct `npx markdownlint-cli2` behave identically). + +**Current devDependencies block** (lines 17–25): +```json + "devDependencies": { + "@eslint/js": "9.39.4", + "eslint": "9.39.4", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-react": "7.37.5", + "eslint-plugin-react-hooks": "7.1.1", + "prettier": "3.8.4", + "typescript-eslint": "8.61.0" + } +``` + +**Add `markdownlint-cli2` (alphabetical, between `eslint-plugin-react-hooks` and `prettier`):** +```json + "markdownlint-cli2": "0.22.1", + "prettier": "3.8.4", +``` + +**Install command (must run at workspace root):** +```bash +pnpm add -D markdownlint-cli2@0.22.1 --workspace-root +``` + +--- + +### `.markdownlint-cli2.jsonc` (new file) + +**Analog:** `.prettierrc` (root JSON tool config, lines 1–7) — same layer (repo root), same purpose (configure a formatting/lint tool), same JSON format. No `.markdownlint-cli2.jsonc` exists yet. + +**`.prettierrc` structural pattern** (lines 1–7): +```json +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 100 +} +``` + +**New file — use JSONC format (`.jsonc` extension supports comments), place at repo root:** +```jsonc +// .markdownlint-cli2.jsonc +{ + "config": { + // Disable all rules that conflict with Prettier (23 rules — line-length, list-indent, + // blanks-around-fences, emphasis-style, etc.) + "extends": "markdownlint/style/prettier", + + // Content rules to KEEP: + "MD001": true, // heading-increment: no skipping h1→h3 + "MD024": true, // no-duplicate-heading + "MD040": true, // fenced-code-language: all fences must declare a language + "MD031": true, // blanks-around-fences (re-enabled — see Pitfall 4 in RESEARCH.md) + "MD051": true, // link-fragments: broken anchor links + "MD052": true, // reference-links-images: undefined link references + + // Rules DISABLED (Prettier owns these OR they fire on non-author-controlled files): + "MD041": false, // first-line-h1: CLAUDE.md legitimately starts with ## Project + "MD034": false, // no-bare-urls: CLAUDE.md version table uses pkg@version syntax + "MD036": false // no-emphasis-as-heading: docs/API.md uses **Response 200** as label + }, + "globs": [ + "docs/**/*.md", + "*.md", + "apps/**/*.md" + ], + "ignores": [ + ".planning/**", + "node_modules/**", + "**/node_modules/**", + ".pnpm-store/**" + ] +} +``` + +**Why `extends` resolves:** `markdownlint-cli2` depends on `markdownlint`, which ships `style/prettier.json`. After `pnpm add -D markdownlint-cli2 --workspace-root`, the file exists at `node_modules/markdownlint/style/prettier.json`. + +**Glob placement in config, not CLI:** Putting `globs` and `ignores` in the config file means `pnpm md:lint` (just `markdownlint-cli2`, no args) and a developer running `npx markdownlint-cli2` directly both use the same scope automatically. + +--- + +### Docs content fixes — 13 baseline violations (MD040 / MD031) + +**Analog:** The violating files themselves. All fixes are mechanical one-liners. + +**Files and fix type:** + +| File | Rule | Fix | +|------|------|-----| +| `apps/api/README.md` | MD040 (1) | Add language tag to bare ` ``` ` | +| `apps/pwa/e2e/README.md` | MD040 (1) | Add language tag to bare ` ``` ` | +| `apps/pwa/README.md` | MD040 (1) | Add language tag to bare ` ``` ` | +| `docs/API.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks | +| `docs/ARCHITECTURE.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks | +| `docs/DEVELOPMENT.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks | +| `docs/GETTING-STARTED.md` | MD031 (2) | Add blank lines around 1 fence pair | +| `README.md` | MD040 (2) | Add language tags to 2 bare ` ``` ` blocks | + +**MD040 fix pattern** — change: +```` +``` +some content +``` +```` +to (pick the language that matches: `bash`, `text`, `json`, `yaml`, etc.): +```` +```bash +some content +``` +```` + +**MD031 fix pattern** — change: +```` +- list item +```bash +code +``` +next paragraph +```` +to: +```` +- list item + +```bash +code +``` + +next paragraph +```` + +**Verification command (run locally after fixes, before pushing):** +```bash +pnpm md:lint +pnpm format:check +``` +Run both — see RESEARCH.md Pitfall 4: MD031 fixes and Prettier must not conflict. + +--- + +## Shared Patterns + +### `runs-on` label +**Source:** ci.yml line 9, 41, 127 — `runs-on: ubuntu-latest` +**Apply to:** All new jobs (`changes`, `gate`) +```yaml + runs-on: ubuntu-latest +``` +The runner advertises `ubuntu-latest`. Never use `self-hosted`. + +### `if: github.event_name == 'pull_request'` guard +**Source:** ci.yml lines 10, 41, 128 — existing jobs all carry this guard +**Apply to:** `changes` job (same guard); `api` and `harness` extend it with `&&` +```yaml + if: github.event_name == 'pull_request' +``` + +### Step ordering in `fast-checks` +**Source:** ci.yml lines 11–38 — current step sequence: +1. `actions/checkout@v4` +2. `actions/setup-node@v4` (node 22) +3. `Enable pnpm` (corepack) +4. `Install dependencies` (pnpm install --frozen-lockfile) +5. `Lint` +6. `Format check` +7. **← insert `Markdown lint` here** +8. `Typecheck` +9. `PWA unit tests` + +The new `Markdown lint` step slots between `Format check` and `Typecheck` — logically grouped with format/style checks, before compilation checks. + +### `if: always()` usage +**Source:** publish.yml line 94 — `if: always()` on the `Docker logout` step +**Apply to:** `gate` job (job-level `if: always()`, not step-level) +```yaml + if: always() +``` +This is the only safe way to ensure `gate` always reports a commit-status regardless of upstream job outcomes. + +### No `actions/cache@v4` +**Source:** ci.yml lines 22–23, 75–76, 161–162 — comment in every job: +```yaml + # actions/cache@v4 is intentionally omitted — probe (D-PROBE-04) showed it + # times out on this runner (socket hang-up between runner container and job + # container cache server). pnpm install without cache takes ~30s; acceptable. +``` +**Apply to:** `changes` job has no install step so this is not relevant. `gate` job has no install step. No new jobs require caching. + +--- + +## No Analog Found + +| File | Role | Data Flow | Reason | +|------|------|-----------|--------| +| `.markdownlint-cli2.jsonc` | tool config | — | No existing markdownlint config in repo; `.prettierrc` used as structural analog only (JSON root tool config pattern) | + +--- + +## publish.yml Comment Update (advisory) + +**Source:** publish.yml lines 16–17 — the file's safety-gate comment names `CI / api` and `CI / harness` as the required checks: +```yaml +# The PR test jobs (fast-checks, api, harness in ci.yml) run on pull_request — they never +# run in the same workflow invocation as publish.yml. Tests gate the PR; main is trusted to +# be green because direct push and force push are blocked and the three required checks +# (CI / fast-checks, CI / api, CI / harness) must pass before merge. +``` +After this phase, the required checks change to `CI / fast-checks` + `CI / gate`. The planner should include a step to update this comment to reflect the new required-check names. + +--- + +## Metadata + +**Analog search scope:** `.gitea/workflows/`, repo root (config files), `package.json` +**Files scanned:** 4 (ci.yml, publish.yml, package.json, .prettierrc) +**Pattern extraction date:** 2026-06-12