From a0fda6b2652249b890c58aa1bdfb98f9ca6d3bb0 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 12 Jul 2026 11:30:39 +0100 Subject: [PATCH] fix(release-control): scrub inherited git env in hook scratch-repo helpers Pre-commit runs from a linked git worktree export an absolute GIT_DIR. The release-control test helpers and the test-patched branch of git_env() only removed GIT_INDEX_FILE, so their scratch-repo commands targeted the REAL repository: 'git init' in a tempdir re-initialized it with core.bare=true (breaking git status/commit for every checkout and worktree) and the subsequent scratch 'git add' failed the hook. Scrub GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, and GIT_COMMON_DIR in: - git_env() of format_staged_go.py, governance_stage_guard.py, and subsystem_contracts.py (test-patched branch only; production hook behavior unchanged) - the scratch-repo git() helpers in format_staged_go_test.py, governance_stage_guard_test.py, readiness_assertion_guard_test.py, subsystem_contracts_test.py Matches the pattern contract_audit_test.py and status_audit_test.py already used. Verified the full hook test battery passes with GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILE pointed at a canary repo, which stays un-corrupted. --- scripts/release_control/format_staged_go.py | 10 ++++++---- scripts/release_control/format_staged_go_test.py | 6 +++++- scripts/release_control/governance_stage_guard.py | 7 ++++++- scripts/release_control/governance_stage_guard_test.py | 6 +++++- .../release_control/readiness_assertion_guard_test.py | 6 +++++- scripts/release_control/subsystem_contracts.py | 7 ++++++- scripts/release_control/subsystem_contracts_test.py | 6 +++++- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/scripts/release_control/format_staged_go.py b/scripts/release_control/format_staged_go.py index 1cc560940..8ca79a9d8 100644 --- a/scripts/release_control/format_staged_go.py +++ b/scripts/release_control/format_staged_go.py @@ -15,11 +15,13 @@ DEFAULT_REPO_ROOT = REPO_ROOT def git_env() -> dict[str, str]: env = os.environ.copy() - # Unit tests patch REPO_ROOT to a temporary repository. In that case, an - # inherited alternate index from the caller should not leak into the temp - # repo and point git plumbing at entries from a different repository. + # Unit tests patch REPO_ROOT to a temporary repository. In that case, the + # inherited hook environment (alternate index, or the absolute GIT_DIR a + # pre-commit run from a linked worktree exports) should not leak into the + # temp repo and point git plumbing at a different repository. if REPO_ROOT != DEFAULT_REPO_ROOT: - env.pop("GIT_INDEX_FILE", None) + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return env diff --git a/scripts/release_control/format_staged_go_test.py b/scripts/release_control/format_staged_go_test.py index 626fcd5a5..0e9273e41 100644 --- a/scripts/release_control/format_staged_go_test.py +++ b/scripts/release_control/format_staged_go_test.py @@ -11,7 +11,11 @@ from format_staged_go import format_staged_go_files class FormatStagedGoTest(unittest.TestCase): def git(self, repo_root: Path, *args: str) -> subprocess.CompletedProcess: env = os.environ.copy() - env.pop("GIT_INDEX_FILE", None) + # Scrub the full hook environment: with only GIT_INDEX_FILE removed, a + # pre-commit run from a linked worktree exports an absolute GIT_DIR and + # "git init" here re-initializes the REAL repository as bare. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return subprocess.run( ["git", *args], cwd=repo_root, diff --git a/scripts/release_control/governance_stage_guard.py b/scripts/release_control/governance_stage_guard.py index c4b2cc2e8..8181da979 100644 --- a/scripts/release_control/governance_stage_guard.py +++ b/scripts/release_control/governance_stage_guard.py @@ -30,7 +30,12 @@ DEFAULT_REPO_ROOT = REPO_ROOT def git_env() -> dict[str, str]: env = os.environ.copy() if REPO_ROOT != DEFAULT_REPO_ROOT: - env.pop("GIT_INDEX_FILE", None) + # Unit tests patch REPO_ROOT to a temporary repository. Scrub the full + # inherited hook environment: a pre-commit run from a linked worktree + # exports an absolute GIT_DIR, which would silently point git at the + # real repository instead of the temp one. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return env diff --git a/scripts/release_control/governance_stage_guard_test.py b/scripts/release_control/governance_stage_guard_test.py index fcf6c7c1e..0807879f3 100644 --- a/scripts/release_control/governance_stage_guard_test.py +++ b/scripts/release_control/governance_stage_guard_test.py @@ -15,7 +15,11 @@ from governance_stage_guard import ( class GovernanceStageGuardTest(unittest.TestCase): def git(self, repo_root: Path, *args: str) -> subprocess.CompletedProcess: env = os.environ.copy() - env.pop("GIT_INDEX_FILE", None) + # Scrub the full hook environment: with only GIT_INDEX_FILE removed, a + # pre-commit run from a linked worktree exports an absolute GIT_DIR and + # "git init" here re-initializes the REAL repository as bare. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return subprocess.run( ["git", *args], cwd=repo_root, diff --git a/scripts/release_control/readiness_assertion_guard_test.py b/scripts/release_control/readiness_assertion_guard_test.py index d7c1ee1af..2c16b5bf8 100644 --- a/scripts/release_control/readiness_assertion_guard_test.py +++ b/scripts/release_control/readiness_assertion_guard_test.py @@ -36,7 +36,11 @@ def base_payload() -> dict: class ReadinessAssertionGuardTest(unittest.TestCase): def git(self, repo_root: Path, *args: str) -> subprocess.CompletedProcess: env = os.environ.copy() - env.pop("GIT_INDEX_FILE", None) + # Scrub the full hook environment: with only GIT_INDEX_FILE removed, a + # pre-commit run from a linked worktree exports an absolute GIT_DIR and + # "git init" here re-initializes the REAL repository as bare. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return subprocess.run( ["git", *args], cwd=repo_root, diff --git a/scripts/release_control/subsystem_contracts.py b/scripts/release_control/subsystem_contracts.py index a3149dfe3..e68698f5f 100644 --- a/scripts/release_control/subsystem_contracts.py +++ b/scripts/release_control/subsystem_contracts.py @@ -52,7 +52,12 @@ PATH_SUFFIXES = ( def git_env() -> dict[str, str]: env = os.environ.copy() if REPO_ROOT != DEFAULT_REPO_ROOT: - env.pop("GIT_INDEX_FILE", None) + # Unit tests patch REPO_ROOT to a temporary repository. Scrub the full + # inherited hook environment: a pre-commit run from a linked worktree + # exports an absolute GIT_DIR, which would silently point git at the + # real repository instead of the temp one. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return env diff --git a/scripts/release_control/subsystem_contracts_test.py b/scripts/release_control/subsystem_contracts_test.py index 9c4781e7e..cea26fef1 100644 --- a/scripts/release_control/subsystem_contracts_test.py +++ b/scripts/release_control/subsystem_contracts_test.py @@ -18,7 +18,11 @@ from subsystem_contracts import ( class SubsystemContractsTest(unittest.TestCase): def git(self, repo_root: Path, *args: str) -> subprocess.CompletedProcess: env = os.environ.copy() - env.pop("GIT_INDEX_FILE", None) + # Scrub the full hook environment: with only GIT_INDEX_FILE removed, a + # pre-commit run from a linked worktree exports an absolute GIT_DIR and + # "git init" here re-initializes the REAL repository as bare. + for name in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"): + env.pop(name, None) return subprocess.run( ["git", *args], cwd=repo_root,