fix(836): never pass --depth to a checkout that may already be complete #884

Merged
timothy merged 3 commits from fix/836-depth-graft into main 2026-08-30 01:55:30 +02:00
Owner

fixes #836

git fetch --depth=N grafts a complete clone shallow — it writes .git/shallow and cuts history at N even though every object is already present. scripts/ci-detect-docs-only.sh has four consumers in docker-build.yml that disagree on checkout depth (test/migrations/functional-e2e at fetch-depth: 2, build at fetch-depth: 0), and it applied a depth chosen for the first group to all of them.

That grafted build's complete clone on every push to main, so the git describe --tags in the immediately following Compute version and tags step found no reachable tag, and a || echo v0.0.0 fallback that cannot fail turned it into a version. Every :latest image published from main carried InformationalVersion 0.0.0-<sha> instead of 26.x.y-<sha>, from 2026-07-17 (#416) until this change, with nothing red anywhere.

What changed

  1. scripts/ci-detect-docs-only.sh — both fetch sites go through fetch_ref, which passes --depth only when git rev-parse --is-shallow-repository answers true. Any other answer passes none.
  2. .gitea/workflows/docker-build.yml — the non-tag path no longer defaults. A failed git describe fails the job with a diagnostic, so no :latest is published rather than a mislabelled one. build is not a required status context, so this cannot block a merge; the tag path never calls describe, so releases are unaffected.
  3. New guard scripts/tests/test_docs_only_detector_clone_depth.py, a declared clause mutation in mutation_manifest.py, and inventory rows — promoting the detector from NONE to MUTATION.
  4. Docsdocs/ci-cd.md (its prose described the defect in the present tense) and a new record ci.fetch-depth-never-grafts-a-complete-clone.

Measured, not inferred

  • The live defect in CI: run 2416 / job 10345 (push to main 8aeacd534) → INFO_VERSION=0.0.0-8aeacd53.
  • The causal premise: the repo is public, so the credential-less fetch that runs after actions/checkout's "Removing auth" does succeed — the graft is real. The checkout log also shows tags being fetched, which rules out "the tags were never there".
  • Against a real anonymous clone of this repository: predecessor detector → grafted, describe fails → 0.0.0-1afad085; fixed detector → complete, describe26.14.0-1afad085.
  • The workflow step body executed on both clones: grafted → exit 1 with a diagnostic and no image; complete → 26.14.0-…; tag path → :prod + 26.14.0, unaffected even on a grafted clone.
  • The guard witnessed red against origin/main's detector: 4 failed, 2 passed (the two that pass are the negative control and the shallow-unchanged case, both correctly script-independent).

Scope note, corrected by review

The PR arm's --depth=200 was inert, not latently firing: on pull_request the only jobs running this script are the three shallow ones. api-docs/format have a step with id: detect but it runs their own inline diff — an earlier draft of this change read them as consumers and said so in the decision record. The depth is dropped there anyway because the clause is shared and the next fetch-depth: 0 consumer would inherit the graft silently. Likewise, keeping the depth for the shallow three is conservative rather than required: a depth-less fetch into a --depth=2 clone leaves .git/shallow in place, still writes FETCH_HEAD, and still reports the parents the push arm reads.

Verification

  • scripts/tests full suite: 1237 passed, 2 skipped, on base 736649b3b.
  • Guard witnessed red against the real predecessor; declared mutation reddens its named proof for the right diagnostic (test_mutation_harness.py 36/36).
  • decisions_validate.py OK · catalog up to date · check-doc-narrative.py 0 warnings · shellcheck clean · no .cs touched, so the BOM/format gate does not apply.
  • Two independent cold-review rounds (Claude, isolated worktree, review-only brief). Round 1 found a HIGH (the consumer claim above) and two MEDIUMs; round 2 returned nothing at BLOCKER/HIGH/MEDIUM. No cross-family review this session — Codex was out of quota.

Post-merge

Read InformationalVersion back out of the newly built :latest image and record it in the issue's ## Closing record. That check is deliberately not a ## Done-when box: build only runs on a push to main, so it could never be ticked before the merge it would gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HETQK67q1bEpDZpHLZNsp9

fixes #836 `git fetch --depth=N` **grafts a complete clone shallow** — it writes `.git/shallow` and cuts history at N even though every object is already present. `scripts/ci-detect-docs-only.sh` has four consumers in `docker-build.yml` that disagree on checkout depth (`test`/`migrations`/`functional-e2e` at `fetch-depth: 2`, `build` at `fetch-depth: 0`), and it applied a depth chosen for the first group to all of them. That grafted `build`'s complete clone on every push to `main`, so the `git describe --tags` in the immediately following `Compute version and tags` step found no reachable tag, and a `|| echo v0.0.0` fallback that cannot fail turned it into a version. Every `:latest` image published from `main` carried `InformationalVersion 0.0.0-<sha>` instead of `26.x.y-<sha>`, from 2026-07-17 (#416) until this change, with nothing red anywhere. ## What changed 1. **`scripts/ci-detect-docs-only.sh`** — both fetch sites go through `fetch_ref`, which passes `--depth` only when `git rev-parse --is-shallow-repository` answers `true`. Any other answer passes none. 2. **`.gitea/workflows/docker-build.yml`** — the non-tag path no longer defaults. A failed `git describe` fails the job with a diagnostic, so no `:latest` is published rather than a mislabelled one. `build` is **not** a required status context, so this cannot block a merge; the tag path never calls `describe`, so releases are unaffected. 3. **New guard** `scripts/tests/test_docs_only_detector_clone_depth.py`, a declared clause mutation in `mutation_manifest.py`, and inventory rows — promoting the detector from `NONE` to `MUTATION`. 4. **Docs** — `docs/ci-cd.md` (its prose described the defect in the present tense) and a new record `ci.fetch-depth-never-grafts-a-complete-clone`. ## Measured, not inferred - **The live defect in CI**: run 2416 / job 10345 (push to `main` `8aeacd534`) → `INFO_VERSION=0.0.0-8aeacd53`. - **The causal premise**: the repo is public, so the credential-less fetch that runs after `actions/checkout`'s "Removing auth" does succeed — the graft is real. The checkout log also shows tags being fetched, which rules out "the tags were never there". - **Against a real anonymous clone of this repository**: predecessor detector → grafted, `describe` fails → `0.0.0-1afad085`; fixed detector → complete, `describe` → `26.14.0-1afad085`. - **The workflow step body executed** on both clones: grafted → `exit 1` with a diagnostic and no image; complete → `26.14.0-…`; tag path → `:prod` + `26.14.0`, unaffected even on a grafted clone. - **The guard witnessed red** against `origin/main`'s detector: 4 failed, 2 passed (the two that pass are the negative control and the shallow-unchanged case, both correctly script-independent). ## Scope note, corrected by review The PR arm's `--depth=200` was **inert, not latently firing**: on `pull_request` the only jobs running this script are the three shallow ones. `api-docs`/`format` have a step with `id: detect` but it runs their own inline diff — an earlier draft of this change read them as consumers and said so in the decision record. The depth is dropped there anyway because the clause is shared and the next `fetch-depth: 0` consumer would inherit the graft silently. Likewise, keeping the depth for the shallow three is **conservative rather than required**: a depth-less fetch into a `--depth=2` clone leaves `.git/shallow` in place, still writes `FETCH_HEAD`, and still reports the parents the push arm reads. ## Verification - `scripts/tests` full suite: **1237 passed, 2 skipped**, on base `736649b3b`. - Guard witnessed red against the real predecessor; declared mutation reddens its named proof for the right diagnostic (`test_mutation_harness.py` 36/36). - `decisions_validate.py` OK · catalog up to date · `check-doc-narrative.py` 0 warnings · `shellcheck` clean · no `.cs` touched, so the BOM/format gate does not apply. - Two independent cold-review rounds (Claude, isolated worktree, review-only brief). Round 1 found a HIGH (the consumer claim above) and two MEDIUMs; round 2 returned nothing at BLOCKER/HIGH/MEDIUM. **No cross-family review this session — Codex was out of quota.** ## Post-merge Read `InformationalVersion` back out of the newly built `:latest` image and record it in the issue's `## Closing record`. That check is deliberately not a `## Done-when` box: `build` only runs on a push to `main`, so it could never be ticked before the merge it would gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01HETQK67q1bEpDZpHLZNsp9
Author
Owner

Review-verdict: MERGEABLE @ 58d2c14

Two cold review rounds; round 2 clean at BLOCKER/HIGH/MEDIUM. Suite 1237 passed on base 736649b3b; guard witnessed red against the real predecessor.

Review-verdict: MERGEABLE @ 58d2c14 Two cold review rounds; round 2 clean at BLOCKER/HIGH/MEDIUM. Suite 1237 passed on base 736649b3b; guard witnessed red against the real predecessor.
Author
Owner

Review-verdict: MERGEABLE @ b8af3bf

Head b8af3bf4a adds only a ruff format pass over the new guard file: one file, ASTs identical to the reviewed 58d2c147a, so the round-2 clean verdict carries. ruff check + format --check pass over all 51 tracked files with the CI-pinned 0.12.11; suite 1237 passed, 2 skipped.

Review-verdict: MERGEABLE @ b8af3bf Head b8af3bf4a adds only a ruff format pass over the new guard file: one file, ASTs identical to the reviewed 58d2c147a, so the round-2 clean verdict carries. ruff check + format --check pass over all 51 tracked files with the CI-pinned 0.12.11; suite 1237 passed, 2 skipped.
Author
Owner

Review-verdict: MERGEABLE @ 008860c

Head 008860c2b fixes a CI-only red in the new guard's fixtures (git identity in a clone). Boundary fix in the test harness only; the shipped script and workflow are byte-identical to the reviewed 58d2c147a. Local run now reproduces the CI condition — verified by removing the identity flags and witnessing the same 'Author identity unknown'.

Review-verdict: MERGEABLE @ 008860c Head 008860c2b fixes a CI-only red in the new guard's fixtures (git identity in a clone). Boundary fix in the test harness only; the shipped script and workflow are byte-identical to the reviewed 58d2c147a. Local run now reproduces the CI condition — verified by removing the identity flags and witnessing the same 'Author identity unknown'.
timothy added 3 commits 2026-08-30 00:56:05 +02:00
`git fetch --depth=N` GRAFTS a complete clone shallow — it writes `.git/shallow`
and cuts history at N even though every object is already present.
`scripts/ci-detect-docs-only.sh` has four consumers in docker-build.yml and they
do not agree on checkout depth: test/migrations/functional-e2e check out
`fetch-depth: 2`, while `build` (and api-docs/format on the PR arm) check out
`fetch-depth: 0`. The script applied a depth chosen for the first group to all of
them.

For `build` that grafted its complete clone on every push to main, so the
`git describe --tags` in the immediately following `Compute version and tags`
step found no reachable tag, and a `|| echo v0.0.0` fallback that cannot fail
turned that into a version: every `:latest` image shipped
`InformationalVersion 0.0.0-<sha>`.

Both fetch sites now go through `fetch_ref`, which passes `--depth` only when
`git rev-parse --is-shallow-repository` answers `true`. That also closes the PR
arm's `--depth=200` — the same defect with a larger N, grafting api-docs/format.
An unknown answer passes no depth: the query fails only where there is no
readable repository, and a depth cannot help there either.

Second half: `Compute version and tags` no longer defaults. A failed
`git describe` on the non-tag path fails the job rather than stamping 0.0.0, so
no `:latest` is published instead of a mislabelled one. `build` is not a required
status context, so this cannot block a merge. The tag path never calls
`describe`, so `:prod`/`:<version>` releases are unaffected — which is also why
they were correctly versioned throughout.

Measured, not inferred:
- The live defect, from run 2416 / job 10345 (push to main 8aeacd534):
  `INFO_VERSION=0.0.0-8aeacd53`.
- Against a real anonymous clone of this repository: the predecessor detector
  grafts it and `describe` fails -> `0.0.0-1afad085`; the fixed detector leaves
  it complete -> `26.14.0-1afad085`.
- The workflow step body executed on both clones: grafted -> exit 1 with a
  diagnostic, complete -> `26.14.0-1afad085`, tag path -> `26.14.0` + `:prod`
  even on a grafted clone.

New guard `scripts/tests/test_docs_only_detector_clone_depth.py` drives the real
script over real `file://` clones (a plain path clone silently ignores --depth,
so it carries a negative control that requires the fixture to be able to graft at
all). It was witnessed red against the predecessor. Its clause mutation is
declared in mutation_manifest.py, which promotes the detector's guard-inventory
row from NONE to MUTATION; the stated reason it was undeclared — that a
skip-gate script's effect is visible only in a workflow run — was itself wrong.

fixes #836

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HETQK67q1bEpDZpHLZNsp9
`ruff format --check` was the only red in `script-tests` — `ruff check` passed on
all 51 tracked files and pytest never ran. Purely cosmetic: the repo's ruff config
allows a wider line than the hand-wrapping used, so several call sites collapse to
one line.

Verified with the version CI pins (ruff 0.12.11) over the same population the job
derives (`git ls-files -z '*.py' '*.pyi' '*.ipynb'` under bash, 51 files): both
`ruff check` and `ruff format --check` pass. Suite unchanged at 1237 passed,
2 skipped.

A follow-up commit rather than an amend: the branch is already pushed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HETQK67q1bEpDZpHLZNsp9
fix(836): the guard's git fixtures must not inherit an identity from the machine
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 11s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 12s
PR Gates / Docs update reminder (pull_request) Successful in 17s
review-verdict/h10 Review-verdict: MERGEABLE @ d44d9eb (base: main)
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 42s
Review verdict / Set review-verdict status (pull_request_target) Successful in 15s
PR Gates / decisions lifecycle (pull_request) Successful in 26s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 16s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 12m16s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 14m21s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 9m46s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 9m1s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s
d44d9eb55f
CI red: `git commit -q -m docs on the PR head failed: Author identity unknown`.
The fixtures set `user.email`/`user.name` on the ORIGIN repos, but the PR-arm test
commits in a CLONE, which inherited nothing. A developer checkout has a global
identity and the CI container has none, so the file was green here and red there.

Fixed at the boundary rather than the site: `_GIT_CONF` is passed to EVERY git
invocation in the file, so no repository it creates can depend on ambient config.
It also disarms a global `core.hooksPath` (which would fire this repo's husky hooks
against throwaway fixtures) and `commit.gpgsign` (which would block on a pinentry
prompt no pytest timeout is watching). The now-redundant per-fixture `git config`
calls are removed.

`_env()` additionally points `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM` at os.devnull,
so the ambient configuration is UNAVAILABLE rather than merely overridden.

That alone did NOT reproduce CI, and the first version of this commit claimed it did:
with the identity flags removed the suite still passed here, because a Mac invents an
identity from the OS user and hostname. `user.useConfigOnly=true` is what closes it.
Verified by removing the identity pair and watching the file go red locally with the
same `Author identity unknown` CI produced — so a local run now catches this class
instead of deferring it to the runner.

Gate: ruff check + format --check pass over all 51 tracked files with the CI-pinned
0.12.11; suite 1237 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HETQK67q1bEpDZpHLZNsp9
timothy force-pushed fix/836-depth-graft from 008860c2bb to d44d9eb55f 2026-08-30 00:56:05 +02:00 Compare
Author
Owner

Review-verdict: MERGEABLE @ d44d9eb

Head d44d9eb55 is 008860c2b rebased onto e8f80c42c (#883). All six shipped artifacts byte-identical to the reviewed content; the only edit is the guard-inventory summary conflict, resolved by DERIVING the six counts from the table rather than hand-merging two deltas, then verified by test_the_summary_counts_match_the_table. Gate: ruff check + format --check over 52 tracked files, suite 1250 passed 2 skipped.

Review-verdict: MERGEABLE @ d44d9eb Head d44d9eb55 is 008860c2b rebased onto e8f80c42c (#883). All six shipped artifacts byte-identical to the reviewed content; the only edit is the guard-inventory summary conflict, resolved by DERIVING the six counts from the table rather than hand-merging two deltas, then verified by test_the_summary_counts_match_the_table. Gate: ruff check + format --check over 52 tracked files, suite 1250 passed 2 skipped.
timothy merged commit 94a3d13495 into main 2026-08-30 01:55:30 +02:00
timothy deleted branch fix/836-depth-graft 2026-08-30 01:55:31 +02:00
Sign in to join this conversation.