Phase 15: skip api/harness CI for doc-only PRs + markdown lint gate #11
+54
-4
@@ -5,6 +5,31 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
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'
|
||||||
|
|
||||||
fast-checks:
|
fast-checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
@@ -42,8 +67,9 @@ jobs:
|
|||||||
|
|
||||||
api:
|
api:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
needs: [changes]
|
||||||
# Runs in PARALLEL with fast-checks (D-03) — no needs: dependency.
|
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:
|
services:
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:11
|
image: mariadb:11
|
||||||
@@ -128,8 +154,9 @@ jobs:
|
|||||||
|
|
||||||
harness:
|
harness:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
needs: [changes]
|
||||||
# Runs in PARALLEL with fast-checks + api (D-03) — no needs: dependency.
|
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:
|
services:
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:11
|
image: mariadb:11
|
||||||
@@ -314,3 +341,26 @@ jobs:
|
|||||||
apps/pwa/test-results/
|
apps/pwa/test-results/
|
||||||
apps/pwa/playwright-report/
|
apps/pwa/playwright-report/
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
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
|
||||||
|
# NOTE: uses individual needs.X.result checks (not the wildcard aggregate) due to
|
||||||
|
# Gitea 1.26.2 bug #31007 where the wildcard expression returns false even when jobs succeed.
|
||||||
|
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."
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user