fix(416): docs-only skip never fired — detect against FETCH_HEAD (shallow-checkout safe) #429

Merged
timothy merged 2 commits from fix/416-shallow-detect into main 2026-07-17 22:39:05 +02:00
Owner

Refs #416. Fixes a real defect in the merged #422 implementation, caught by #416's own "verify on a real PR" Done-when.

The bug

The docs-only skip merged in #422 was safe but completely ineffective — every docs-only PR still ran the full matrix. Verification PR #426 (a single-.md change) ran test/migrations/functional-e2e for ~18 min instead of skipping.

Root cause (confirmed deterministically)

The test/migrations jobs check out fetch-depth: 1 (shallow). In a shallow clone:

  • origin/main has no remote-tracking ref, and there's no merge-base, so the detect script's git diff origin/main...HEAD (three-dot) errors → || true → empty diff → the fail-safe returns docs_only=false → full matrix.

Reproduced in a real shallow file:// clone: origin/main did not resolve and three-dot errored, while git diff FETCH_HEAD HEAD (two-dot) correctly returned the docs file. The api-docs/format jobs were unaffected only because they use fetch-depth: 0.

Fix

  • Diff against FETCH_HEAD (always written by git fetch, resolves in a shallow clone) instead of origin/main.
  • Use a two-dot tree diff (git diff FETCH_HEAD HEAD) — no merge-base needed.
  • Push case: fetch depth 2 for the merge commit's first-parent tree; fall through to full matrix if unresolved.
  • Fail-safe + --no-renames (rename-misclassification guard) unchanged.

Verification

  • Local: shellcheck clean; in the confirmed-broken shallow synthetic the fix now returns docs_only=true for docs and false for a .cs change.
  • Post-merge (required before closing #416): open a real docs-only PR and confirm test/migrations/functional-e2e complete in seconds (measured job durations, not just green). I won't tick #416's boxes until that's shown.

Docs

docs/ci-cd.md and docs/decisions.md will be updated with the shallow-checkout note in a follow-up to this same PR (or here) — flagging now so it's not missed.

🤖 Generated with Claude Code

Refs #416. Fixes a real defect in the merged #422 implementation, caught by #416's own "verify on a real PR" Done-when. ## The bug The docs-only skip merged in #422 was **safe but completely ineffective** — every docs-only PR still ran the full matrix. Verification PR #426 (a single-`.md` change) ran `test`/`migrations`/`functional-e2e` for ~18 min instead of skipping. ## Root cause (confirmed deterministically) The `test`/`migrations` jobs check out **`fetch-depth: 1`** (shallow). In a shallow clone: - **`origin/main` has no remote-tracking ref**, and there's **no merge-base**, so the detect script's `git diff origin/main...HEAD` (three-dot) **errors** → `|| true` → empty diff → the fail-safe returns `docs_only=false` → **full matrix**. Reproduced in a real shallow `file://` clone: `origin/main` did not resolve and three-dot errored, while `git diff FETCH_HEAD HEAD` (two-dot) correctly returned the docs file. The `api-docs`/`format` jobs were unaffected only because they use `fetch-depth: 0`. ## Fix - Diff against **`FETCH_HEAD`** (always written by `git fetch`, resolves in a shallow clone) instead of `origin/main`. - Use a **two-dot** tree diff (`git diff FETCH_HEAD HEAD`) — no merge-base needed. - Push case: fetch depth 2 for the merge commit's first-parent tree; fall through to full matrix if unresolved. - Fail-safe + `--no-renames` (rename-misclassification guard) unchanged. ## Verification - Local: shellcheck clean; in the confirmed-broken shallow synthetic the fix now returns `docs_only=true` for docs and `false` for a `.cs` change. - **Post-merge (required before closing #416):** open a real docs-only PR and confirm `test`/`migrations`/`functional-e2e` complete in **seconds** (measured job durations, not just green). I won't tick #416's boxes until that's shown. ## Docs `docs/ci-cd.md` and `docs/decisions.md` will be updated with the shallow-checkout note in a follow-up to this same PR (or here) — flagging now so it's not missed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
timothy added 1 commit 2026-07-17 22:01:05 +02:00
fix(416): detect against FETCH_HEAD with a two-dot diff (shallow-checkout safe)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 7s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 7s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m39s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 15s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 14s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m54s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 14m27s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
74005cc952
The test/migrations jobs check out fetch-depth:1. A shallow clone has NO
origin/<base> tracking ref and no merge-base, so the three-dot
origin/main...HEAD errored -> empty diff -> docs_only=false -> EVERY docs-only PR
silently ran the full matrix (safe but the skip never fired). git fetch always
writes FETCH_HEAD, which resolves in a shallow clone; diff against it with a
two-dot tree diff (no merge-base). Confirmed in a real shallow file:// clone:
origin/main did NOT resolve and three-dot errored, while FETCH_HEAD two-dot
correctly returned the docs file. api-docs/format were unaffected only because
they use fetch-depth:0.

Refs #416
timothy added 1 commit 2026-07-17 22:02:21 +02:00
docs(416): note shallow-checkout FETCH_HEAD/two-dot fix (ci-cd + decisions)
Build ErsatzTV Image / CI image pin matches docker/ci (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 8s
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 14s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 15s
Build ErsatzTV Image / Functional E2E (curl contracts) (pull_request) Successful in 5m18s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 18m36s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 19m7s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
082124f706
Refs #416
Author
Owner

Cold-context review-only agent, every claim verified by execution in real git clone --depth=1 clones (reproducing the exact fetch-depth:1 shape), not reasoned:

  • Fix works: in a shallow clone origin/main does not resolve (the old three-dot break); after the fetch, FETCH_HEAD resolves and git diff --no-renames FETCH_HEAD HEAD (two-dot, no merge-base) returns the correct set → docs-only PR now yields docs_only=true.
  • False-positive direction is provably safe: built the adversarial case (docs-only PR behind main; main advanced with a .cs the PR lacks) → the .cs surfaces in the two-dot diff → docs_only=false → full matrix. It cannot wrongly skip a code change; behind-main biases toward running more.
  • Fail-open confirmed, --no-renames retained, push merge-commit case works (depth-2 deepen for HEAD^1), no regression for up-to-date PRs (two-dot == old three-dot there).
  • NIT (accepted): push-path nfields=$(git rev-list … | wc -w …) lacks || true under pipefail+errexit. Unreachable in a valid checkout (HEAD always resolves) and fail-safe if ever hit (fails the required step → merge blocked, never a silent skip). Accepting rather than spending another full-matrix CI run on a cosmetic, unreachable line.

Review-verdict: MERGEABLE @ 082124f7

Cold-context review-only agent, **every claim verified by execution in real `git clone --depth=1` clones** (reproducing the exact `fetch-depth:1` shape), not reasoned: - **Fix works:** in a shallow clone `origin/main` does not resolve (the old three-dot break); after the fetch, `FETCH_HEAD` resolves and `git diff --no-renames FETCH_HEAD HEAD` (two-dot, no merge-base) returns the correct set → docs-only PR now yields `docs_only=true`. - **False-positive direction is provably safe:** built the adversarial case (docs-only PR *behind* main; main advanced with a `.cs` the PR lacks) → the `.cs` surfaces in the two-dot diff → `docs_only=false` → full matrix. It **cannot** wrongly skip a code change; behind-main biases toward running more. - **Fail-open confirmed**, `--no-renames` retained, push merge-commit case works (depth-2 deepen for `HEAD^1`), no regression for up-to-date PRs (two-dot == old three-dot there). - **NIT (accepted):** push-path `nfields=$(git rev-list … | wc -w …)` lacks `|| true` under `pipefail`+`errexit`. Unreachable in a valid checkout (HEAD always resolves) and fail-*safe* if ever hit (fails the required step → merge blocked, never a silent skip). Accepting rather than spending another full-matrix CI run on a cosmetic, unreachable line. Review-verdict: MERGEABLE @ 082124f7
timothy merged commit 4818b07668 into main 2026-07-17 22:39:05 +02:00
timothy deleted branch fix/416-shallow-detect 2026-07-17 22:39:06 +02:00
Sign in to join this conversation.