---
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/<base>` 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/<base>` 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.
