14 KiB
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):
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/checkoutstep —dorny/paths-filter@v4uses the Gitea/GitHub REST API on PR events (no fetch-depth needed). permissions: pull-requests: readis required by v4; declare it at job scope (not workflow scope).- Define the positive
codefilter (not adocsfilter) —code == 'false'means "only docs changed", and ambiguous new file types safely default to the full test run. - Insert this job before
fast-checksin 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):
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.yml — harness job modification
Analog: Existing harness job, ci.yml lines 126–314.
Current header (lines 126–129):
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.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:
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:
- 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 6–16):
"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 17–25):
"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 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):
{
"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 11–38 — current step sequence:
actions/checkout@v4actions/setup-node@v4(node 22)Enable pnpm(corepack)Install dependencies(pnpm install --frozen-lockfile)LintFormat check- ← insert
Markdown linthere TypecheckPWA 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 22–23, 75–76, 161–162 — 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 16–17 — 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