fix: restore pinned Git in each Windows bootstrap stage

This commit is contained in:
ethernet
2026-09-24 04:41:57 -04:00
parent bb02b309f8
commit fb7fe862ef
2 changed files with 71 additions and 13 deletions

View File

@@ -443,8 +443,6 @@ function Get-Uv {
# the same store slot (<store>\git-<version>-<target>\) pm uses. Returns the
# git.exe path, or $null when no pinned artifact exists for this target.
function Get-PinnedGit {
$existing = Get-Command git -ErrorAction SilentlyContinue
if ($existing) { return $existing.Source } # dev shortcut; fetches nothing
$target = "win32-$(Get-WindowsArch)"
$pin = $script:GitPinFiles[$target]
if (-not $pin) { return $null }
@@ -482,18 +480,14 @@ function Get-PinnedGit {
return $gitExe
}
# Ensure a usable git for the rest of the ladder: pinned pm store slot
# first, then PATH. Returns $true on success.
# Each -Stage is a new PowerShell process. Restore the pinned pm store Git
# PATH in every stage that invokes git; never inherit an unpinned system Git.
function Ensure-Git {
$g = Get-PinnedGit
if (-not $g) { return $false }
if ($g -ne "git") {
# Store-staged git: expose cmd + usr\bin on this process's PATH so
# bare `git` works for the rest of the ladder (the same dirs pm's
# git package env() composes).
$gitEntry = Split-Path (Split-Path $g -Parent) -Parent
$env:Path = "$gitEntry\cmd;$gitEntry\usr\bin;$env:Path"
}
# The same dirs pm's git package env() composes.
$gitEntry = Split-Path (Split-Path $g -Parent) -Parent
$env:Path = "$gitEntry\cmd;$gitEntry\usr\bin;$env:Path"
return $true
}
@@ -552,12 +546,13 @@ $Stages = @(
$Stages += @{ name = "complete"; title = "Finish install"; category = "runtime"; needs_user_input = $false }
function Stage-Prerequisites {
if (-not (Ensure-Git)) {
Fail "git is required. Install Git for Windows: https://git-scm.com/download/win"
Fail "no pinned Git artifact for this Windows architecture"
}
Log "prerequisites ok (git)"
}
function Stage-Repository {
if (-not (Ensure-Git)) { Fail "no pinned Git artifact for this Windows architecture" }
# An interrupted clone from an older installer can leave a .git with no
# initial commit, where stash/checkout abort ("You do not have the initial
# commit yet", #40998). Move it aside -- never delete it, it may hold
@@ -886,7 +881,10 @@ function Confirm-DesktopArtifact {
function Stage-Complete {
$commit = $Commit
if (-not $commit) { $commit = Invoke-Native { git -C $InstallDir rev-parse HEAD 2>$null } }
if (-not $commit) {
if (-not (Ensure-Git)) { Fail "no pinned Git artifact for this Windows architecture" }
$commit = Invoke-Native { git -C $InstallDir rev-parse HEAD 2>$null }
}
if ($commit) {
$marker = [ordered]@{
schemaVersion = 1

View File

@@ -0,0 +1,60 @@
"""The bootstrap's separately launched stages must each use PM's pinned Git."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import pytest
INSTALLER = Path(__file__).resolve().parents[3] / "scripts" / "install.ps1"
@pytest.mark.platforms("windows")
def test_stage_processes_restore_pinned_git_and_never_fall_back(tmp_path):
powershell = shutil.which("powershell.exe")
system_git = shutil.which("git")
assert powershell and system_git
origin = tmp_path / "origin"
origin.mkdir()
subprocess.run([system_git, "-C", str(origin), "init", "-q", "-b", "main"], check=True)
(origin / "README").write_text("fixture", encoding="utf-8")
subprocess.run([system_git, "-C", str(origin), "add", "README"], check=True)
subprocess.run([system_git, "-C", str(origin), "-c", "user.name=Smoke",
"-c", "user.email=smoke@example.invalid", "commit", "-qm", "initial"], check=True)
expected = subprocess.run([system_git, "-C", str(origin), "rev-parse", "HEAD"],
check=True, capture_output=True, text=True).stdout.strip()
home = tmp_path / "home"
store = tmp_path / "tools"
env = dict(os.environ, HERMES_HOME=str(home), HERMES_RUNTIME_DIR=str(store),
HERMES_REPO_URL=str(origin))
def stage(name):
result = subprocess.run([powershell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", str(INSTALLER), "-Stage", name, "-Json"],
env=env, capture_output=True, text=True, timeout=240)
frames = [json.loads(line) for line in result.stdout.splitlines() if line.startswith("{")]
assert result.returncode == 0 and len(frames) == 1 and frames[0]["ok"], result.stdout + result.stderr
stage("prerequisites")
staged = list(store.glob("git-*/cmd/git.exe"))
assert len(staged) == 1
stage("repository") # new process; prerequisites' PATH cannot propagate
stage("complete") # the marker's bare git call is also a new process
checkout = home / "hermes-agent"
actual = subprocess.run([str(staged[0]), "-C", str(checkout), "rev-parse", "HEAD"],
check=True, capture_output=True, text=True).stdout.strip()
marker = json.loads((checkout / ".hermes-bootstrap-complete").read_text(encoding="utf-8-sig"))
assert actual == expected == marker["pinnedCommit"]
# Even when system Git is on PATH, an unsupported pin must fail closed.
probe = (f". '{INSTALLER}'; "
"$script:GitPinFiles.Remove(('win32-' + (Get-WindowsArch))); "
"if (Ensure-Git) { exit 1 } else { exit 0 }")
refused = subprocess.run([powershell, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-Command", probe], env=env, capture_output=True, text=True, timeout=30)
assert refused.returncode == 0, refused.stdout + refused.stderr