Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 15-ci-skip-api-harness-jobs-for-doc-only-prs | 02 | execute | 2 |
|
|
true |
|
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.
<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_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 Task 1: Add the changes job and gate api/harness on the code output - .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) 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.
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.
<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 byapi/harnessif: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> |
Static (pre-merge):
yqparses ci.yml;changes/gatejobs present; api/harness carryneeds: [changes]+ combinedif; gate uses individualneeds.X.result, no wildcard.
<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 / gateaggregate exists that passes when each heavy job is success-or-skipped; the branch-protection required-check change is Plan 03. </success_criteria>