---
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
