Summary
In a Windows job-level container: job, as soon as any step writes to $GITHUB_PATH (directly, or via a setup-* action), every subsequent in-container step fails to launch its shell:
container <id> encountered an error during hcs::System::CreateProcess: powershell ...:
The system cannot find the file specified. (0x2)
and the step exits 126/127.
Root cause
When a step appends to $GITHUB_PATH, ContainerStepHost.ExecuteAsync re-runs the in-container command with an explicit PATH override:
docker exec ... -e PATH="<prepended-dirs>:<ContainerRuntimePath>" <id> <shell> ...
ContainerRuntimePath is captured once at container start in ContainerOperationProvider from
docker inspect --format "{{range .Config.Env}}{{println .}}{{end}}" <id>, parsed by DockerUtil.ParsePathFromConfigEnv.
Two problems surface on Windows:
.Config.Env has no PATH. Typical Windows images (mcr.microsoft.com/windows/servercore, nanoserver, even the dotnet images) do not declare Path/PATH in their image config. A Windows container's real PATH (System32, PowerShell, …) is provided by the OS at runtime and is not visible via docker inspect .Config.Env. So ParsePathFromConfigEnv returns empty and ContainerRuntimePath is empty.
- POSIX separator. The override joins the prepended dirs and the base with a hard-coded
:, which is wrong on Windows (separator is ;, and drive letters contain :).
With an empty base, the override collapses to -e PATH="<prepended-dirs>" — the container's System32/PowerShell are gone, so the next step's shell can't be found.
Reproduction
docker inspect <id> --format "{{range .Config.Env}}{{println .}}{{end}}" shows no PATH, while docker exec <id> cmd /c echo %PATH% shows the real one. Replaying the exact command line the runner produces against a real Windows container:
# broken (empty base — what the runner emits today):
docker exec -e PATH="C:\probe" <id> powershell -NoProfile -Command "echo hi"
# -> hcs::System::CreateProcess ...: The system cannot find the file specified. (0x2), exit 127
# with the container's real PATH appended and ';' separator:
docker exec -e PATH="C:\probe;C:\Windows\system32;C:\Windows;..." <id> powershell -NoProfile -Command "echo hi"
# -> hi, exit 0
Minimal workflow that triggers it:
jobs:
repro:
runs-on: [self-hosted, windows]
container: mcr.microsoft.com/windows/servercore:ltsc2025
defaults: { run: { shell: powershell } }
steps:
- run: Add-Content -Path $env:GITHUB_PATH -Value 'C:\probe'
- run: Write-Host "next step's shell launches: $env:PATH" # fails today
Suggested fix
- In
ContainerOperationProvider, when .Config.Env yields no PATH on Windows, read the container's real PATH (docker exec <id> cmd /c echo %PATH%).
- In
ContainerStepHost.ExecuteAsync, join with Path.PathSeparator instead of :.
Small patch (2 files, ~9 lines); PR to follow.
Summary
In a Windows job-level
container:job, as soon as any step writes to$GITHUB_PATH(directly, or via asetup-*action), every subsequent in-container step fails to launch its shell:and the step exits 126/127.
Root cause
When a step appends to
$GITHUB_PATH,ContainerStepHost.ExecuteAsyncre-runs the in-container command with an explicit PATH override:ContainerRuntimePathis captured once at container start inContainerOperationProviderfromdocker inspect --format "{{range .Config.Env}}{{println .}}{{end}}" <id>, parsed byDockerUtil.ParsePathFromConfigEnv.Two problems surface on Windows:
.Config.Envhas no PATH. Typical Windows images (mcr.microsoft.com/windows/servercore,nanoserver, even thedotnetimages) do not declarePath/PATHin their image config. A Windows container's real PATH (System32, PowerShell, …) is provided by the OS at runtime and is not visible viadocker inspect .Config.Env. SoParsePathFromConfigEnvreturns empty andContainerRuntimePathis empty.:, which is wrong on Windows (separator is;, and drive letters contain:).With an empty base, the override collapses to
-e PATH="<prepended-dirs>"— the container's System32/PowerShell are gone, so the next step's shell can't be found.Reproduction
docker inspect <id> --format "{{range .Config.Env}}{{println .}}{{end}}"shows no PATH, whiledocker exec <id> cmd /c echo %PATH%shows the real one. Replaying the exact command line the runner produces against a real Windows container:Minimal workflow that triggers it:
Suggested fix
ContainerOperationProvider, when.Config.Envyields no PATH on Windows, read the container's real PATH (docker exec <id> cmd /c echo %PATH%).ContainerStepHost.ExecuteAsync, join withPath.PathSeparatorinstead of:.Small patch (2 files, ~9 lines); PR to follow.