--- phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs plan: "02" subsystem: ci tags: [ci, gitea-actions, paths-filter, gate, doc-only-skip] dependency_graph: requires: [15-01] provides: [CI / gate commit-status, doc-only PR skip] affects: [.gitea/workflows/ci.yml] tech_stack: added: - dorny/paths-filter@v4 (Gitea action for PR diff detection) patterns: - changes job with paths-filter - always-running gate aggregate job - Gitea-safe individual needs.X.result checks key_files: modified: - .gitea/workflows/ci.yml decisions: - D-15-02-CHANGES-JOB: dorny/paths-filter@v4 with positive code filter; code==false means doc-only; ambiguous files default to full gate - D-15-02-GATE-INDIVIDUAL: individual needs.X.result checks (not wildcard) due to Gitea 1.26.2 bug #31007 - D-15-02-GATE-ALWAYS: if:always() on gate prevents deadlock on skipped upstream jobs (fix in Gitea 1.21.8, instance is 1.26.2) metrics: duration_minutes: 5 completed_date: "2026-06-12" tasks_completed: 2 tasks_total: 2 files_modified: 1 --- # Phase 15 Plan 02: CI doc-only skip + gate aggregate Summary **One-liner:** `changes` job (dorny/paths-filter@v4) classifies each PR; `api`/`harness` skip on `code==false`; always-running `gate` job aggregates all results using Gitea-safe individual `needs.X.result` checks, emitting the `CI / gate` status that Plan 03's branch-protection update requires. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Add changes job and gate api/harness on code output | 7260438 | .gitea/workflows/ci.yml | | 2 | Add always-running gate aggregate job | 547b12c | .gitea/workflows/ci.yml | ## What Was Built **Task 1 — changes job + conditional api/harness:** A new `changes` job using `dorny/paths-filter@v4` was inserted before `fast-checks` in `.gitea/workflows/ci.yml`. It: - Runs on `ubuntu-latest` with `if: github.event_name == 'pull_request'` - Declares `permissions: pull-requests: read` (job-scoped, required by paths-filter v4) - Emits `outputs.code: ${{ steps.filter.outputs.code }}` - Contains a single `uses: dorny/paths-filter@v4` step with `id: filter` and NO `actions/checkout` - Defines a positive `code` filter covering `**/*.ts`, `**/*.tsx`, `**/*.js`, `**/*.json`, `**/*.yaml`, `**/*.yml`, `apps/**`, `packages/**`, `pnpm-lock.yaml`, `Dockerfile`, `docker-compose*.yml` The `api` and `harness` jobs were modified: - Added `needs: [changes]` - Changed `if` from `github.event_name == 'pull_request'` to `github.event_name == 'pull_request' && needs.changes.outputs.code == 'true'` - Updated inline comment to say "skipped for doc-only PRs" - All services, env, and step bodies left unchanged **Task 2 — gate aggregate job:** A new `gate` job was appended at the end of `ci.yml`: - `runs-on: ubuntu-latest` - `needs: [fast-checks, changes, api, harness]` - `if: always()` — ensures the job reports regardless of upstream outcome - One step running a bash script that: 1. Fails (exit 1) if `needs.fast-checks.result` is not `success` (fast-checks always runs) 2. Iterates over `needs.api.result` and `needs.harness.result`, failing if either is not `success` or `skipped` 3. Echoes "Gate passed." otherwise - Uses individual `needs.X.result` references (not `contains(needs.*.result,...)`) due to Gitea 1.26.2 bug #31007 ## Decisions Made - **D-15-02-CHANGES-JOB:** Positive `code` filter chosen (not a `docs` exclusion filter) so any new or ambiguous file type defaults to the full gate. A doc-only PR must have every changed file fall outside the code patterns. - **D-15-02-GATE-INDIVIDUAL:** Individual `needs.X.result` checks used instead of `contains(needs.*.result, ...)` wildcard — Gitea 1.26.2 issue #31007 confirms the wildcard returns false even when jobs succeed. - **D-15-02-GATE-ALWAYS:** `if: always()` on the `gate` job prevents deadlock when upstream jobs are skipped. The deadlock bug (Gitea #27906) was fixed in 1.21.8; this instance runs 1.26.2. ## Deviations from Plan None — plan executed exactly as written. ## Threat Model Coverage | Threat | Mitigation | Status | |--------|-----------|--------| | T-15-04: gate passes when fast-checks fails | gate exits 1 on fast-checks != success; skipped accepted only for api/harness | Implemented | | T-15-05: code PR misclassified doc-only | positive code filter — any ambiguous file matches code and runs full gate | Implemented | | T-15-06: supply chain via dorny/paths-filter@v4 | pinned to @v4 tag; pull-requests:read only; no secrets access | Implemented | | T-15-07: required check deadlock | gate uses if:always(); api/harness NOT added as required checks (Plan 03 scope) | Implemented | ## Known Stubs None — this plan produces only CI workflow YAML. No runtime state or UI involved. ## Threat Flags None — no new network endpoints, auth paths, file access patterns, or schema changes. ## Verification **Static (pre-merge):** - Python yaml.safe_load parses ci.yml without error - `changes` job present with `dorny/paths-filter@v4`, `id: filter`, `permissions: pull-requests: read`, `outputs.code` - No `actions/checkout` step in `changes` job - `api` and `harness` have `needs: [changes]` and combined `if` with `needs.changes.outputs.code == 'true'` - `gate` job present with `if: always()` and `needs: [fast-checks, changes, api, harness]` - `needs.fast-checks.result`, `needs.api.result`, `needs.harness.result` individually referenced in gate - No `contains(needs.*.result` wildcard in file **Behavioral (post-merge, per 15-VALIDATION.md):** - Doc-only PR: `changes` emits `code=false`; `api`/`harness` show skipped; `fast-checks` runs; `gate` passes - Code PR: all three run; `gate` passes when green - Gate-fail path: failing fast-checks causes `gate` to exit 1 ## Self-Check: PASSED - `.gitea/workflows/ci.yml` modified: EXISTS - Commit 7260438 (Task 1): FOUND - Commit 547b12c (Task 2): FOUND