docs(15): create phase plan — doc-only CI skip + markdown lint
3 plans across 3 waves: - 15-01: markdownlint-cli2 config/script/step + fix 13 baseline violations (SC-4) - 15-02: ci.yml changes job + conditional api/harness + always-running gate (SC-1/2, SC-3 YAML) - 15-03: operator branch-protection checkpoint + publish.yml comment (SC-3)
This commit is contained in:
@@ -280,7 +280,14 @@ Plans:
|
|||||||
- **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`.
|
- **Prettier vs markdownlint overlap** — Prettier already owns markdown *formatting*; scope markdownlint to *content* rules (heading increments, no broken/duplicate link refs, list/code-fence conventions) and disable its purely-stylistic rules that fight Prettier (e.g. line-length, list-indent), so the two don't conflict on the same `.md`.
|
||||||
- **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files.
|
- **`.planning/*` is push-direct, never linted** — planning bookkeeping bypasses CI via the Unprotected file pattern, so markdownlint never sees it; scope the lint glob to `docs/` + repo-root/app `*.md` and exclude `.planning/**` (and any generated markdown) to avoid a baseline cleanup of churny bookkeeping files.
|
||||||
|
|
||||||
**Plans**: TBD
|
**Plans**: 3 plans (3 waves)
|
||||||
|
|
||||||
|
Plans:
|
||||||
|
|
||||||
|
- [ ] 15-01-PLAN.md — markdownlint-cli2 + `.markdownlint-cli2.jsonc` + `md:lint` script + fast-checks step + fix 13 baseline violations (SC-4)
|
||||||
|
- [ ] 15-02-PLAN.md — ci.yml: `changes` (dorny/paths-filter@v4) + conditional api/harness + always-running `gate` aggregate (SC-1/SC-2, SC-3 YAML)
|
||||||
|
- [ ] 15-03-PLAN.md — operator branch-protection checkpoint (require `CI / fast-checks` + `CI / gate`, drop api/harness) + publish.yml comment update (SC-3)
|
||||||
|
|
||||||
**UI hint**: no
|
**UI hint**: no
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
@@ -301,7 +308,7 @@ Plans:
|
|||||||
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
|
| 12. Initial Setup Wizard | v1.1 | 0/? | Not started | - |
|
||||||
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
| 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 |
|
||||||
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
| 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 |
|
||||||
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 0/? | Not started | - |
|
| 15. Doc-Only CI Skip + MD Lint | v1.1 | 0/3 | Planned | - |
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
---
|
||||||
|
phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- .markdownlint-cli2.jsonc
|
||||||
|
- package.json
|
||||||
|
- pnpm-lock.yaml
|
||||||
|
- .gitea/workflows/ci.yml
|
||||||
|
- README.md
|
||||||
|
- apps/api/README.md
|
||||||
|
- apps/pwa/README.md
|
||||||
|
- apps/pwa/e2e/README.md
|
||||||
|
- docs/API.md
|
||||||
|
- docs/ARCHITECTURE.md
|
||||||
|
- docs/DEVELOPMENT.md
|
||||||
|
- docs/GETTING-STARTED.md
|
||||||
|
autonomous: true
|
||||||
|
requirements: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "pnpm md:lint exits 0 on the current repo (13 baseline violations fixed)"
|
||||||
|
- "Introducing a bare fenced code block (MD040) makes pnpm md:lint exit non-zero"
|
||||||
|
- "The fast-checks CI job runs a Markdown lint step between Format check and Typecheck"
|
||||||
|
- "pnpm format:check still exits 0 after the MD031 blank-line fixes (no Prettier conflict)"
|
||||||
|
- "markdownlint never scans .planning/** (excluded by config ignores)"
|
||||||
|
artifacts:
|
||||||
|
- path: ".markdownlint-cli2.jsonc"
|
||||||
|
provides: "markdownlint-cli2 config (extends prettier preset; content rules; globs + ignores)"
|
||||||
|
contains: "markdownlint/style/prettier"
|
||||||
|
- path: "package.json"
|
||||||
|
provides: "md:lint script + markdownlint-cli2 devDependency"
|
||||||
|
contains: "md:lint"
|
||||||
|
- path: ".gitea/workflows/ci.yml"
|
||||||
|
provides: "Markdown lint step in fast-checks job"
|
||||||
|
contains: "pnpm md:lint"
|
||||||
|
key_links:
|
||||||
|
- from: ".gitea/workflows/ci.yml"
|
||||||
|
to: "package.json md:lint script"
|
||||||
|
via: "fast-checks step runs pnpm md:lint"
|
||||||
|
pattern: "pnpm md:lint"
|
||||||
|
- from: "package.json md:lint script"
|
||||||
|
to: ".markdownlint-cli2.jsonc"
|
||||||
|
via: "markdownlint-cli2 auto-discovers root config (globs + ignores)"
|
||||||
|
pattern: "markdownlint-cli2"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Add a real markdown lint gate to the existing `fast-checks` CI job: install `markdownlint-cli2`, create the root `.markdownlint-cli2.jsonc` config (Prettier-compatible preset + content rules + scoped globs that exclude `.planning/**`), add a root `md:lint` script, wire a "Markdown lint" step into `fast-checks`, and fix the 13 baseline violations so the gate starts GREEN.
|
||||||
|
|
||||||
|
This delivers Success Criterion 4: `fast-checks` runs markdownlint over docs; an introduced violation fails the gate; the existing baseline passes.
|
||||||
|
|
||||||
|
Purpose: Docs get a fast but real format+lint gate without a separate CI job, scoped so churny `.planning/**` bookkeeping is never linted.
|
||||||
|
Output: `.markdownlint-cli2.jsonc`, updated `package.json` (script + devDep), one new `fast-checks` step, 7 doc files fixed (13 violations), green `pnpm md:lint` + `pnpm format:check`.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="execute">
|
||||||
|
<name>Task 1: Install markdownlint-cli2, add md:lint script, create .markdownlint-cli2.jsonc</name>
|
||||||
|
<read_first>
|
||||||
|
- package.json (root — current scripts block lines 6-16 and devDependencies lines 17-25; mirror the existing `format:check` script + `prettier` devDep placement)
|
||||||
|
- .prettierrc (root JSON tool-config structural analog: printWidth 100 — the reason MD013 must stay disabled)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 5 + "Code Examples" → the exact `.markdownlint-cli2.jsonc` body and rule-decision table; Pitfall 5 `#` vs `!` negation; Pitfall 6 .pnpm-store)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`.markdownlint-cli2.jsonc` (new file)" and "package.json (root)" sections — exact insertion points)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Install `markdownlint-cli2` at version `0.22.1` as a root-workspace devDependency: run `pnpm add -D markdownlint-cli2@0.22.1 --workspace-root` (this updates root package.json devDependencies and pnpm-lock.yaml). Add a root script `"md:lint": "markdownlint-cli2"` immediately after the existing `"format:check"` script — NO glob args on the CLI; globs/ignores live in the config file so `pnpm md:lint` and a bare `npx markdownlint-cli2` resolve identically (RESEARCH Open Question 3 resolved).
|
||||||
|
|
||||||
|
Create `.markdownlint-cli2.jsonc` at the repo root using the exact body from RESEARCH "Code Examples → .markdownlint-cli2.jsonc" / PATTERNS "`.markdownlint-cli2.jsonc` (new file)". Required keys: `config.extends` = `"markdownlint/style/prettier"`; ENABLE `MD001`, `MD024`, `MD040`, `MD031`, `MD051`, `MD052` (all `true`); DISABLE `MD041`, `MD034`, `MD036` (all `false`) — rationale per the rule-decision table (MD041 because CLAUDE.md starts with `## Project`; MD034 because the version table uses `pkg@version` syntax; MD036 because docs/API.md uses bold response labels). Set `globs` to exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]` and `ignores` to exactly `[".planning/**", "node_modules/**", "**/node_modules/**", ".pnpm-store/**"]`. Do NOT add MD013 (line-length) — it is disabled by the prettier preset and re-enabling it produces 700+ false violations against `.prettierrc` printWidth 100.
|
||||||
|
|
||||||
|
Do NOT run md:lint to green yet — the 13 baseline violations are fixed in Task 2; this task may leave `pnpm md:lint` red.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>test -f .markdownlint-cli2.jsonc && node -e "const p=require('./package.json'); if(!p.scripts['md:lint']) process.exit(1); if(!p.devDependencies['markdownlint-cli2']) process.exit(1); console.log('script+dep present')" && grep -q 'markdownlint/style/prettier' .markdownlint-cli2.jsonc && grep -q '.planning/' .markdownlint-cli2.jsonc && echo OK</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- package.json `scripts` contains `"md:lint": "markdownlint-cli2"` (no glob args)
|
||||||
|
- package.json `devDependencies` contains `markdownlint-cli2` at `0.22.1`; pnpm-lock.yaml updated
|
||||||
|
- `.markdownlint-cli2.jsonc` exists at repo root and `config.extends` is `"markdownlint/style/prettier"`
|
||||||
|
- `.markdownlint-cli2.jsonc` enables MD001/MD024/MD040/MD031/MD051/MD052 and disables MD041/MD034/MD036
|
||||||
|
- `.markdownlint-cli2.jsonc` `ignores` array includes `".planning/**"`, `"node_modules/**"`, `"**/node_modules/**"`, `".pnpm-store/**"`
|
||||||
|
- `.markdownlint-cli2.jsonc` `globs` array is exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]`
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>markdownlint-cli2@0.22.1 is a root devDep, `pnpm md:lint` is wired to the root config, and the config scopes the lint to docs/repo-root/app markdown while excluding `.planning/**`. md:lint may still be red (fixed next task).</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="execute">
|
||||||
|
<name>Task 2: Fix the 13 baseline markdown violations; green md:lint + format:check; wire the fast-checks step</name>
|
||||||
|
<read_first>
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md ("Baseline Violation Assessment" table — exact file/rule/count; Pitfall 4 — verify MD031 fix does not break Prettier)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("Docs content fixes" table + MD040/MD031 fix patterns; "Markdown lint step in fast-checks" insertion slot)
|
||||||
|
- .gitea/workflows/ci.yml (fast-checks job lines 8-38 — confirm the real step names: "Format check" line 31-32, "Typecheck" line 34; insert the new step between them)
|
||||||
|
- README.md (lines ~78, ~119 — the 2 bare fences)
|
||||||
|
- docs/API.md (lines ~517, ~544), docs/ARCHITECTURE.md (lines ~64, ~171), docs/DEVELOPMENT.md (lines ~9, ~22) — 2 bare fences each
|
||||||
|
- docs/GETTING-STARTED.md (lines ~52, ~54 — the MD031 fence pair needing blank lines)
|
||||||
|
- apps/api/README.md (line ~19), apps/pwa/README.md (line ~45), apps/pwa/e2e/README.md (line ~76) — 1 bare fence each
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Fix all 13 violations confirmed at these exact locations (run `pnpm md:lint` first to re-confirm line numbers, then fix):
|
||||||
|
- MD040 (11 bare fences — add a language tag matching the fence content; use `bash` for shell, `text` for plain output, `json`/`yaml`/`ts` as appropriate): apps/api/README.md:19, apps/pwa/e2e/README.md:76, apps/pwa/README.md:45, docs/API.md:517, docs/API.md:544, docs/ARCHITECTURE.md:64, docs/ARCHITECTURE.md:171, docs/DEVELOPMENT.md:9, docs/DEVELOPMENT.md:22, README.md:78, README.md:119.
|
||||||
|
- MD031 (1 fence pair — add a blank line before the opening ```` ```bash ```` and after the closing ```` ``` ````): docs/GETTING-STARTED.md:52 and :54.
|
||||||
|
|
||||||
|
Only change what each rule requires — add a language token after the opening backticks (MD040) or add surrounding blank lines (MD031); do NOT rewrite fence bodies or restructure docs. Do not "fix" any rule that the config disables (MD013/MD034/MD036/MD041) — those are intentionally off.
|
||||||
|
|
||||||
|
After fixes, add the new step to the `fast-checks` job in ci.yml between the "Format check" step and the "Typecheck" step — a named step `Markdown lint` running `pnpm md:lint` (mirror the one-line `Format check` step pattern exactly). This is the only ci.yml change in this plan; do not touch the api/harness/changes/gate jobs (Plan 02 owns those).
|
||||||
|
|
||||||
|
Then run BOTH `pnpm md:lint` (must exit 0) AND `pnpm format:check` (must exit 0) — per RESEARCH Pitfall 4, the MD031 blank-line additions must not introduce a Prettier conflict on docs/GETTING-STARTED.md. If format:check newly fails on a file you touched, run `pnpm format` on that file and re-confirm md:lint is still 0.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>pnpm md:lint && pnpm format:check && grep -q 'pnpm md:lint' .gitea/workflows/ci.yml && echo OK</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- `pnpm md:lint` exits 0 (13 baseline violations resolved)
|
||||||
|
- `pnpm format:check` exits 0 (no Prettier conflict introduced by the MD031 fix)
|
||||||
|
- `.gitea/workflows/ci.yml` fast-checks job contains a step named `Markdown lint` running `pnpm md:lint`, positioned after the `Format check` step and before the `Typecheck` step
|
||||||
|
- Re-running md:lint after temporarily inserting a bare ```` ``` ```` fence into any in-scope .md (then reverting) exits non-zero (gate can fail) — proven during execution, not left in the tree
|
||||||
|
- No api/harness/changes/gate job was modified by this plan
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>The markdown baseline is clean, `fast-checks` runs `pnpm md:lint`, the gate can fail on a real violation, and Prettier and markdownlint do not conflict. SC-4 satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<artifacts_produced>
|
||||||
|
This phase introduces the following new symbols (Plan 01 portion). The plan-review source-grounding pass must treat these as newly-created, not drift:
|
||||||
|
|
||||||
|
- `.markdownlint-cli2.jsonc` — new root config file
|
||||||
|
- `md:lint` — new root `package.json` script
|
||||||
|
- `markdownlint-cli2` — new root devDependency (0.22.1)
|
||||||
|
- `Markdown lint` — new step name in the `fast-checks` job
|
||||||
|
</artifacts_produced>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| npm registry → repo devDependencies | `markdownlint-cli2` is fetched and runs in CI and on developer machines |
|
||||||
|
| markdown content → lint tool | doc files are the input; markdownlint only reads, never executes content |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-15-01 | Tampering (supply chain) | new `markdownlint-cli2` devDependency | mitigate | Pin to exact `0.22.1` (no `^`/`~`); RESEARCH Package Legitimacy Audit verdict OK (~4 yrs, ~3M/wk, DavidAnson/markdownlint-cli2). Audit table present in RESEARCH; no `[ASSUMED]`/`[SUS]` packages so no blocking-human checkpoint required. |
|
||||||
|
| T-15-02 | Denial of Service (gate noise) | lint glob accidentally scanning `.planning/**` or `node_modules` | accept→mitigate | `ignores` excludes `.planning/**`, `node_modules/**`, `**/node_modules/**`, `.pnpm-store/**`; verified the glob lints exactly 12 in-scope files, not bookkeeping churn. |
|
||||||
|
| T-15-03 | Tampering (false-green) | a disabled content rule silently hides a real doc defect | accept | Rule-disable decisions (MD034/MD036/MD041) are scoped to non-author-controlled patterns documented in RESEARCH; MD040/MD031/MD001/MD024/MD051/MD052 stay enabled to catch broken fences/links. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- `pnpm md:lint` exits 0 on the clean tree; exits non-zero when a bare fence is introduced.
|
||||||
|
- `pnpm format:check` exits 0 (Prettier/markdownlint do not conflict).
|
||||||
|
- `.gitea/workflows/ci.yml` fast-checks job runs `pnpm md:lint` between Format check and Typecheck.
|
||||||
|
- markdownlint scans 12 in-scope files and never touches `.planning/**` (confirm "Finding:" line lists the negated ignores).
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
Maps to Phase 15 Success Criterion 4: `fast-checks` runs markdownlint-cli2 over the docs glob; an introduced markdown-lint violation fails the gate; the existing markdown baseline passes (13 violations fixed, rules configured) so the gate starts green.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md` when done.
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on:
|
||||||
|
- 15-01
|
||||||
|
files_modified:
|
||||||
|
- .gitea/workflows/ci.yml
|
||||||
|
autonomous: true
|
||||||
|
requirements: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "A doc-only PR to main skips the api and harness jobs but still runs fast-checks"
|
||||||
|
- "A PR touching code runs fast-checks, api, and harness; a failure in any blocks the gate"
|
||||||
|
- "An always-running gate job reports CI / gate and passes only when fast-checks succeeded and each heavy job is success OR skipped"
|
||||||
|
- "The gate fails (exit 1) when fast-checks fails or when api/harness fails or is cancelled"
|
||||||
|
artifacts:
|
||||||
|
- path: ".gitea/workflows/ci.yml"
|
||||||
|
provides: "changes job (paths-filter), conditional api/harness, always-running gate aggregate"
|
||||||
|
contains: "dorny/paths-filter@v4"
|
||||||
|
key_links:
|
||||||
|
- from: "ci.yml api/harness jobs"
|
||||||
|
to: "ci.yml changes job output"
|
||||||
|
via: "needs: [changes] + if needs.changes.outputs.code == 'true'"
|
||||||
|
pattern: "needs.changes.outputs.code"
|
||||||
|
- from: "ci.yml gate job"
|
||||||
|
to: "ci.yml fast-checks/api/harness results"
|
||||||
|
via: "needs: [fast-checks, changes, api, harness] + if: always() + per-job needs.X.result checks"
|
||||||
|
pattern: "needs.fast-checks.result"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Restructure `.gitea/workflows/ci.yml` so doc-only PRs skip the slow `api` and `harness` jobs without deadlocking branch protection: add a `changes` job (`dorny/paths-filter@v4`) that emits a `code` output, gate `api`/`harness` on `needs.changes.outputs.code == 'true'`, and add an always-running `gate` aggregate job that branch protection can require in place of the heavy jobs directly.
|
||||||
|
|
||||||
|
This delivers Success Criteria 1 and 2 fully, and lands the `CI / gate` status that Success Criterion 3 (Plan 03's branch-protection checkpoint) requires to exist first.
|
||||||
|
|
||||||
|
Purpose: Doc-only PRs go from ~5 min to ~30s while a single always-reporting `gate` keeps the merge gated. The conditional skip and the gate are interdependent and ship together in one PR so the heavy jobs can actually be skipped while the gate still reports.
|
||||||
|
Output: One modified file — `ci.yml` with a new `changes` job, `needs`/`if` on `api` and `harness`, and a new `gate` job.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="execute">
|
||||||
|
<name>Task 1: Add the changes job and gate api/harness on the code output</name>
|
||||||
|
<read_first>
|
||||||
|
- .gitea/workflows/ci.yml (FULL file — current job names `fast-checks`/`api`/`harness`; api header lines 40-44 with existing `if: github.event_name == 'pull_request'`; harness header lines 126-129; runner label `ubuntu-latest`; both heavy jobs run in parallel with no `needs:` today)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 1 changes job + Pattern 2 conditional heavy jobs + "Code Examples → Complete changes job"; Pitfall 3 permissions; Gitea-Specific Notes #5/#6 action resolution + runner label)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`changes` job (new)", "`api` job modification", "`harness` job modification" — exact YAML and insertion ordering: changes BEFORE fast-checks)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Insert a new `changes` job (use the exact YAML from RESEARCH "Code Examples → Complete changes job" / PATTERNS "`changes` job (new)") positioned BEFORE `fast-checks` so the UI ordering reads changes → fast-checks/api/harness → gate. The job: `runs-on: ubuntu-latest`; `if: github.event_name == 'pull_request'`; `permissions: pull-requests: read` (job-scoped, required by paths-filter v4 — Pitfall 3); `outputs.code: ${{ steps.filter.outputs.code }}`; a single step `uses: dorny/paths-filter@v4` with `id: filter`. The `code` filter must list exactly the positive code patterns from RESEARCH: `**/*.ts`, `**/*.tsx`, `**/*.js`, `**/*.json`, `**/*.yaml`, `**/*.yml`, `apps/**`, `packages/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml`. Define the POSITIVE `code` filter (not a `docs` filter) so `code == 'false'` means doc-only and any new/ambiguous file type defaults to the full gate. No `actions/checkout` and no `fetch-depth` — paths-filter uses the PR REST API on `pull_request` events.
|
||||||
|
|
||||||
|
Modify the `api` job header: add `needs: [changes]` and change its `if` to `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'`. EXTEND the existing event guard with `&&` — do not replace it (replacing loses the event-type guard). Update the inline comment to say the job is skipped for doc-only PRs. Change nothing else in the api job body (services, env, steps, mariadb wait, migrate, test).
|
||||||
|
|
||||||
|
Modify the `harness` job header identically: `needs: [changes]` + the same combined `if`. Update its comment; change nothing else in the harness body.
|
||||||
|
|
||||||
|
Pin the third-party action to the `@v4` tag exactly as written (`dorny/paths-filter@v4`); do not float to a branch.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>command -v yq >/dev/null 2>&1 && yq -e '.jobs.changes' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.changes.outputs.code' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.api.needs | contains(["changes"])' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.harness.needs | contains(["changes"])' .gitea/workflows/ci.yml >/dev/null && grep -q 'dorny/paths-filter@v4' .gitea/workflows/ci.yml && grep -q "needs.changes.outputs.code == 'true'" .gitea/workflows/ci.yml && echo OK</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- ci.yml contains a job named `changes` using `dorny/paths-filter@v4` with `id: filter`
|
||||||
|
- the `changes` job declares `permissions: pull-requests: read` (job-scoped) and `outputs.code: ${{ steps.filter.outputs.code }}`
|
||||||
|
- the `changes` job has NO `actions/checkout` step
|
||||||
|
- the `code` filter lists the positive code patterns (`**/*.ts`, `apps/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml`, etc.) — not a `docs` filter
|
||||||
|
- the `api` job has `needs: [changes]` and its `if` is `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'`
|
||||||
|
- the `harness` job has `needs: [changes]` and the same combined `if`
|
||||||
|
- the api/harness `services`, `env`, and step bodies are unchanged from the pre-edit file
|
||||||
|
- ci.yml parses as valid YAML (`yq` exits 0)
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>The `changes` job classifies each PR; `api` and `harness` skip on doc-only PRs and run on code PRs. SC-1/SC-2 job-skip behavior is wired.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="execute">
|
||||||
|
<name>Task 2: Add the always-running gate aggregate job (Gitea-safe per-job result checks)</name>
|
||||||
|
<read_first>
|
||||||
|
- .gitea/workflows/ci.yml (post-Task-1 state — confirm final job names `fast-checks`/`changes`/`api`/`harness`; gate goes at end of file)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 3 gate job + "Code Examples → Gate job"; Pitfall 2 — `contains(needs.*.result, ...)` is BROKEN on Gitea 1.26.2 issue #31007; Gitea-Specific Notes #1 skipped-status quirk, #2 if:always() fixed in 1.21.8, #3 contains bug)
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`gate` job (new)" — exact YAML and the publish.yml `if: always()` analog)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Append a new `gate` job at the end of ci.yml using the exact YAML from RESEARCH "Code Examples → Gate job" / PATTERNS "`gate` job (new)". The job: `runs-on: ubuntu-latest`; `needs: [fast-checks, changes, api, harness]`; `if: always()` (job-scoped — without it a skipped upstream skips the gate and branch protection on `CI / gate` deadlocks; the if:always() deadlock bug was fixed in Gitea 1.21.8 and this instance is 1.26.2). One step `Check all required jobs passed or were skipped` running a bash script that: (1) fails with exit 1 if `${{ needs.fast-checks.result }}` is not `success` (fast-checks always runs — never skipped); (2) loops over `${{ needs.api.result }}` and `${{ needs.harness.result }}` and fails with exit 1 if either is neither `success` NOR `skipped`; (3) echoes a pass message otherwise.
|
||||||
|
|
||||||
|
CRITICAL Gitea 1.26.2 constraint: do NOT use `contains(needs.*.result, 'success')` or any `needs.*.result` wildcard — issue #31007 makes the wildcard return false even when jobs succeed. Reference each job result individually via `needs.fast-checks.result`, `needs.api.result`, `needs.harness.result`. Treat `skipped` as acceptable ONLY for `api`/`harness` (the conditionally-skippable jobs), never for `fast-checks`. Do not add `changes` to the pass/fail evaluation logic — it is in `needs` for ordering only; its result is not gated (a failure there already fails downstream `if` evaluation).
|
||||||
|
|
||||||
|
This is the change-set that, once merged, makes Gitea start emitting a `CI / gate` commit-status — the prerequisite for Plan 03's branch-protection update. Do not touch branch protection here.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>command -v yq >/dev/null 2>&1 && yq -e '.jobs.gate' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.gate.needs | contains(["fast-checks","changes","api","harness"])' .gitea/workflows/ci.yml >/dev/null && yq -e '.jobs.gate.if == "always()"' .gitea/workflows/ci.yml >/dev/null && grep -q 'needs.fast-checks.result' .gitea/workflows/ci.yml && grep -q 'needs.api.result' .gitea/workflows/ci.yml && grep -q 'needs.harness.result' .gitea/workflows/ci.yml && ! grep -q 'needs.\*.result' .gitea/workflows/ci.yml && echo OK</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- ci.yml contains a job named `gate` with `if: always()` and `needs: [fast-checks, changes, api, harness]`
|
||||||
|
- the gate references `needs.fast-checks.result`, `needs.api.result`, and `needs.harness.result` individually
|
||||||
|
- the gate does NOT contain `contains(needs.*.result` or any `needs.*.result` wildcard
|
||||||
|
- the gate fails (exit 1) when fast-checks != success
|
||||||
|
- the gate accepts `success` OR `skipped` for api and harness, and fails on any other result
|
||||||
|
- ci.yml parses as valid YAML (`yq` exits 0)
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>An always-running `gate` job aggregates the four jobs using Gitea-safe per-job result checks, passing when fast-checks succeeds and each heavy job is success-or-skipped. Once merged it emits `CI / gate`, unblocking Plan 03. SC-3's gating surface exists in YAML.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<artifacts_produced>
|
||||||
|
This phase introduces the following new symbols (Plan 02 portion). The plan-review source-grounding pass must treat these as newly-created, not drift:
|
||||||
|
|
||||||
|
- `changes` — new ci.yml job name (dorny/paths-filter@v4)
|
||||||
|
- `code` — new paths-filter output name consumed by `api`/`harness` `if:`
|
||||||
|
- `gate` — new ci.yml aggregate job name (the always-running required surface)
|
||||||
|
- `CI / gate` — new commit-status context Gitea emits once this lands (required by Plan 03)
|
||||||
|
- `dorny/paths-filter@v4` — newly-referenced third-party action
|
||||||
|
</artifacts_produced>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| PR author → CI gating decision | a PR's changed-file set decides whether the heavy code jobs run — the core security-relevant invariant of this phase |
|
||||||
|
| github.com → CI runner | `dorny/paths-filter@v4` is a third-party action resolved and executed on the runner with a scoped token |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-15-04 | Elevation of Privilege / bypass (CORE) | a misconfigured gate passes when fast-checks fails, letting unreviewed code merge | mitigate | gate fails (exit 1) on `fast-checks != success`; `skipped` accepted ONLY for api/harness, never fast-checks; uses individual `needs.X.result` (not the broken Gitea `contains(needs.*.result)`) so it cannot silently always-pass (Pitfall 2). |
|
||||||
|
| T-15-05 | Spoofing (skip-detection) | a PR that actually changes code is misclassified doc-only and skips api/harness | mitigate | `code` is the POSITIVE filter — any new/ambiguous file type matches `code` and runs the full gate; doc-only requires EVERY changed file to fall outside the code patterns; `code` includes `**/*.ts`, `apps/**`, lockfile, Dockerfile, compose. |
|
||||||
|
| T-15-06 | Tampering (supply chain) | `dorny/paths-filter@v4` third-party action | mitigate | Pinned to the `@v4` tag; RESEARCH Package Legitimacy Audit verdict OK (~5 yrs, widely used, github.com/dorny/paths-filter); runs with `pull-requests: read` only — no write, no secrets access (RESEARCH Security Domain). |
|
||||||
|
| T-15-07 | Denial of Service (deadlock) | a required check that never reports blocks the PR forever | mitigate | api/harness are NEVER added as required checks (Plan 03); the only gating surfaces are `fast-checks` (always runs) and `gate` (`if: always()`, always reports); no workflow-level `on: paths` filter on any required job. |
|
||||||
|
| T-15-SC | Tampering | npm/action installs | mitigate | No package-manager install task in this plan (paths-filter is a `uses:` action, not an npm dep; markdownlint-cli2 install is Plan 01). RESEARCH Package Legitimacy Audit present; no `[ASSUMED]`/`[SUS]` packages → no blocking-human checkpoint required. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Behavioral (observable in CI after this PR merges and on a follow-up test PR — see 15-VALIDATION.md):
|
||||||
|
- Doc-only PR: `changes` emits `code=false`; `api`/`harness` show skipped; `fast-checks` runs; `gate` passes.
|
||||||
|
- Code PR: `changes` emits `code=true`; all three run; `gate` passes when green, fails when any heavy job fails.
|
||||||
|
- Gate-fail path: a deliberately failing fast-checks (or api/harness) makes `gate` exit 1 → merge blocked.
|
||||||
|
|
||||||
|
Static (pre-merge):
|
||||||
|
- `yq` parses ci.yml; `changes`/`gate` jobs present; api/harness carry `needs: [changes]` + combined `if`; gate uses individual `needs.X.result`, no wildcard.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
Maps to Phase 15 Success Criteria 1, 2, and the YAML half of 3:
|
||||||
|
- SC-1: doc-only PR skips api/harness, still runs fast-checks.
|
||||||
|
- SC-2: code PR runs all three; a failure blocks the merge (via the gate).
|
||||||
|
- SC-3 (YAML half): an always-running `CI / gate` aggregate exists that passes when each heavy job is success-or-skipped; the branch-protection required-check change is Plan 03.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-02-SUMMARY.md` when done.
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs
|
||||||
|
plan: 03
|
||||||
|
type: execute
|
||||||
|
wave: 3
|
||||||
|
depends_on:
|
||||||
|
- 15-02
|
||||||
|
files_modified:
|
||||||
|
- .gitea/workflows/publish.yml
|
||||||
|
autonomous: false
|
||||||
|
requirements: []
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Branch protection on main requires CI / fast-checks + CI / gate"
|
||||||
|
- "Branch protection on main no longer requires CI / api or CI / harness directly"
|
||||||
|
- "A doc-only PR (which skips api/harness) is mergeable — no missing-required-check deadlock"
|
||||||
|
- "publish.yml's safety-gate comment names the new required checks (fast-checks + gate)"
|
||||||
|
artifacts:
|
||||||
|
- path: ".gitea/workflows/publish.yml"
|
||||||
|
provides: "Updated safety-gate comment naming the new required checks"
|
||||||
|
contains: "CI / gate"
|
||||||
|
key_links:
|
||||||
|
- from: "Gitea branch-protection (main)"
|
||||||
|
to: "ci.yml gate job"
|
||||||
|
via: "required status check 'CI / gate' (always-running aggregate)"
|
||||||
|
pattern: "CI / gate"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Finalize the gating surface: update Gitea branch protection on `main` to require `CI / fast-checks` + `CI / gate` and DROP the now-skippable `CI / api` and `CI / harness` direct requirements, and update the `publish.yml` safety-gate comment to name the new required checks.
|
||||||
|
|
||||||
|
This delivers Success Criterion 3 — the only step that makes doc-only PRs actually mergeable without a missing-required-check deadlock.
|
||||||
|
|
||||||
|
Purpose: With the heavy jobs conditionally skipped (Plan 02), a doc-only PR no longer emits `CI / api` / `CI / harness` statuses; leaving them required would deadlock the merge. The always-running `CI / gate` is the correct gating surface.
|
||||||
|
Output: A Gitea branch-protection change (operator checkpoint — not automatable in YAML) + a one-comment edit to publish.yml.
|
||||||
|
|
||||||
|
ORDERING HAZARD (read before executing): This plan MUST run only AFTER Plan 02's ci.yml change has merged to `main` and Gitea has emitted at least one `CI / gate` commit-status. Dropping `CI / api` + `CI / harness` before `CI / gate` exists would leave `main` with no valid heavy-job gate, and adding `CI / gate` as required before it has ever reported can itself block PRs. Confirm `CI / gate` has appeared on a recent run before changing branch protection.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||||
|
@$HOME/.claude/gsd-core/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md
|
||||||
|
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="execute">
|
||||||
|
<name>Task 1: Update the publish.yml safety-gate comment to name the new required checks</name>
|
||||||
|
<read_first>
|
||||||
|
- .gitea/workflows/publish.yml (lines 13-17 — the "Safety gate" comment block currently naming "the three required checks (CI / fast-checks, CI / api, CI / harness)")
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("publish.yml Comment Update (advisory)" section — exact current text and the required-check change)
|
||||||
|
</read_first>
|
||||||
|
<action>
|
||||||
|
Edit ONLY the safety-gate comment in publish.yml (lines ~14-17). Change the clause that names "the three required checks (CI / fast-checks, CI / api, CI / harness)" to name the new required checks: `CI / fast-checks` and `CI / gate`. Reflect that `api`/`harness` are now conditionally skipped and gated via the always-running `CI / gate` aggregate rather than being required directly. Do not change any publish.yml job, step, env, or trigger — comment text only. This is the documentation half; the actual protection change is Task 2 (operator).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>grep -q 'CI / gate' .gitea/workflows/publish.yml && grep -q 'CI / fast-checks' .gitea/workflows/publish.yml && ! grep -qE 'three required checks \(CI / fast-checks, CI / api, CI / harness\)' .gitea/workflows/publish.yml && command -v yq >/dev/null 2>&1 && yq -e '.jobs.publish' .gitea/workflows/publish.yml >/dev/null && echo OK</automated>
|
||||||
|
</verify>
|
||||||
|
<acceptance_criteria>
|
||||||
|
- publish.yml safety-gate comment names `CI / fast-checks` and `CI / gate` as the required checks
|
||||||
|
- publish.yml safety-gate comment no longer asserts `CI / api` and `CI / harness` are required (the old "three required checks" line is replaced)
|
||||||
|
- no publish.yml job/step/env/trigger changed (only comment text); publish.yml still parses as valid YAML
|
||||||
|
</acceptance_criteria>
|
||||||
|
<done>publish.yml's safety-gate rationale matches the new branch-protection reality.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-action" gate="blocking">
|
||||||
|
<name>Task 2: Operator — update Gitea branch protection on main (drop api/harness, require gate)</name>
|
||||||
|
<read_first>
|
||||||
|
- .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Gitea-Specific Notes #4 — branch-protection required-check update is a MANUAL Gitea admin step; Pitfall 1 — required-check deadlock from skipped jobs)
|
||||||
|
</read_first>
|
||||||
|
<what-built>
|
||||||
|
Plan 02 added an always-running `CI / gate` aggregate job to ci.yml and made `CI / api` / `CI / harness` conditionally skipped on doc-only PRs. Once Plan 02 merged, Gitea emits a `CI / gate` commit-status on every PR run. Branch protection still requires the old `CI / api` + `CI / harness` contexts, which a doc-only PR will never emit — so until you make this change, doc-only PRs deadlock on "missing required checks". This change is a Gitea admin action that cannot be done in a workflow file.
|
||||||
|
</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
PRECONDITION — confirm first: open the most recent PR run in Gitea Actions for this repo and verify a `CI / gate` job ran and reported a status. Do NOT proceed until `CI / gate` has appeared at least once (Plan 02 must already be merged to main). If it has not appeared, stop — the protection change is premature.
|
||||||
|
|
||||||
|
Then update branch protection:
|
||||||
|
1. Gitea → this repo → Settings → Branches → edit the protection rule for `main`.
|
||||||
|
2. Under "Status Check Patterns" / required status checks, set the required contexts to EXACTLY these two:
|
||||||
|
- `CI / fast-checks`
|
||||||
|
- `CI / gate`
|
||||||
|
3. REMOVE these two from the required list (they are now conditionally skipped and gated via `CI / gate`):
|
||||||
|
- `CI / api`
|
||||||
|
- `CI / harness`
|
||||||
|
4. Save the protection rule.
|
||||||
|
(Alternative: the `tea` CLI / Gitea API can set branch-protection `status_check_contexts` to `["CI / fast-checks", "CI / gate"]` — login Bergerhouse — if you prefer not to use the UI.)
|
||||||
|
|
||||||
|
VERIFY the change end-to-end:
|
||||||
|
- Open a throwaway DOC-ONLY PR (edit only a `*.md` under docs/ or the repo root). Expect: `fast-checks` runs, `api`/`harness` show skipped, `CI / gate` passes, and the PR is MERGEABLE (no "missing required checks"). This proves SC-1 + SC-3.
|
||||||
|
- Open a throwaway CODE PR (touch a `*.ts` file). Expect: `fast-checks`, `api`, `harness`, and `CI / gate` all run; the PR is mergeable only when all are green. This proves SC-2.
|
||||||
|
- Close both throwaway PRs without merging.
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" once branch protection requires exactly `CI / fast-checks` + `CI / gate`, `CI / api` and `CI / harness` are no longer required, and the doc-only + code throwaway PRs behaved as described. Or describe what differed.</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<artifacts_produced>
|
||||||
|
This phase introduces the following new configuration state (Plan 03 portion). The plan-review source-grounding pass must treat these as newly-created, not drift:
|
||||||
|
|
||||||
|
- Branch-protection required checks on `main` changed to `CI / fast-checks` + `CI / gate` (Gitea admin state, not a repo file)
|
||||||
|
- publish.yml safety-gate comment updated to name the new required checks
|
||||||
|
</artifacts_produced>
|
||||||
|
|
||||||
|
<threat_model>
|
||||||
|
## Trust Boundaries
|
||||||
|
|
||||||
|
| Boundary | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| operator → Gitea branch-protection config | the required-check set decides whether unreviewed/failing code can merge to main |
|
||||||
|
| skipped heavy job → branch-protection check list | a skipped job may emit no commit-status; a still-required skipped context would deadlock |
|
||||||
|
|
||||||
|
## STRIDE Threat Register
|
||||||
|
|
||||||
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||||
|
|-----------|----------|-----------|-------------|-----------------|
|
||||||
|
| T-15-08 | Denial of Service (deadlock) | leaving `CI / api`/`CI / harness` required after the skip logic ships | mitigate | This plan explicitly drops both from required checks and requires the always-running `CI / gate` instead (RESEARCH Pitfall 1 / Gitea-Specific Note #4). |
|
||||||
|
| T-15-09 | Elevation of Privilege (bypass window) | dropping the heavy-job requirements BEFORE `CI / gate` exists leaves main ungated for code | mitigate | Ordering hazard called out: this plan is wave 3, `depends_on: [15-02]`; the checkpoint's PRECONDITION requires confirming `CI / gate` has reported at least once before any required-check edit. |
|
||||||
|
| T-15-10 | Tampering (false sense of gating) | publish.yml comment still claims api/harness are required, misleading a future maintainer | mitigate | Task 1 updates the comment to name `CI / fast-checks` + `CI / gate`. |
|
||||||
|
</threat_model>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
- Gitea branch protection for `main` requires exactly `CI / fast-checks` + `CI / gate`; `CI / api` and `CI / harness` are not required.
|
||||||
|
- Throwaway doc-only PR is mergeable with api/harness skipped (SC-1/SC-3).
|
||||||
|
- Throwaway code PR runs and gates on all jobs (SC-2).
|
||||||
|
- publish.yml safety-gate comment names the new required checks.
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
Maps to Phase 15 Success Criterion 3: branch protection requires `CI / fast-checks` + an always-running `CI / gate` aggregate; the direct `api`/`harness` requirements are dropped so a skipped heavy job never deadlocks the merge. (SC-1 doc-only mergeability and SC-2 code-PR gating are confirmed end-to-end here against the live protection rule.)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-03-SUMMARY.md` when done.
|
||||||
|
</output>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
phase: 15
|
phase: 15
|
||||||
slug: ci-skip-api-harness-jobs-for-doc-only-prs
|
slug: ci-skip-api-harness-jobs-for-doc-only-prs
|
||||||
status: draft
|
status: draft
|
||||||
nyquist_compliant: false
|
nyquist_compliant: true
|
||||||
wave_0_complete: false
|
wave_0_complete: false
|
||||||
created: 2026-06-12
|
created: 2026-06-12
|
||||||
---
|
---
|
||||||
@@ -11,34 +11,43 @@ created: 2026-06-12
|
|||||||
|
|
||||||
> Per-phase validation contract for feedback sampling during execution.
|
> Per-phase validation contract for feedback sampling during execution.
|
||||||
|
|
||||||
|
This phase is CI workflow config + tool config + mechanical markdown fixes. There is
|
||||||
|
no application business logic; the gate-job result logic is validated by static YAML
|
||||||
|
checks plus real CI behavior on doc-only vs code PRs (not a unit-test framework).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Test Infrastructure
|
## Test Infrastructure
|
||||||
|
|
||||||
| Property | Value |
|
| Property | Value |
|
||||||
|----------|-------|
|
|----------|-------|
|
||||||
| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} |
|
| **Framework** | Static (markdownlint-cli2 + Prettier + `yq`/`grep`) for local checks; Gitea Actions CI (behavioral) for SC-1/2/3 |
|
||||||
| **Config file** | {path or "none — Wave 0 installs"} |
|
| **Config file** | `.markdownlint-cli2.jsonc` (new, Plan 01); `.gitea/workflows/ci.yml` (modified, Plans 01/02) |
|
||||||
| **Quick run command** | `{quick command}` |
|
| **Quick run command** | `pnpm md:lint && pnpm format:check` |
|
||||||
| **Full suite command** | `{full command}` |
|
| **Full suite command** | `pnpm md:lint && pnpm format:check` locally; then a doc-only PR + a code PR observed in Gitea Actions |
|
||||||
| **Estimated runtime** | ~{N} seconds |
|
| **Estimated runtime** | local ~3s; CI doc-only PR ~30s; CI code PR ~5min |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sampling Rate
|
## Sampling Rate
|
||||||
|
|
||||||
- **After every task commit:** Run `{quick run command}`
|
- **After every task commit:** Run `pnpm md:lint` (Plan 01) or `yq -e '.jobs.<job>' .gitea/workflows/ci.yml` (Plan 02).
|
||||||
- **After every plan wave:** Run `{full suite command}`
|
- **After every plan wave:** Run `pnpm md:lint && pnpm format:check`; for Plan 02, parse ci.yml with `yq` and grep the gate's per-job result checks.
|
||||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
- **Before `/gsd-verify-work`:** local checks green; CI behavioral checks observed on throwaway PRs (Plan 03 checkpoint).
|
||||||
- **Max feedback latency:** {N} seconds
|
- **Max feedback latency:** ~3s local; CI behavioral confirmation is the operator checkpoint in Plan 03.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Per-Task Verification Map
|
## Per-Task Verification Map
|
||||||
|
|
||||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
| Task ID | Plan | Wave | Success Criterion | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
|---------|------|------|-------------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||||
| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending |
|
| 15-01-01 | 01 | 1 | SC-4 (setup) | T-15-01/02/03 | Pinned devDep; `.planning/**` excluded from lint scope | static | `test -f .markdownlint-cli2.jsonc && node -e "require('./package.json').scripts['md:lint']"` | ❌ W0 (config new) | ⬜ pending |
|
||||||
|
| 15-01-02 | 01 | 1 | SC-4 | T-15-03 | Gate can fail on a real violation; baseline green; no Prettier conflict | static | `pnpm md:lint && pnpm format:check` | ✅ (after fixes) | ⬜ pending |
|
||||||
|
| 15-02-01 | 02 | 2 | SC-1, SC-2 | T-15-05/06 | Positive `code` filter — ambiguous files default to full gate; action pinned @v4 | static | `yq -e '.jobs.changes.outputs.code' ci.yml && grep "needs.changes.outputs.code == 'true'" ci.yml` | ❌ W0 (jobs new) | ⬜ pending |
|
||||||
|
| 15-02-02 | 02 | 2 | SC-1, SC-2, SC-3(YAML) | T-15-04/07 | Gate fails on fast-checks!=success; individual `needs.X.result` (no broken wildcard) | static | `yq -e '.jobs.gate.if == "always()"' ci.yml && grep 'needs.fast-checks.result' ci.yml && ! grep 'needs.\*.result' ci.yml` | ❌ W0 (gate new) | ⬜ pending |
|
||||||
|
| 15-03-01 | 03 | 3 | SC-3 (docs) | T-15-10 | publish.yml comment matches new required checks | static | `grep 'CI / gate' .gitea/workflows/publish.yml` | ✅ (publish.yml exists) | ⬜ pending |
|
||||||
|
| 15-03-02 | 03 | 3 | SC-1, SC-2, SC-3 | T-15-08/09 | Drop api/harness required; require gate; only after `CI / gate` reports | manual (operator) | Gitea branch-protection UI/API + throwaway doc-only & code PRs | Manual | ⬜ pending |
|
||||||
|
|
||||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||||
|
|
||||||
@@ -46,31 +55,33 @@ created: 2026-06-12
|
|||||||
|
|
||||||
## Wave 0 Requirements
|
## Wave 0 Requirements
|
||||||
|
|
||||||
- [ ] `{tests/test_file.py}` — stubs for REQ-{XX}
|
These artifacts do not exist before this phase and are created within it (no separate test scaffold needed — the "tests" are the static checks above):
|
||||||
- [ ] `{tests/conftest.py}` — shared fixtures
|
|
||||||
- [ ] `{framework install}` — if no framework detected
|
|
||||||
|
|
||||||
*If none: "Existing infrastructure covers all phase requirements."*
|
- [ ] `.markdownlint-cli2.jsonc` — new config (Plan 01 Task 1); required before `pnpm md:lint` runs
|
||||||
|
- [ ] root `md:lint` script + `markdownlint-cli2` devDep (Plan 01 Task 1)
|
||||||
|
- [ ] 13 baseline markdown violations fixed (Plan 01 Task 2) — needed for `pnpm md:lint` to pass
|
||||||
|
- [ ] `changes` + `gate` jobs in ci.yml (Plan 02) — needed before any `yq`/grep gate check passes
|
||||||
|
- [ ] Gitea branch-protection update — manual operator step (Plan 03 Task 2), only after `CI / gate` has reported at least once
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manual-Only Verifications
|
## Manual-Only Verifications
|
||||||
|
|
||||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
| Behavior | Success Criterion | Why Manual | Test Instructions |
|
||||||
|----------|-------------|------------|-------------------|
|
|----------|-------------------|------------|-------------------|
|
||||||
| {behavior} | REQ-{XX} | {reason} | {steps} |
|
| Branch protection requires `CI / fast-checks` + `CI / gate`, not `CI / api`/`CI / harness` | SC-3 | Gitea admin config — cannot be set in a workflow YAML file (RESEARCH Gitea-Specific Note #4) | Gitea → Settings → Branches → edit `main` rule; set required contexts to exactly `CI / fast-checks` + `CI / gate`; remove `CI / api` + `CI / harness` (Plan 03 Task 2) |
|
||||||
|
| Doc-only PR is mergeable with api/harness skipped | SC-1, SC-3 | Requires a live PR run + the live protection rule | Open a throwaway `*.md`-only PR; confirm api/harness skipped, `CI / gate` passes, PR mergeable |
|
||||||
*If none: "All phase behaviors have automated verification."*
|
| Code PR runs and gates on all jobs | SC-2 | Requires a live PR run | Open a throwaway `*.ts` PR; confirm fast-checks/api/harness/gate all run; PR mergeable only when green |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Validation Sign-Off
|
## Validation Sign-Off
|
||||||
|
|
||||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
- [x] All tasks have `<automated>`/static verify or are the designated manual operator checkpoint
|
||||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
- [x] Sampling continuity: no 3 consecutive code tasks without automated verify (the only manual task is the unavoidable Gitea-admin checkpoint)
|
||||||
- [ ] Wave 0 covers all MISSING references
|
- [x] Wave 0 covers all newly-created artifacts
|
||||||
- [ ] No watch-mode flags
|
- [x] No watch-mode flags
|
||||||
- [ ] Feedback latency < {N}s
|
- [x] Feedback latency ~3s local
|
||||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
- [x] `nyquist_compliant: true` set in frontmatter
|
||||||
|
|
||||||
**Approval:** {pending / approved YYYY-MM-DD}
|
**Approval:** pending
|
||||||
|
|||||||
Reference in New Issue
Block a user