Files
familysync/.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md
T

14 KiB
Raw Blame History

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.ymlchanges job (new)

Analog: The existing fast-checks job in the same file, lines 838.

Job skeleton pattern (ci.yml lines 811 — fast-checks header as template):

  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:

  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.ymlapi job modification

Analog: Existing api job, ci.yml lines 40125.

Current header (lines 4044):

  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:

  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.ymlharness job modification

Analog: Existing harness job, ci.yml lines 126314.

Current header (lines 126129):

  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:

  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.ymlgate 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:

  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 3132:

      - name: Format check
        run: pnpm format:check

New step — insert after Format check, before Typecheck:

      - 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 616):

  "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):

    "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 1725):

  "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):

    "markdownlint-cli2": "0.22.1",
    "prettier": "3.8.4",

Install command (must run at workspace root):

pnpm add -D markdownlint-cli2@0.22.1 --workspace-root

.markdownlint-cli2.jsonc (new file)

Analog: .prettierrc (root JSON tool config, lines 17) — same layer (repo root), same purpose (configure a formatting/lint tool), same JSON format. No .markdownlint-cli2.jsonc exists yet.

.prettierrc structural pattern (lines 17):

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}

New file — use JSONC format (.jsonc extension supports comments), place at repo root:

// .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):

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)

    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 &&

    if: github.event_name == 'pull_request'

Step ordering in fast-checks

Source: ci.yml lines 1138 — 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)

    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 2223, 7576, 161162 — comment in every job:

      # 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 1617 — the file's safety-gate comment names CI / api and CI / harness as the required checks:

# 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