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

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
This commit is contained in:
2026-08-30 00:46:49 +02:00
co-authored by Claude Opus 5
parent 7fa2e35617
commit d44d9eb55f
@@ -52,20 +52,53 @@ def _env(**extra: str) -> dict[str, str]:
which arm it takes. Pinning only the one variable that bites today would leave the class open for
the next arm that starts reading a sibling — the whole prefix goes, and `_run_detector` puts back
exactly what each test means to set.
THE MACHINE'S GIT CONFIG IS NEUTRALISED TOO, and that is not belt-and-braces — it is what keeps
this file from passing here and failing in CI. A developer checkout has a global `user.email`;
the CI container has none, and git refuses to commit without one ("Author identity unknown").
The fixtures below commit, so the first version of this file was green locally and red in CI on
exactly that. Pointing the global and system config at `os.devnull` makes the ambient
configuration UNAVAILABLE rather than merely overridden, so a missing `-c` flag in `_git` fails
everywhere instead of only where the machine is unhelpful — the local run reproduces CI by
construction. It also disarms a global `core.hooksPath` (which would fire this repo's husky
hooks against throwaway fixtures) and a global `commit.gpgsign` (which would block on a pinentry
prompt no pytest timeout is watching).
"""
env = {k: v for k, v in os.environ.items() if not k.startswith(("GIT_", "GITHUB_"))}
env["GIT_CONFIG_GLOBAL"] = os.devnull
env["GIT_CONFIG_SYSTEM"] = os.devnull
env.update(extra)
return env
# Passed to every git invocation, so no repository here can inherit an identity or a hook path from
# the machine. `_env()` makes the ambient config unavailable; these supply what the commits need.
_GIT_CONF = [
"-c",
"user.email=test@example.com",
"-c",
"user.name=Test",
"-c",
"commit.gpgsign=false",
"-c",
"core.hooksPath=",
# Forbids git from INVENTING an identity from the OS user and hostname. Without it the two
# settings above are not enough to reproduce CI: a Mac happily auto-detects one, so dropping the
# identity flags stays green locally and fails in the container. With it, a missing flag fails
# everywhere — verified by removing the identity pair and watching this file go red.
"-c",
"user.useConfigOnly=true",
]
def _git(args: list[str], cwd: Path) -> str:
r = subprocess.run(["git", *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
r = subprocess.run(["git", *_GIT_CONF, *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
assert r.returncode == 0, f"git {' '.join(args)} failed: {r.stderr}"
return r.stdout.strip()
def _git_rc(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
return subprocess.run(["git", *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
return subprocess.run(["git", *_GIT_CONF, *args], cwd=str(cwd), env=_env(), capture_output=True, text=True)
def _is_shallow(repo: Path) -> str:
@@ -84,8 +117,6 @@ def origin_url(tmp_path_factory) -> str:
origin = root / "origin"
origin.mkdir()
_git(["init", "-q", "-b", "main", "."], origin)
_git(["config", "user.email", "test@example.com"], origin)
_git(["config", "user.name", "Test"], origin)
for i in range(1, 13):
(origin / f"f{i}.txt").write_text(f"{i}\n")
@@ -123,8 +154,6 @@ def deep_origin_url(tmp_path_factory) -> str:
origin = root / "origin"
origin.mkdir()
_git(["init", "-q", "-b", "main", "."], origin)
_git(["config", "user.email", "test@example.com"], origin)
_git(["config", "user.name", "Test"], origin)
(origin / "a.txt").write_text("a\n")
_git(["add", "-A"], origin)
_git(["commit", "-q", "-m", "c0"], origin)