--- key: ci.docs-only-detect-shallow-safe title: '2026-07-17 — Docs-only detect must be shallow-checkout safe: FETCH_HEAD + two-dot, not origin/main + three-dot (#416 follow-up)' status: active since: '2026-07-17' supersedes: none superseded-by: none rule: 'The docs-only detect script must diff against `FETCH_HEAD` (always resolves after `git fetch`, even shallow) using a two-dot tree diff — not `origin/` with three-dot — because a `fetch-depth: 1` shallow clone has no remote-tracking ref and no merge-base, which silently fails the original detect into `docs_only=false` (full matrix, no functional error). A CI-behavior change must be verified by measuring the effect (job durations), not just a green check.' signals: 'shallow clone, FETCH_HEAD, two-dot vs three-dot diff, fetch-depth 1 · paths: `scripts/ci-detect-docs-only.sh` · issues: #416, #422' mechanics: '`docs/ci-cd.md` → "Docs-only skip"; verified via a real shallow `file://` clone reproduction' --- The #416 docs-only skip shipped (#422) safe but **ineffective**: every docs-only PR still ran the full matrix. Root cause — `test`/`migrations` check out `fetch-depth: 1`, and in a shallow clone `origin/` has no remote-tracking ref and there is no merge-base, so the detect script's three-dot `git diff origin/main...HEAD` errored → `|| true` → empty diff → the fail-safe returned `docs_only=false` → full matrix. Confirmed in a real shallow `file://` clone (`origin/main` did not resolve; three-dot errored; `git diff FETCH_HEAD HEAD` returned the changed files correctly). Decision: the detect diffs against **`FETCH_HEAD`** (always written by `git fetch`, resolves in a shallow clone) with a **two-dot** tree diff (no merge-base). `api-docs`/`format` were unaffected only because they use `fetch-depth: 0` — a difference the first cut missed. Meta-lesson reinforced: a CI gating change can pass every local test and merge green while being a complete no-op in CI; only real-PR verification that **measures the effect** (job durations, not just a green check) catches it — which is exactly what #416's Done-when demanded. Fixed in the #416 follow-up PR.