--- phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs plan: "03" subsystem: ci tags: [ci, gitea-actions, branch-protection, gate, doc-only-skip] dependency_graph: requires: [15-02] provides: [main branch protection requires fast-checks + gate, api/harness no longer required] affects: [.gitea/workflows/publish.yml, "Gitea branch protection (main) — admin state"] tech_stack: patterns: - Gitea branch-protection status_check_contexts via tea API key_files: modified: - .gitea/workflows/publish.yml decisions: - D-15-03-CTX-SUFFIX: Gitea status-check contexts carry a "(pull_request)" suffix; required checks set to "CI / fast-checks (pull_request)" + "CI / gate (pull_request)" — NOT the bare names the plan text suggested (bare names would deadlock — Gitea never emits them) - D-15-03-API-ENDPOINT: branch protection lives at repos/{owner}/{repo}/branch_protections/{name}, not branches/{name}/protection (latter 404s on this Gitea) metrics: completed_date: "2026-06-12" tasks_completed: 2 tasks_total: 2 files_modified: 1 --- # Phase 15 Plan 03: Finalize gating surface Summary **One-liner:** Updated `publish.yml`'s safety-gate comment to name the new required checks, then (post-merge, operator-authorized) changed Gitea branch protection on `main` to require exactly `CI / fast-checks (pull_request)` + `CI / gate (pull_request)` and drop the now-skippable `CI / api`/`CI / harness` — making doc-only PRs mergeable without a missing-required-check deadlock. ## Tasks Completed | Task | Name | Commit / Artifact | Files | |------|------|-------------------|-------| | 1 | Update publish.yml safety-gate comment to name new required checks | da623ac | .gitea/workflows/publish.yml | | 2 | Operator — update Gitea branch protection on main (drop api/harness, require gate) | Gitea admin state (tea API PATCH) | branch_protections/main | ## What Was Built **Task 1 — publish.yml comment:** Edited only the safety-gate comment block (no job/step/env/trigger change). The comment now states that the required checks are `CI / fast-checks` and `CI / gate`, and that `CI / api`/`CI / harness` are conditionally skipped on doc-only PRs and gated via the always-running `CI / gate` aggregate. publish.yml still parses as valid YAML. **Task 2 — branch protection (operator, executed post-merge):** After PR #11 merged to `main` (bringing the `gate` job) and `CI / gate (pull_request)` reported `success` on the PR run, the `main` branch-protection rule's `status_check_contexts` was set to: - `CI / fast-checks (pull_request)` - `CI / gate (pull_request)` and `CI / api (pull_request)` + `CI / harness (pull_request)` were removed. Applied via: `tea api --method PATCH repos/luckberg/familysync/branch_protections/main --data '{"status_check_contexts":["CI / fast-checks (pull_request)","CI / gate (pull_request)"]}'` ## Decisions Made - **D-15-03-CTX-SUFFIX:** The live Gitea status-check contexts carry a `(pull_request)` suffix (e.g. `CI / gate (pull_request)`), as confirmed by the PR #11 commit-status API. The plan's how-to-verify named bare `CI / fast-checks`/`CI / gate`; using those would have required contexts Gitea never emits, re-creating the very deadlock this phase fixes. Required checks were therefore set with the suffix to match emitted reality. - **D-15-03-API-ENDPOINT:** Branch protection is read/written at `repos/{owner}/{repo}/branch_protections/{name}` on this Gitea (1.26.2); the older `branches/{name}/protection` path returns "not found". ## Deviations from Plan - The operator action was performed via the `tea` API by the agent (operator-authorized "proceed") rather than via the Gitea UI by a human, and used the **suffixed** context names (D-15-03-CTX-SUFFIX) instead of the bare names in the plan text. - The throwaway doc-only / code PR live A/B test described in the plan's how-to-verify was replaced with: (a) live proof from PR #11 that all five jobs (`changes`/`fast-checks`/`api`/`harness`/`gate`) run and report `success` on a code PR (SC-2), and (b) static proof that the `changes` `code` filter matches no `docs/**` or root `*.md` path, so a doc-only PR yields `code=false` → `api`/`harness` skip → `gate` passes (SC-1/SC-3). The protection change was verified by re-reading the rule (required = fast-checks + gate; api/harness absent). ## Threat Model Coverage | Threat | Mitigation | Status | |--------|-----------|--------| | T-15-08: leaving api/harness required after skip logic ships (deadlock) | both dropped from required checks; always-running `CI / gate` required instead | Implemented | | T-15-09: dropping heavy-job requirements before `CI / gate` exists (bypass window) | precondition confirmed — `CI / gate (pull_request)` reported `success` on PR #11 before any protection edit | Implemented | | T-15-10: publish.yml comment still claims api/harness required (false sense of gating) | Task 1 updated the comment to name `CI / fast-checks` + `CI / gate` | Implemented | ## Verification - publish.yml: `CI / gate` and `CI / fast-checks` present; old "three required checks (… api, … harness)" line replaced; valid YAML (`publish` job intact). - Branch protection (re-GET after PATCH): `status_check_contexts == ['CI / fast-checks (pull_request)', 'CI / gate (pull_request)']`; api/harness no longer present; `enable_status_check: true`. - Precondition: PR #11 head commit-status shows `CI / gate (pull_request) => success`. - SC-1 (doc-only mergeable): static — `changes` `code` filter excludes `docs/**` + root `*.md`. - SC-2 (code PR gates all): live — PR #11 ran and gated on all jobs. ## Self-Check: PASSED - `.gitea/workflows/publish.yml` modified (comment): EXISTS - Commit da623ac (Task 1): FOUND - Branch protection required checks = fast-checks + gate, api/harness dropped: VERIFIED via tea API