From 0042cfe0d0162c72ea3cc3a940ebd523350ea05d Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sat, 17 Oct 2026 20:29:10 +0800 Subject: [PATCH 1/4] test(e2e): share one tools stage across the Windows images and drop the MSIs Build the compose runner and the per-combo image from one multi-stage Dockerfile, so pwsh, Git and Go install once per host and the suite's own build only copies the plugin in. Unpack pwsh and Go from their zips instead of msiexec, reuse the image Go for vfox@main instead of a second MSI install per combo, and run every Windows combo at once in CI. --- .dockerignore | 6 ++-- .github/workflows/e2e.yml | 11 ++++-- .licenserc.yaml | 4 +-- docs/e2e.md | 16 ++++++--- tests/e2e/windows/Dockerfile | 27 +++++++++++++- tests/e2e/windows/Dockerfile-runner | 40 --------------------- tests/e2e/windows/bootstrap-tools.ps1 | 51 ++++++++++++++++++++++----- tests/e2e/windows/compose.amd64.yaml | 13 ++++--- tests/e2e/windows/compose.arm64.yaml | 13 ++++--- tests/e2e/windows/e2e_test.go | 7 ++-- tests/e2e/windows/setup.go | 29 ++++++--------- 11 files changed, 125 insertions(+), 92 deletions(-) delete mode 100644 tests/e2e/windows/Dockerfile-runner diff --git a/.dockerignore b/.dockerignore index 5e65fbb..c0ebe42 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# NOTE: tests/ is deliberately kept in the context: the Windows runner image -# (tests/e2e/windows/Dockerfile-runner) is built from the repo root, and both -# Windows Dockerfiles live under tests/. +# NOTE: tests/ is deliberately kept in the context: the Windows images +# (both stages of tests/e2e/windows/Dockerfile) are built from the repo root +# and copy tests/e2e/windows/bootstrap-tools.ps1. .git .gitignore .gitattributes diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 302b23c..80ce8f5 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,9 +29,9 @@ concurrency: # The Linux and WSL jobs run the suite natively, the way the dev container # runs it locally: Go at the dev container's version (.devcontainer/Dockerfile) # plus the runner's stock Docker, with the same TESTCONTAINERS_RYUK_DISABLED -# knob. The Windows jobs keep the compose runner image -# (tests/e2e/windows/Dockerfile-runner), which carries its own Go toolchain, -# so a Windows host needs only Docker. See docs/e2e.md. +# knob. The Windows jobs keep the compose runner image (the `runner` stage of +# tests/e2e/windows/Dockerfile), which carries its own Go toolchain, so a +# Windows host needs only Docker. See docs/e2e.md. jobs: e2e-linux: @@ -100,6 +100,11 @@ jobs: compose: tests/e2e/windows/compose.arm64.yaml runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 + env: + # Run every combo at once (amd64 has three, arm64 two) instead of the + # local default of 2, so the slowest combo alone bounds the test phase + # rather than a queued one running after it (see docs/e2e.md). + E2E_MAX_JOBS: "3" steps: - uses: actions/checkout@v7 with: diff --git a/.licenserc.yaml b/.licenserc.yaml index 396e658..db4ae09 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -16,9 +16,9 @@ # Dockerfiles, ignore files and similar text formats are covered. # Some headers stay hand-written because `header fix` cannot place them: # go.mod, .gitattributes and .dockerignore have no comment style upstream, -# the Windows Dockerfiles must keep `# escape=` on line 1 (their headers sit +# the Windows Dockerfile must keep `# escape=` on line 1 (its header sits # right below it), and the fixer only recognises the exact name `Dockerfile`, -# not Dockerfile-runner. The checker still verifies them all. +# not Dockerfile-alpine. The checker still verifies them all. # PowerShell is check-only here (no `<# ... #>` fix support upstream): # tests/e2e/windows/*.ps1 is fixed and checked with addlicense instead (see # the addlicense step in .github/workflows/license.yml and docs/e2e.md). diff --git a/docs/e2e.md b/docs/e2e.md index a38c87b..381ff11 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -156,9 +156,14 @@ suite needs an ARM64 host (a Copilot+ PC or the `windows-11-arm` CI runner). There is no dev container for this suite — the Dev Containers spec rejects Windows container images (see [Dev container](#dev-container)) — so it keeps -its Docker Compose runner. The runner image (`Dockerfile-runner`: stock -servercore plus the pinned Go MSI) drives the host daemon through the mounted -named pipe; the host requirement is Docker in Windows-container mode plus the +its Docker Compose runner. `tests/e2e/windows/Dockerfile` holds both Windows +images: a shared `tools` stage (stock servercore plus pwsh, Git for Windows +and the pinned Go; pwsh and Go are unpacked from their zips rather than run +through msiexec, which is the slow path in a container), the compose `runner` stage +on top of it, and the final `e2e` stage the suite runs per combo. Compose +builds `runner` first, so the suite's own build reuses the `tools` layers +and only copies the plugin in; the same Go also builds `vfox@main` inside +each combo. The runner drives the host daemon through the mounted named pipe; the host requirement is Docker in Windows-container mode plus the Compose v5 plugin (installed once per machine below). Go never touches the host, so it cannot conflict with a vfox-managed toolchain. @@ -244,7 +249,10 @@ docker compose -f tests/e2e/windows/compose.arm64.yaml run --rm --build e2e On arm64, Go is also what builds the engine, so `install-docker-arm64.ps1` runs before the suite; the compose runner image then carries its own pinned -Go for the tests, and the host Go is not used again. Scope the run down with +Go for the tests, and the host Go is not used again. + +CI sets `E2E_MAX_JOBS=3` on both Windows jobs so every combo runs at once; +the local default stays at 2. Scope the run down with `$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'` in front of the compose command, as on the other platforms. diff --git a/tests/e2e/windows/Dockerfile b/tests/e2e/windows/Dockerfile index 1452dc3..e8a0489 100644 --- a/tests/e2e/windows/Dockerfile +++ b/tests/e2e/windows/Dockerfile @@ -12,14 +12,39 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +# One Dockerfile for both Windows images, so the slow part (base pull plus +# tool installs) runs once per host: +# +# tools stock servercore plus pwsh, Git for Windows and the pinned Go +# runner compose target (compose.amd64.yaml / compose.arm64.yaml) that +# runs `go test` against the host daemon +# e2e the per-combo target image, built by e2e_test.go +# +# compose builds `runner` first; the suite then builds the final stage from +# the same context and build args, so `tools` is a cache hit and only the +# plugin COPY layers are new. The Go in `tools` is also what builds +# vfox@main inside each combo (GO_VERSION must match goWindowsVersion in +# setup.go, which the suite asserts). ARG BASE_IMAGE=mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 -FROM ${BASE_IMAGE} +FROM ${BASE_IMAGE} AS tools SHELL ["powershell", "-NoProfile", "-Command"] +ARG GO_VERSION=1.27.1 COPY tests/e2e/windows/bootstrap-tools.ps1 C:\bootstrap-tools.ps1 RUN powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\bootstrap-tools.ps1' +FROM tools AS runner + +# Short module/build caches, kept out of the slot profiles; C:\m is also +# the compose module-cache volume. +RUN New-Item -ItemType Directory -Force -Path 'C:\m', 'C:\b' | Out-Null +ENV GOMODCACHE=C:\m +ENV GOCACHE=C:\b + +FROM tools AS e2e + WORKDIR C:\e2e COPY metadata.lua C:\e2e\metadata.lua diff --git a/tests/e2e/windows/Dockerfile-runner b/tests/e2e/windows/Dockerfile-runner deleted file mode 100644 index 8bacead..0000000 --- a/tests/e2e/windows/Dockerfile-runner +++ /dev/null @@ -1,40 +0,0 @@ -# escape=` -# Copyright 2026 Han Li and contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Runner image for the Windows container E2E suite: stock servercore plus the -# pinned Go toolchain, so no host Go is needed on any arch. (There is no -# stock golang Windows image for arm64, hence this file instead of pinning -# one.) Built via compose --build (see compose.amd64.yaml / compose.arm64.yaml). -ARG BASE_IMAGE=mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 -FROM ${BASE_IMAGE} - -SHELL ["powershell", "-NoProfile", "-Command"] - -ARG GO_VERSION=1.27.1 -# NB: no double quotes or backticks below. The classic builder wraps the -# joined lines in powershell -Command "..." through cmd.exe, where both -# break parsing. Single quotes, ${var} braces and array arguments only. -RUN $goArch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'amd64' }; ` - $goMsi = $env:TEMP + '\go.msi'; ` - curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors -o $goMsi https://go.dev/dl/go${env:GO_VERSION}.windows-${goArch}.msi; ` - $goInstall = Start-Process msiexec.exe -Wait -PassThru -ArgumentList '/i', $goMsi, '/quiet', '/norestart'; ` - if ($goInstall.ExitCode -notin 0, 3010, 1937) { throw 'FAIL the Go MSI install exited with code ' + $goInstall.ExitCode }; ` - Remove-Item $goMsi; ` - New-Item -ItemType Directory -Force -Path 'C:\m', 'C:\b' | Out-Null; ` - & 'C:\Program Files\Go\bin\go.exe' version - -# Short module/build caches: Windows long paths are off in this image. -ENV GOMODCACHE=C:\m -ENV GOCACHE=C:\b diff --git a/tests/e2e/windows/bootstrap-tools.ps1 b/tests/e2e/windows/bootstrap-tools.ps1 index 7d04931..570b63d 100644 --- a/tests/e2e/windows/bootstrap-tools.ps1 +++ b/tests/e2e/windows/bootstrap-tools.ps1 @@ -32,22 +32,57 @@ function Invoke-Native { if ($code -notin $Ok) { throw "FAIL $What exited with code $code" } } +if (-not $env:GO_VERSION) { throw 'FAIL GO_VERSION is not set (Dockerfile ARG)' } + $IsArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' $pwshArch = if ($IsArm64) { 'arm64' } else { 'x64' } +$goArch = if ($IsArm64) { 'arm64' } else { 'amd64' } $gitAsset = if ($IsArm64) { 'Git-2.55.0.5-arm64.exe' } else { 'Git-2.55.0.5-64-bit.exe' } -$pwshInstaller = "$env:TEMP\pwsh.msi" -$pwshUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-$pwshArch.msi" +# pwsh and Go ship as zips and are unpacked with tar.exe: msiexec inside a +# container is the slow path (minutes per MSI on the hosted runners), while +# the zips are the same payload the official golang / powershell Windows +# images unpack. Git keeps its installer, so Git Bash stays a stock Git for +# Windows install; it runs while the zips are unpacked. +$pwshArchive = "$env:TEMP\pwsh.zip" +$pwshUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-$pwshArch.zip" +$goArchive = "$env:TEMP\go.zip" +$goUrl = "https://go.dev/dl/go$($env:GO_VERSION).windows-$goArch.zip" $gitInstaller = "$env:TEMP\git-installer.exe" $gitUrl = "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/$gitAsset" -Invoke-Native { & curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors -o $pwshInstaller $pwshUrl } 'the PowerShell download' -Invoke-Native { & curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors -o $gitInstaller $gitUrl } 'the Git download' -$pwshSetup = Start-Process msiexec.exe -Wait -PassThru -ArgumentList '/i', "`"$pwshInstaller`"", '/quiet', '/norestart' -if ($pwshSetup.ExitCode -notin 0, 3010) { throw "FAIL the PowerShell MSI install exited with code $($pwshSetup.ExitCode)" } -$gitSetup = Start-Process $gitInstaller -Wait -PassThru -ArgumentList '/VERYSILENT', '/NORESTART', '/SP-', '/SUPPRESSMSGBOXES' +$pwshHome = 'C:\Program Files\PowerShell\7' +$goHome = 'C:\Program Files\Go' + +Invoke-Native { + & curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors --parallel ` + -o $pwshArchive $pwshUrl -o $goArchive $goUrl -o $gitInstaller $gitUrl +} 'the tool downloads' + +$gitSetup = Start-Process $gitInstaller -PassThru -ArgumentList '/VERYSILENT', '/NORESTART', '/SP-', '/SUPPRESSMSGBOXES' +# Touch the handle now, or ExitCode stays empty once the process is gone. +$null = $gitSetup.Handle + +New-Item -ItemType Directory -Force -Path $pwshHome | Out-Null +Invoke-Native { & tar.exe -xf $pwshArchive -C $pwshHome } 'the PowerShell unpack' +# The Go zip has a single top-level go\ directory. +Invoke-Native { & tar.exe -xf $goArchive -C 'C:\Program Files' } 'the Go unpack' +Remove-Item $pwshArchive, $goArchive + +$gitSetup.WaitForExit() if ($gitSetup.ExitCode -ne 0) { throw "FAIL the Git installer exited with code $($gitSetup.ExitCode)" } -if (-not (Test-Path 'C:\Program Files\PowerShell\7\pwsh.exe')) { throw 'FAIL pwsh.exe is missing after the PowerShell MSI install' } +Remove-Item $gitInstaller + +if (-not (Test-Path "$pwshHome\pwsh.exe")) { throw 'FAIL pwsh.exe is missing after the PowerShell unpack' } +if (-not (Test-Path "$goHome\bin\go.exe")) { throw 'FAIL go.exe is missing after the Go unpack' } if (-not (Test-Path 'C:\Program Files\Git\cmd\git.exe')) { throw 'FAIL git.exe is missing after the Git installer' } + +# The zips do not touch PATH (the MSIs did). Append after the Git installer +# finished, since it rewrites the machine PATH itself. +$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') +[Environment]::SetEnvironmentVariable('Path', "$machinePath;$pwshHome;$goHome\bin", 'Machine') + +Invoke-Native { & "$pwshHome\pwsh.exe" -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' } 'pwsh' +Invoke-Native { & "$goHome\bin\go.exe" version } 'go version' Invoke-Native { & 'C:\Program Files\Git\cmd\git.exe' config --system core.longpaths true } 'enable system-wide git long paths' New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -PropertyType DWORD -Force | Out-Null diff --git a/tests/e2e/windows/compose.amd64.yaml b/tests/e2e/windows/compose.amd64.yaml index e9ac888..4f03534 100644 --- a/tests/e2e/windows/compose.amd64.yaml +++ b/tests/e2e/windows/compose.amd64.yaml @@ -19,10 +19,12 @@ # # The host requirement is Docker plus the Compose v5 plugin (installed once # per machine, see docs/e2e.md); Go never touches the host, so it cannot -# conflict with a vfox-managed toolchain. The runner image -# (Dockerfile-runner: stock servercore plus the pinned Go MSI) is built for -# the host arch, because there is no stock golang Windows image for arm64. -# It drives the host daemon through the mounted named pipe. Ryuk is disabled +# conflict with a vfox-managed toolchain. The runner image is the `runner` +# stage of tests/e2e/windows/Dockerfile (stock servercore plus pwsh, Git and +# the pinned Go), built for the host arch, because there is no stock golang +# Windows image for arm64. The suite's own target image shares that tools +# stage, so its build is a cache hit. The runner drives the host daemon +# through the mounted named pipe. Ryuk is disabled # in code (it has no Windows image); every container is terminated explicitly # via t.Cleanup instead. # @@ -37,7 +39,8 @@ services: e2e: build: context: ../../.. - dockerfile: tests/e2e/windows/Dockerfile-runner + dockerfile: tests/e2e/windows/Dockerfile + target: runner args: BASE_IMAGE: mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 image: vfox-nodejs/e2e-runner:1.27.1 diff --git a/tests/e2e/windows/compose.arm64.yaml b/tests/e2e/windows/compose.arm64.yaml index f771f67..c79286c 100644 --- a/tests/e2e/windows/compose.arm64.yaml +++ b/tests/e2e/windows/compose.arm64.yaml @@ -19,10 +19,12 @@ # # The host requirement is Docker plus the Compose v5 plugin (installed once # per machine, see docs/e2e.md); Go never touches the host, so it cannot -# conflict with a vfox-managed toolchain. The runner image -# (Dockerfile-runner: stock servercore plus the pinned Go MSI) is built for -# the host arch, because there is no stock golang Windows image for arm64. -# It drives the host daemon through the mounted named pipe. Ryuk is disabled +# conflict with a vfox-managed toolchain. The runner image is the `runner` +# stage of tests/e2e/windows/Dockerfile (stock servercore plus pwsh, Git and +# the pinned Go), built for the host arch, because there is no stock golang +# Windows image for arm64. The suite's own target image shares that tools +# stage, so its build is a cache hit. The runner drives the host daemon +# through the mounted named pipe. Ryuk is disabled # in code (it has no Windows image); every container is terminated explicitly # via t.Cleanup instead. # @@ -37,7 +39,8 @@ services: e2e: build: context: ../../.. - dockerfile: tests/e2e/windows/Dockerfile-runner + dockerfile: tests/e2e/windows/Dockerfile + target: runner args: BASE_IMAGE: mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-arm64 image: vfox-nodejs/e2e-runner:1.27.1 diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index 6b17837..582e0b9 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -90,8 +90,10 @@ func TestE2E(t *testing.T) { } // buildImage replicates `docker build --pull --platform $platform -// --build-arg BASE_IMAGE=$baseImage -f tests/e2e/windows/Dockerfile -// -t $image $repoRoot`. +// --build-arg BASE_IMAGE=$baseImage --target e2e +// -f tests/e2e/windows/Dockerfile -t $image $repoRoot`. Under the compose +// runner the shared tools stage is already built, so only the plugin COPY +// layers run here. func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, baseImage string) { t.Helper() @@ -118,6 +120,7 @@ func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, ba BuildOptionsModifier: func(opts *client.ImageBuildOptions) { opts.Platforms = []specs.Platform{pf} opts.PullParent = true + opts.Target = "e2e" }, }, }) diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index dfe5c3f..4c78810 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -29,8 +29,9 @@ import ( // Node.js defaults live in the common package; only the Windows-specific // toolchain pin stays here. const ( - // goWindowsVersion pins the Go toolchain MSI used to build vfox@main - // inside the container. + // goWindowsVersion pins the Go toolchain that builds vfox@main inside + // the container. The image installs it (GO_VERSION in + // tests/e2e/windows/Dockerfile); installVfoxMain asserts both agree. goWindowsVersion = "1.27.1" // spacedVfoxHome is the default vfox home of a Windows account whose @@ -158,29 +159,19 @@ func installVfoxRelease(ctx context.Context, t *testing.T, ctr tc.Container, box t.Logf("PASS installed vfox %s (%s)", tag, vfoxArch) } -// installVfoxMain installs the pinned Go MSI, clones vfox, and builds -// vfox.exe. msiexec exit codes 0/3010/1937 all mean success (ok / -// reboot-required / in-use-files). +// installVfoxMain clones vfox and builds vfox.exe with the Go toolchain +// baked into the image (bootstrap-tools.ps1), after checking it is the +// pinned version. func installVfoxMain(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, procArch string) { t.Helper() goArch := "amd64" if procArch == "ARM64" { goArch = "arm64" } - goMsi := box.tmp + `\go.msi` - execOK(ctx, t, ctr, fmt.Sprintf( - "curl.exe %s -fsSL -o %s %s", - curlRetry, pwshQuote(goMsi), - pwshQuote(fmt.Sprintf("https://go.dev/dl/go%s.windows-%s.msi", goWindowsVersion, goArch))), - box.vars...) - execOK(ctx, t, ctr, fmt.Sprintf(`$goInstall = $null -for ($attempt = 1; $attempt -le 5; $attempt++) { - $goInstall = Start-Process msiexec.exe -Wait -PassThru -ArgumentList '/i', %s, '/quiet', '/norestart' - if ($goInstall.ExitCode -in 0, 3010, 1937) { break } - Start-Sleep -Seconds 15 -} -if ($goInstall.ExitCode -notin 0, 3010, 1937) { throw ('FAIL the Go MSI install exited with code ' + $goInstall.ExitCode) } -Remove-Item %s`, pwshQuote(goMsi), pwshQuote(goMsi)), box.vars...) + goVersion := execOK(ctx, t, ctr, `& 'C:\Program Files\Go\bin\go.exe' version`, box.vars...) + want := fmt.Sprintf("go%s windows/%s", goWindowsVersion, goArch) + require.Containsf(t, goVersion, want, + "image Go differs from goWindowsVersion; keep GO_VERSION in tests/e2e/windows/Dockerfile in sync") src := box.workDir + `\vfox-src` execOK(ctx, t, ctr, From b0619eaf93ad1499c2791eff26acc533f741a961 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sat, 17 Oct 2026 21:05:56 +0800 Subject: [PATCH 2/4] test(e2e): run the Windows suite from a host script with bind-mounted tools Replace the compose runner with tests/e2e/windows/run.ps1. It pulls the servercore base image in the background while it fetches pwsh, PortableGit and the pinned Go into a per-arch host cache, builds vfox@main once, and compiles the suite. The cache is bind-mounted read-only at C:\tools into every combo container, so the image no longer commits a layer of tens of thousands of tool files, which took 2-6 minutes on the hosted runners. --- .dockerignore | 6 +- .github/workflows/e2e.yml | 19 +-- docs/e2e.md | 68 +++++----- tests/e2e/doc.go | 5 +- tests/e2e/go.mod | 2 +- tests/e2e/windows/Dockerfile | 41 ++---- tests/e2e/windows/bootstrap-tools.ps1 | 88 ------------- tests/e2e/windows/compose.amd64.yaml | 64 --------- tests/e2e/windows/compose.arm64.yaml | 64 --------- tests/e2e/windows/e2e_test.go | 39 ++++-- tests/e2e/windows/exec.go | 2 +- tests/e2e/windows/run.ps1 | 183 ++++++++++++++++++++++++++ tests/e2e/windows/setup.go | 40 ++---- tests/e2e/wsl/setup.go | 2 +- 14 files changed, 280 insertions(+), 343 deletions(-) delete mode 100644 tests/e2e/windows/bootstrap-tools.ps1 delete mode 100644 tests/e2e/windows/compose.amd64.yaml delete mode 100644 tests/e2e/windows/compose.arm64.yaml create mode 100644 tests/e2e/windows/run.ps1 diff --git a/.dockerignore b/.dockerignore index c0ebe42..e73ad74 100644 --- a/.dockerignore +++ b/.dockerignore @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -# NOTE: tests/ is deliberately kept in the context: the Windows images -# (both stages of tests/e2e/windows/Dockerfile) are built from the repo root -# and copy tests/e2e/windows/bootstrap-tools.ps1. +# NOTE: tests/ is deliberately kept in the context: the Windows image +# (tests/e2e/windows/Dockerfile) is built from the repo root, and its +# Dockerfile lives under tests/. .git .gitignore .gitattributes diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 80ce8f5..e333688 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,9 +29,9 @@ concurrency: # The Linux and WSL jobs run the suite natively, the way the dev container # runs it locally: Go at the dev container's version (.devcontainer/Dockerfile) # plus the runner's stock Docker, with the same TESTCONTAINERS_RYUK_DISABLED -# knob. The Windows jobs keep the compose runner image (the `runner` stage of -# tests/e2e/windows/Dockerfile), which carries its own Go toolchain, so a -# Windows host needs only Docker. See docs/e2e.md. +# knob. The Windows jobs run tests/e2e/windows/run.ps1, which fetches its own +# pinned Go and tools into a cache dir, so a Windows host needs only Docker. +# See docs/e2e.md. jobs: e2e-linux: @@ -93,11 +93,6 @@ jobs: matrix: # TODO See https://github.com/actions/runner-images/issues/13729 runs-on: [windows-latest, windows-11-arm] - include: - - runs-on: windows-latest - compose: tests/e2e/windows/compose.amd64.yaml - - runs-on: windows-11-arm - compose: tests/e2e/windows/compose.arm64.yaml runs-on: ${{ matrix.runs-on }} timeout-minutes: 60 env: @@ -118,13 +113,7 @@ jobs: cache: false - if: matrix.runs-on == 'windows-11-arm' run: pwsh -NoProfile -File tests/e2e/windows/install-docker-arm64.ps1 - - if: matrix.runs-on == 'windows-11-arm' - name: Install Compose CLI plugin - run: | - New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null - curl.exe -fsSL "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-aarch64.exe" -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" - docker compose version - - run: docker compose -f ${{ matrix.compose }} run --rm --build e2e + - run: pwsh -NoProfile -File tests/e2e/windows/run.ps1 e2e-wsl: if: github.repository == 'version-fox/vfox-nodejs' diff --git a/docs/e2e.md b/docs/e2e.md index 381ff11..0216375 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -5,8 +5,9 @@ workflow. The end-to-end suite runs in containers against real vfox on Linux and Windows; each `vfox` x `mirror` combination gets its own throwaway container. On Linux, CI runs the suite natively on the stock runners (Docker plus Go from `actions/setup-go`), the way the repository dev container -reproduces it locally. On Windows it runs through a Docker Compose runner -image, since dev containers cannot host Windows container images (see below). +reproduces it locally. On Windows it runs from the host through +`tests/e2e/windows/run.ps1`, since dev containers cannot host Windows +container images (see below). ## Dev container @@ -58,7 +59,8 @@ Every combination runs the same flow (one long-lived container per combo, driven from Go through `docker exec`-style calls): 1. vfox is installed from the requested channel — the latest release - (`latest`) or built from source with the in-image Go toolchain (`main`) — + (`latest`) or built from source with the pinned Go (`main`; inside the + container on Linux, once per run on the host by `run.ps1` on Windows) — and the local `nodejs` plugin is registered from this checkout. 2. `$VFOX_NODEJS_MIRROR` is honoured positively and negatively: the configured mirror serves the version index, while a bogus mirror makes @@ -155,17 +157,24 @@ Windows containers cannot be emulated across architectures, so the ARM64 suite needs an ARM64 host (a Copilot+ PC or the `windows-11-arm` CI runner). There is no dev container for this suite — the Dev Containers spec rejects -Windows container images (see [Dev container](#dev-container)) — so it keeps -its Docker Compose runner. `tests/e2e/windows/Dockerfile` holds both Windows -images: a shared `tools` stage (stock servercore plus pwsh, Git for Windows -and the pinned Go; pwsh and Go are unpacked from their zips rather than run -through msiexec, which is the slow path in a container), the compose `runner` stage -on top of it, and the final `e2e` stage the suite runs per combo. Compose -builds `runner` first, so the suite's own build reuses the `tools` layers -and only copies the plugin in; the same Go also builds `vfox@main` inside -each combo. The runner drives the host daemon through the mounted named pipe; the host requirement is Docker in Windows-container mode plus the -Compose v5 plugin (installed once per machine below). Go never touches the -host, so it cannot conflict with a vfox-managed toolchain. +Windows container images (see [Dev container](#dev-container)) — so it runs +from the Windows host through `tests/e2e/windows/run.ps1`. The host +requirement is Docker in Windows-container mode; the script needs nothing +beyond the `curl.exe` and `tar.exe` that ship with Windows. It fetches the +pinned PowerShell, PortableGit (Git for Windows, including Git Bash) and Go +into a per-arch cache (`$env:E2E_CACHE_DIR`, default +`%LOCALAPPDATA%\vfox-nodejs-e2e\`, reused across runs), builds +`vfox@main` once with that Go, compiles the suite, and runs it. None of it is +installed or put on the user `PATH`, so it cannot conflict with a +vfox-managed toolchain. + +The cached tools are bind-mounted read-only at `C:\tools` into every combo +container, so `tests/e2e/windows/Dockerfile` only adds `PATH` entries and the +plugin files to stock servercore. Baking the tools into an image layer +instead costs minutes of layer commit on the hosted runners (they are tens of +thousands of files). The servercore pull — five to seven minutes on a fresh +CI runner — runs in the background while the tools are fetched and the Go +code is compiled. The `windows-11-arm` runner has no Windows Hypervisor Platform feature, so Hyper-V isolation is unavailable; process isolation needs only the @@ -208,15 +217,9 @@ curl.exe -fsSL ` 3. Execute in PowerShell 7, ```powershell -New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null - -curl.exe -fsSL ` - "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-x86_64.exe" ` - -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" - git clone git@github.com:version-fox/vfox-nodejs.git cd ./vfox-nodejs/ -docker compose -f tests/e2e/windows/compose.amd64.yaml run --rm --build e2e +pwsh -NoProfile -File tests/e2e/windows/run.ps1 ``` ### arm64 @@ -238,23 +241,18 @@ winget install --id GoLang.Go --source winget --exact git clone git@github.com:version-fox/vfox-nodejs.git cd ./vfox-nodejs/ pwsh -NoProfile -File .\tests\e2e\windows\install-docker-arm64.ps1 -New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.docker\cli-plugins" | Out-Null - -curl.exe -fsSL ` - "https://github.com/docker/compose/releases/download/v5.5.1/docker-compose-windows-aarch64.exe" ` - -o "$env:USERPROFILE\.docker\cli-plugins\docker-compose.exe" - -docker compose -f tests/e2e/windows/compose.arm64.yaml run --rm --build e2e +pwsh -NoProfile -File .\tests\e2e\windows\run.ps1 ``` -On arm64, Go is also what builds the engine, so `install-docker-arm64.ps1` -runs before the suite; the compose runner image then carries its own pinned -Go for the tests, and the host Go is not used again. +On arm64, a host Go is needed only to build the engine +(`install-docker-arm64.ps1`); `run.ps1` uses its own cached Go for the suite. -CI sets `E2E_MAX_JOBS=3` on both Windows jobs so every combo runs at once; -the local default stays at 2. Scope the run down with -`$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'` in front of the -compose command, as on the other platforms. +`$env:E2E_RUN` narrows the `-test.run` pattern (default `TestE2E`), e.g. +`TestE2E/latest-default`. Scope the run down with +`$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'` in front of it, as on +the other platforms; without `main` in `VFOX_VERSION` the script skips the +`vfox@main` build. CI sets `E2E_MAX_JOBS=3` on both Windows jobs so every +combo runs at once; the local default stays at 2. ## WSL diff --git a/tests/e2e/doc.go b/tests/e2e/doc.go index dabc136..9b9b327 100644 --- a/tests/e2e/doc.go +++ b/tests/e2e/doc.go @@ -22,9 +22,8 @@ // defaults, retry counts, name handling); // - linux: the Linux container suite (bash-driven), run in CI and locally // through the repository dev container (.devcontainer/, see docs/e2e.md); -// - windows: the Windows container suite (pwsh-driven), run via -// tests/e2e/windows/compose.amd64.yaml (amd64) or -// tests/e2e/windows/compose.arm64.yaml (arm64). +// - windows: the Windows container suite (pwsh-driven), run on a Windows +// host via tests/e2e/windows/run.ps1. // - wsl: the WSL suite (bash-driven through wsl.exe, no containers), // run on a windows-2025 host with an installed distro; it reproduces // https://github.com/version-fox/vfox-nodejs/issues/32. diff --git a/tests/e2e/go.mod b/tests/e2e/go.mod index 000b43f..ebba9d4 100644 --- a/tests/e2e/go.mod +++ b/tests/e2e/go.mod @@ -19,6 +19,7 @@ go 1.25.0 require ( github.com/cenkalti/backoff/v7 v7.0.1 github.com/containerd/platforms v0.2.1 + github.com/moby/moby/api v1.55.0 github.com/moby/moby/client v0.5.0 github.com/opencontainers/image-spec v1.1.1 github.com/stretchr/testify v1.12.1 @@ -49,7 +50,6 @@ require ( github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/moby/api v1.55.0 // indirect github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/user v0.4.0 // indirect diff --git a/tests/e2e/windows/Dockerfile b/tests/e2e/windows/Dockerfile index e8a0489..879527d 100644 --- a/tests/e2e/windows/Dockerfile +++ b/tests/e2e/windows/Dockerfile @@ -13,37 +13,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -# One Dockerfile for both Windows images, so the slow part (base pull plus -# tool installs) runs once per host: -# -# tools stock servercore plus pwsh, Git for Windows and the pinned Go -# runner compose target (compose.amd64.yaml / compose.arm64.yaml) that -# runs `go test` against the host daemon -# e2e the per-combo target image, built by e2e_test.go -# -# compose builds `runner` first; the suite then builds the final stage from -# the same context and build args, so `tools` is a cache hit and only the -# plugin COPY layers are new. The Go in `tools` is also what builds -# vfox@main inside each combo (GO_VERSION must match goWindowsVersion in -# setup.go, which the suite asserts). +# Per-combo image for the Windows container E2E suite, built by e2e_test.go. +# pwsh, Git for Windows and vfox@main are not baked in: run.ps1 fetches them +# on the host and the suite bind-mounts them read-only at C:\tools, because +# committing an image layer with their tens of thousands of files takes +# minutes on the hosted runners. This image only points PATH at the mount +# and adds the plugin files. ARG BASE_IMAGE=mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 -FROM ${BASE_IMAGE} AS tools +FROM ${BASE_IMAGE} SHELL ["powershell", "-NoProfile", "-Command"] -ARG GO_VERSION=1.27.1 -COPY tests/e2e/windows/bootstrap-tools.ps1 C:\bootstrap-tools.ps1 -RUN powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\bootstrap-tools.ps1' - -FROM tools AS runner - -# Short module/build caches, kept out of the slot profiles; C:\m is also -# the compose module-cache volume. -RUN New-Item -ItemType Directory -Force -Path 'C:\m', 'C:\b' | Out-Null -ENV GOMODCACHE=C:\m -ENV GOCACHE=C:\b - -FROM tools AS e2e +# NB: no double quotes or backticks inside the command. The classic builder +# wraps it in powershell -Command "..." through cmd.exe, where both break +# parsing. Single quotes only. +RUN [Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', 'Machine') + ';C:\tools\pwsh;C:\tools\git\cmd', 'Machine'); ` + New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -PropertyType DWORD -Force | Out-Null WORKDIR C:\e2e @@ -53,5 +38,5 @@ COPY lib C:\e2e\lib # Long-lived container: the Go test (e2e_test.go) drives every phase through # pwsh exec calls, so the main process just idles here. -ENTRYPOINT ["C:/Program Files/PowerShell/7/pwsh.exe", "-NoProfile", "-Command"] +ENTRYPOINT ["C:/tools/pwsh/pwsh.exe", "-NoProfile", "-Command"] CMD ["while ($true) { Start-Sleep 3600 }"] diff --git a/tests/e2e/windows/bootstrap-tools.ps1 b/tests/e2e/windows/bootstrap-tools.ps1 deleted file mode 100644 index 570b63d..0000000 --- a/tests/e2e/windows/bootstrap-tools.ps1 +++ /dev/null @@ -1,88 +0,0 @@ -<# - Copyright 2026 Han Li and contributors - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -#> - -$ErrorActionPreference = 'Stop' - -# Local copy of the native-command guard. Fails unless the command exits 0. -function Invoke-Native { - param( - [Parameter(Mandatory)] [scriptblock] $Cmd, - [Parameter(Mandatory)] [string] $What, - [int[]] $Ok = 0 - ) - $prev = $PSNativeCommandUseErrorActionPreference - $PSNativeCommandUseErrorActionPreference = $false - & $Cmd - $code = $LASTEXITCODE - $PSNativeCommandUseErrorActionPreference = $prev - if ($null -eq $code) { throw "FAIL $What did not run" } - if ($code -notin $Ok) { throw "FAIL $What exited with code $code" } -} - -if (-not $env:GO_VERSION) { throw 'FAIL GO_VERSION is not set (Dockerfile ARG)' } - -$IsArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -$pwshArch = if ($IsArm64) { 'arm64' } else { 'x64' } -$goArch = if ($IsArm64) { 'arm64' } else { 'amd64' } -$gitAsset = if ($IsArm64) { 'Git-2.55.0.5-arm64.exe' } else { 'Git-2.55.0.5-64-bit.exe' } - -# pwsh and Go ship as zips and are unpacked with tar.exe: msiexec inside a -# container is the slow path (minutes per MSI on the hosted runners), while -# the zips are the same payload the official golang / powershell Windows -# images unpack. Git keeps its installer, so Git Bash stays a stock Git for -# Windows install; it runs while the zips are unpacked. -$pwshArchive = "$env:TEMP\pwsh.zip" -$pwshUrl = "https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-$pwshArch.zip" -$goArchive = "$env:TEMP\go.zip" -$goUrl = "https://go.dev/dl/go$($env:GO_VERSION).windows-$goArch.zip" -$gitInstaller = "$env:TEMP\git-installer.exe" -$gitUrl = "https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.5/$gitAsset" - -$pwshHome = 'C:\Program Files\PowerShell\7' -$goHome = 'C:\Program Files\Go' - -Invoke-Native { - & curl.exe -fsSL --retry 3 --retry-delay 5 --retry-all-errors --parallel ` - -o $pwshArchive $pwshUrl -o $goArchive $goUrl -o $gitInstaller $gitUrl -} 'the tool downloads' - -$gitSetup = Start-Process $gitInstaller -PassThru -ArgumentList '/VERYSILENT', '/NORESTART', '/SP-', '/SUPPRESSMSGBOXES' -# Touch the handle now, or ExitCode stays empty once the process is gone. -$null = $gitSetup.Handle - -New-Item -ItemType Directory -Force -Path $pwshHome | Out-Null -Invoke-Native { & tar.exe -xf $pwshArchive -C $pwshHome } 'the PowerShell unpack' -# The Go zip has a single top-level go\ directory. -Invoke-Native { & tar.exe -xf $goArchive -C 'C:\Program Files' } 'the Go unpack' -Remove-Item $pwshArchive, $goArchive - -$gitSetup.WaitForExit() -if ($gitSetup.ExitCode -ne 0) { throw "FAIL the Git installer exited with code $($gitSetup.ExitCode)" } -Remove-Item $gitInstaller - -if (-not (Test-Path "$pwshHome\pwsh.exe")) { throw 'FAIL pwsh.exe is missing after the PowerShell unpack' } -if (-not (Test-Path "$goHome\bin\go.exe")) { throw 'FAIL go.exe is missing after the Go unpack' } -if (-not (Test-Path 'C:\Program Files\Git\cmd\git.exe')) { throw 'FAIL git.exe is missing after the Git installer' } - -# The zips do not touch PATH (the MSIs did). Append after the Git installer -# finished, since it rewrites the machine PATH itself. -$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine') -[Environment]::SetEnvironmentVariable('Path', "$machinePath;$pwshHome;$goHome\bin", 'Machine') - -Invoke-Native { & "$pwshHome\pwsh.exe" -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' } 'pwsh' -Invoke-Native { & "$goHome\bin\go.exe" version } 'go version' -Invoke-Native { & 'C:\Program Files\Git\cmd\git.exe' config --system core.longpaths true } 'enable system-wide git long paths' -New-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 -PropertyType DWORD -Force | Out-Null diff --git a/tests/e2e/windows/compose.amd64.yaml b/tests/e2e/windows/compose.amd64.yaml deleted file mode 100644 index 4f03534..0000000 --- a/tests/e2e/windows/compose.amd64.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2026 Han Li and contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Host-side runner for the Windows container E2E suite on amd64 hosts -# (see docs/e2e.md). -# -# docker compose -f tests/e2e/windows/compose.amd64.yaml run --rm --build e2e -# -# The host requirement is Docker plus the Compose v5 plugin (installed once -# per machine, see docs/e2e.md); Go never touches the host, so it cannot -# conflict with a vfox-managed toolchain. The runner image is the `runner` -# stage of tests/e2e/windows/Dockerfile (stock servercore plus pwsh, Git and -# the pinned Go), built for the host arch, because there is no stock golang -# Windows image for arm64. The suite's own target image shares that tools -# stage, so its build is a cache hit. The runner drives the host daemon -# through the mounted named pipe. Ryuk is disabled -# in code (it has no Windows image); every container is terminated explicitly -# via t.Cleanup instead. -# -# Every knob below forwards only when set in the caller's env; empty means -# "use the built-in default" (see common/common.go). -# -# The module root is the working dir (single Go module for common + linux + -# windows); the command targets the windows package only, because Linux -# containers cannot run on a Windows host. - -services: - e2e: - build: - context: ../../.. - dockerfile: tests/e2e/windows/Dockerfile - target: runner - args: - BASE_IMAGE: mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-amd64 - image: vfox-nodejs/e2e-runner:1.27.1 - working_dir: C:/work/tests/e2e - volumes: - - type: npipe - source: \\.\pipe\docker_engine - target: \\.\pipe\docker_engine - - ../../..:C:/work - - e2e-go-modcache:C:/m - environment: - E2E_MAX_JOBS: ${E2E_MAX_JOBS:-} - VFOX_VERSION: ${VFOX_VERSION:-} - MIRROR: ${MIRROR:-} - NODE_VERSION: ${NODE_VERSION:-} - LTS_NODE_VERSION: ${LTS_NODE_VERSION:-} - E2E_RETRY_ATTEMPTS: ${E2E_RETRY_ATTEMPTS:-} - command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./windows"] - -volumes: - e2e-go-modcache: diff --git a/tests/e2e/windows/compose.arm64.yaml b/tests/e2e/windows/compose.arm64.yaml deleted file mode 100644 index c79286c..0000000 --- a/tests/e2e/windows/compose.arm64.yaml +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2026 Han Li and contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Host-side runner for the Windows container E2E suite on arm64 hosts -# (see docs/e2e.md). -# -# docker compose -f tests/e2e/windows/compose.arm64.yaml run --rm --build e2e -# -# The host requirement is Docker plus the Compose v5 plugin (installed once -# per machine, see docs/e2e.md); Go never touches the host, so it cannot -# conflict with a vfox-managed toolchain. The runner image is the `runner` -# stage of tests/e2e/windows/Dockerfile (stock servercore plus pwsh, Git and -# the pinned Go), built for the host arch, because there is no stock golang -# Windows image for arm64. The suite's own target image shares that tools -# stage, so its build is a cache hit. The runner drives the host daemon -# through the mounted named pipe. Ryuk is disabled -# in code (it has no Windows image); every container is terminated explicitly -# via t.Cleanup instead. -# -# Every knob below forwards only when set in the caller's env; empty means -# "use the built-in default" (see common/common.go). -# -# The module root is the working dir (single Go module for common + linux + -# windows); the command targets the windows package only, because Linux -# containers cannot run on a Windows host. - -services: - e2e: - build: - context: ../../.. - dockerfile: tests/e2e/windows/Dockerfile - target: runner - args: - BASE_IMAGE: mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-arm64 - image: vfox-nodejs/e2e-runner:1.27.1 - working_dir: C:/work/tests/e2e - volumes: - - type: npipe - source: \\.\pipe\docker_engine - target: \\.\pipe\docker_engine - - ../../..:C:/work - - e2e-go-modcache:C:/m - environment: - E2E_MAX_JOBS: ${E2E_MAX_JOBS:-} - VFOX_VERSION: ${VFOX_VERSION:-} - MIRROR: ${MIRROR:-} - NODE_VERSION: ${NODE_VERSION:-} - LTS_NODE_VERSION: ${LTS_NODE_VERSION:-} - E2E_RETRY_ATTEMPTS: ${E2E_RETRY_ATTEMPTS:-} - command: ["go", "test", "-v", "-timeout", "55m", "-run", "TestE2E", "./windows"] - -volumes: - e2e-go-modcache: diff --git a/tests/e2e/windows/e2e_test.go b/tests/e2e/windows/e2e_test.go index 582e0b9..2d61f0b 100644 --- a/tests/e2e/windows/e2e_test.go +++ b/tests/e2e/windows/e2e_test.go @@ -34,6 +34,8 @@ import ( specs "github.com/opencontainers/image-spec/specs-go/v1" tc "github.com/testcontainers/testcontainers-go" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/mount" client "github.com/moby/moby/client" "github.com/version-fox/vfox-nodejs/tests/e2e/common" @@ -42,12 +44,19 @@ import ( // TestE2E builds the image once, then runs one container per matrix combo. // Bounded parallelism via t.Parallel + semaphore (like E2E_MAX_JOBS). // -// Full matrix runs in CI; locally scope it down, e.g.: -// VFOX_VERSION=latest MIRROR=default E2E_MAX_JOBS=1 go test -v ./windows/ +// Run it through tests/e2e/windows/run.ps1, which fetches the tools, +// pulls the base image and sets E2E_TOOLS_DIR / E2E_BASE_IMAGE. Full matrix +// runs in CI; locally scope it down, e.g.: +// $env:VFOX_VERSION='latest'; $env:MIRROR='default'; pwsh -File tests/e2e/windows/run.ps1 func TestE2E(t *testing.T) { if testing.Short() { t.Skip("skip container E2E in short mode") } + toolsDir := os.Getenv("E2E_TOOLS_DIR") + baseImage := os.Getenv("E2E_BASE_IMAGE") + if toolsDir == "" || baseImage == "" { + t.Fatal("E2E_TOOLS_DIR and E2E_BASE_IMAGE are unset: run the suite via tests/e2e/windows/run.ps1") + } // Ryuk has no Windows image, so the reaper can never start here. // Every container is terminated explicitly via t.Cleanup instead. t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") @@ -58,7 +67,6 @@ func TestE2E(t *testing.T) { } platform := "windows/" + arch image := "vfox-nodejs-e2e:windows-" + arch - baseImage := "mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-" + arch here, err := filepath.Abs(".") if err != nil { @@ -84,16 +92,14 @@ func TestE2E(t *testing.T) { t.Parallel() sem <- struct{}{} defer func() { <-sem }() - runOne(t, ctx, image, platform, c) + runOne(t, ctx, image, platform, toolsDir, c) }) } } -// buildImage replicates `docker build --pull --platform $platform -// --build-arg BASE_IMAGE=$baseImage --target e2e -// -f tests/e2e/windows/Dockerfile -t $image $repoRoot`. Under the compose -// runner the shared tools stage is already built, so only the plugin COPY -// layers run here. +// buildImage replicates `docker build --platform $platform +// --build-arg BASE_IMAGE=$baseImage -f tests/e2e/windows/Dockerfile +// -t $image $repoRoot`. run.ps1 has already pulled the base image. func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, baseImage string) { t.Helper() @@ -119,8 +125,6 @@ func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, ba BuildLogWriter: os.Stdout, BuildOptionsModifier: func(opts *client.ImageBuildOptions) { opts.Platforms = []specs.Platform{pf} - opts.PullParent = true - opts.Target = "e2e" }, }, }) @@ -136,8 +140,9 @@ func buildImage(t *testing.T, ctx context.Context, repoRoot, image, platform, ba // E2E flow (setup -> install -> verify) through pwsh exec calls, asserting // in Go. A combo passes when every phase exits 0 and the outputs carry the // expected markers. Every combo runs against a VFOX_HOME containing a space -// (see spacedVfoxHome). -func runOne(t *testing.T, ctx context.Context, image, platform string, c common.Combo) { +// (see spacedVfoxHome). The host tools dir (see run.ps1) is mounted +// read-only at toolsMount. +func runOne(t *testing.T, ctx context.Context, image, platform, toolsDir string, c common.Combo) { t.Helper() prefix := c.Prefix(platform) @@ -170,6 +175,14 @@ func runOne(t *testing.T, ctx context.Context, image, platform string, c common. ImagePlatform: platform, Name: common.ContainerName(c.Slug()), Env: env, + HostConfigModifier: func(hc *container.HostConfig) { + hc.Mounts = append(hc.Mounts, mount.Mount{ + Type: mount.TypeBind, + Source: toolsDir, + Target: toolsMount, + ReadOnly: true, + }) + }, }) if err != nil { t.Fatalf("[%s] start container: %v", prefix, err) diff --git a/tests/e2e/windows/exec.go b/tests/e2e/windows/exec.go index 4fd1fd0..6e3c6c4 100644 --- a/tests/e2e/windows/exec.go +++ b/tests/e2e/windows/exec.go @@ -57,7 +57,7 @@ func execRun(ctx context.Context, t *testing.T, ctr tc.Container, script string, func execGitBashRun(ctx context.Context, t *testing.T, ctr tc.Container, script string, extraEnv ...string) (int, string) { t.Helper() command := fmt.Sprintf("& %s --login -c %s", - pwshQuote(`C:\Program Files\Git\bin\bash.exe`), pwshQuote(script)) + pwshQuote(toolsMount+`\git\bin\bash.exe`), pwshQuote(script)) return execRun(ctx, t, ctr, command, extraEnv...) } diff --git a/tests/e2e/windows/run.ps1 b/tests/e2e/windows/run.ps1 new file mode 100644 index 0000000..d9dd50f --- /dev/null +++ b/tests/e2e/windows/run.ps1 @@ -0,0 +1,183 @@ +<# + Copyright 2026 Han Li and contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +#> + +# Host-side runner for the Windows container E2E suite (see docs/e2e.md): +# +# pwsh -NoProfile -File tests/e2e/windows/run.ps1 +# +# $env:E2E_RUN narrows the -test.run pattern (default TestE2E), e.g. +# 'TestE2E/latest-default'; the other knobs are the env vars in docs/e2e.md. +# +# The host needs only Docker in Windows-container mode: curl.exe and tar.exe +# ship with Windows. Everything else is fetched into a per-arch cache +# ($env:E2E_CACHE_DIR, default %LOCALAPPDATA%\vfox-nodejs-e2e\) and +# never installed or put on the user PATH, so it cannot conflict with a +# vfox-managed toolchain: +# +# tools\pwsh PowerShell, unpacked from its zip +# tools\git PortableGit (Git for Windows, incl. Git Bash) +# tools\go the pinned Go, which compiles the suite and vfox@main +# tools\vfox-main vfox.exe built from main for this run +# +# tools\ is bind-mounted read-only at C:\tools into every combo container, +# so the image only adds the plugin files. Unpacking the tools inside an +# image layer instead costs minutes of layer commit on the hosted runners. +# The base image pull (the slowest step) runs in the background while the +# tools are fetched and the suite and vfox@main are compiled. + +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +# Local copy of the native-command guard. Fails unless the command exits 0. +function Invoke-Native { + param( + [Parameter(Mandatory)] [scriptblock] $Cmd, + [Parameter(Mandatory)] [string] $What, + [int[]] $Ok = 0 + ) + $prev = $PSNativeCommandUseErrorActionPreference + $PSNativeCommandUseErrorActionPreference = $false + & $Cmd + $code = $LASTEXITCODE + $PSNativeCommandUseErrorActionPreference = $prev + if ($null -eq $code) { throw "FAIL $What did not run" } + if ($code -notin $Ok) { throw "FAIL $What exited with code $code" } +} + +$PwshVersion = '7.6.6' +$GitVersion = '2.55.0.5' +$GitTag = 'v2.55.0.windows.5' +$GoVersion = '1.27.1' + +$IsArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' +$arch = if ($IsArm64) { 'arm64' } else { 'amd64' } +$pwshArch = if ($IsArm64) { 'arm64' } else { 'x64' } +$gitArch = if ($IsArm64) { 'arm64' } else { '64-bit' } +$baseImage = "mcr.microsoft.com/windows/servercore:ltsc2025-KB5122871-$arch" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$cacheRoot = if ($env:E2E_CACHE_DIR) { $env:E2E_CACHE_DIR } else { Join-Path $env:LOCALAPPDATA "vfox-nodejs-e2e\$arch" } +$toolsRoot = Join-Path $cacheRoot 'tools' +$downloads = Join-Path $cacheRoot 'downloads' +New-Item -ItemType Directory -Force -Path $toolsRoot, $downloads | Out-Null + +Write-Output "Pulling $baseImage in the background ..." +$pull = Start-Process docker -ArgumentList 'pull', '--quiet', $baseImage -NoNewWindow -PassThru +# Touch the handle now, or ExitCode stays empty once the process is gone. +$null = $pull.Handle + +# Each tool lives in tools\ with a .version marker; a missing or stale +# marker refetches it. +$tools = @( + @{ Name = 'pwsh'; Version = $PwshVersion; File = 'pwsh.zip' + Url = "https://github.com/PowerShell/PowerShell/releases/download/v$PwshVersion/PowerShell-$PwshVersion-win-$pwshArch.zip" } + @{ Name = 'git'; Version = $GitVersion; File = 'PortableGit.7z.exe' + Url = "https://github.com/git-for-windows/git/releases/download/$GitTag/PortableGit-$GitVersion-$gitArch.7z.exe" } + @{ Name = 'go'; Version = $GoVersion; File = 'go.zip' + Url = "https://go.dev/dl/go$GoVersion.windows-$arch.zip" } +) +$stale = @($tools | Where-Object { + $marker = Join-Path $toolsRoot "$($_.Name)\.version" + -not ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $_.Version)) + }) +if ($stale.Count -gt 0) { + $curlArgs = @('-fsSL', '--retry', '3', '--retry-delay', '5', '--retry-all-errors', '--parallel') + foreach ($tool in $stale) { $curlArgs += @('-o', (Join-Path $downloads $tool.File), $tool.Url) } + Invoke-Native { & curl.exe @curlArgs } 'the tool downloads' + foreach ($tool in $stale) { + $dir = Join-Path $toolsRoot $tool.Name + $archive = Join-Path $downloads $tool.File + if (Test-Path $dir) { Remove-Item -Recurse -Force $dir } + switch ($tool.Name) { + 'pwsh' { + New-Item -ItemType Directory -Force -Path $dir | Out-Null + Invoke-Native { & tar.exe -xf $archive -C $dir } 'the PowerShell unpack' + } + 'go' { + # The Go zip has a single top-level go\ directory. + Invoke-Native { & tar.exe -xf $archive -C $toolsRoot } 'the Go unpack' + } + 'git' { + $sfx = Start-Process $archive -ArgumentList '-y', "-o`"$dir`"" -Wait -PassThru + if ($sfx.ExitCode -ne 0) { throw "FAIL the PortableGit unpack exited with code $($sfx.ExitCode)" } + # The mount is read-only in the containers, so the first-run + # setup must finish here (the script deletes itself when done). + $postInstall = Join-Path $dir 'post-install.bat' + if (Test-Path $postInstall) { + Start-Process (Join-Path $dir 'git-bash.exe') -WorkingDirectory $dir -Wait ` + -ArgumentList '--no-needs-console', '--hide', '--no-cd', '--command=post-install.bat' + $deadline = (Get-Date).AddMinutes(2) + while ((Test-Path $postInstall) -and ((Get-Date) -lt $deadline)) { Start-Sleep -Seconds 1 } + if (Test-Path $postInstall) { throw 'FAIL the PortableGit post-install did not finish' } + } + Invoke-Native { & (Join-Path $dir 'cmd\git.exe') config --system core.longpaths true } 'enable system-wide git long paths' + } + } + Remove-Item $archive + Set-Content -Path (Join-Path $dir '.version') -Value $tool.Version + Write-Output "PASS fetched $($tool.Name) $($tool.Version)" + } +} +else { + Write-Output "PASS tools cached at $toolsRoot" +} + +$git = Join-Path $toolsRoot 'git\cmd\git.exe' +$go = Join-Path $toolsRoot 'go\bin\go.exe' +# Process-scoped only: the cached Go and its caches never touch the user env. +$env:GOTOOLCHAIN = 'local' +$env:CGO_ENABLED = '0' +$env:GOPATH = Join-Path $cacheRoot 'gopath' +$env:GOMODCACHE = Join-Path $cacheRoot 'gomod' +$env:GOCACHE = Join-Path $cacheRoot 'gocache' +Remove-Item Env:GOROOT -ErrorAction SilentlyContinue + +# vfox@main is built here, once, instead of inside every main combo. +$vfoxMain = Join-Path $toolsRoot 'vfox-main' +if (Test-Path $vfoxMain) { Remove-Item -Recurse -Force $vfoxMain } +if (-not $env:VFOX_VERSION -or ($env:VFOX_VERSION -split '\s+') -contains 'main') { + $src = Join-Path $cacheRoot 'vfox-src' + if (Test-Path $src) { Remove-Item -Recurse -Force $src } + Invoke-Native { & $git clone --quiet --depth 1 https://github.com/version-fox/vfox $src } 'the vfox clone' + Invoke-Native { & $go -C $src build -trimpath -o (Join-Path $vfoxMain 'vfox.exe') . } 'the vfox@main build' + $commit = (& $git -C $src rev-parse --short HEAD).Trim() + Write-Output "PASS built vfox@main ($commit) with Go $GoVersion ($arch)" +} + +$testBin = Join-Path $cacheRoot 'windows.test.exe' +Push-Location (Join-Path $repoRoot 'tests\e2e') +try { + Invoke-Native { & $go test -c -o $testBin ./windows } 'the suite build' +} +finally { + Pop-Location +} + +$pull.WaitForExit() +if ($pull.ExitCode -ne 0) { throw "FAIL docker pull $baseImage exited with code $($pull.ExitCode)" } +Write-Output "PASS pulled $baseImage" + +$env:E2E_TOOLS_DIR = $toolsRoot +$env:E2E_BASE_IMAGE = $baseImage +$runPattern = if ($env:E2E_RUN) { $env:E2E_RUN } else { 'TestE2E' } +# go test runs a test binary from its package directory; do the same. +Push-Location $PSScriptRoot +try { + Invoke-Native { & $testBin -test.v -test.timeout 55m -test.run $runPattern } 'the Windows E2E suite' +} +finally { + Pop-Location +} diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index 4c78810..d64bd60 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -29,10 +29,9 @@ import ( // Node.js defaults live in the common package; only the Windows-specific // toolchain pin stays here. const ( - // goWindowsVersion pins the Go toolchain that builds vfox@main inside - // the container. The image installs it (GO_VERSION in - // tests/e2e/windows/Dockerfile); installVfoxMain asserts both agree. - goWindowsVersion = "1.27.1" + // toolsMount is where every combo container sees the host tools dir + // that run.ps1 prepares (pwsh, git, go, vfox-main), read-only. + toolsMount = `C:\tools` // spacedVfoxHome is the default vfox home of a Windows account whose // name contains a space, the layout reported in @@ -159,39 +158,26 @@ func installVfoxRelease(ctx context.Context, t *testing.T, ctr tc.Container, box t.Logf("PASS installed vfox %s (%s)", tag, vfoxArch) } -// installVfoxMain clones vfox and builds vfox.exe with the Go toolchain -// baked into the image (bootstrap-tools.ps1), after checking it is the -// pinned version. -func installVfoxMain(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, procArch string) { +// installVfoxMain stages the vfox.exe that run.ps1 built from main on the +// host (once per run, with the pinned Go) from the read-only tools mount. +func installVfoxMain(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox) { t.Helper() - goArch := "amd64" - if procArch == "ARM64" { - goArch = "arm64" - } - goVersion := execOK(ctx, t, ctr, `& 'C:\Program Files\Go\bin\go.exe' version`, box.vars...) - want := fmt.Sprintf("go%s windows/%s", goWindowsVersion, goArch) - require.Containsf(t, goVersion, want, - "image Go differs from goWindowsVersion; keep GO_VERSION in tests/e2e/windows/Dockerfile in sync") - - src := box.workDir + `\vfox-src` - execOK(ctx, t, ctr, - fmt.Sprintf("git clone --depth 1 https://github.com/version-fox/vfox %s", pwshQuote(src)), - box.vars...) + built := toolsMount + `\vfox-main\vfox.exe` execOK(ctx, t, ctr, fmt.Sprintf( - `& 'C:\Program Files\Go\bin\go.exe' -C %s build -trimpath -o %s .`, - pwshQuote(src), pwshQuote(box.vfoxExe)), box.vars...) - t.Logf("PASS built vfox from main with Go %s (%s)", goWindowsVersion, goArch) + "if (-not (Test-Path %s)) { throw 'FAIL no vfox@main build in the tools mount (run the suite via run.ps1)' }; "+ + "Copy-Item -Path %s -Destination %s", + pwshQuote(built), pwshQuote(built), pwshQuote(box.vfoxExe)), box.vars...) + t.Logf("PASS staged vfox built from main: %s", strings.TrimSpace(execOK(ctx, t, ctr, "vfox --version", box.vars...))) } // setupVfox installs vfox (release, or builds main) and registers the local // nodejs plugin. func setupVfox(ctx context.Context, t *testing.T, ctr tc.Container, box slotBox, vfoxVersion string) { t.Helper() - procArch := procArchOfContainer(ctx, t, ctr) if vfoxVersion == "main" { - installVfoxMain(ctx, t, ctr, box, procArch) + installVfoxMain(ctx, t, ctr, box) } else { - installVfoxRelease(ctx, t, ctr, box, procArch) + installVfoxRelease(ctx, t, ctr, box, procArchOfContainer(ctx, t, ctr)) } execOK(ctx, t, ctr, fmt.Sprintf( "tar -a -cf %s -C 'C:\\e2e' metadata.lua hooks lib", diff --git a/tests/e2e/wsl/setup.go b/tests/e2e/wsl/setup.go index 223a303..1539677 100644 --- a/tests/e2e/wsl/setup.go +++ b/tests/e2e/wsl/setup.go @@ -36,7 +36,7 @@ const ( // defaultNodeVersion reproduces the issue report. defaultNodeVersion = "20.16.0" // goWslVersion pins the Go toolchain tarball used to build vfox@main - // inside WSL (matches goWindowsVersion in tests/e2e/windows/setup.go). + // inside WSL (matches $GoVersion in tests/e2e/windows/run.ps1). goWslVersion = "1.27.1" ) From d8d77b1d2ad55960430db579869407abdedf1ed2 Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sat, 17 Oct 2026 21:18:14 +0800 Subject: [PATCH 3/4] test(e2e): quote the test binary flags in run.ps1 PowerShell splits a bare -test.v into -test and .v, so the suite exited with 'flag provided but not defined: -test'. --- tests/e2e/windows/run.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/windows/run.ps1 b/tests/e2e/windows/run.ps1 index d9dd50f..d78758f 100644 --- a/tests/e2e/windows/run.ps1 +++ b/tests/e2e/windows/run.ps1 @@ -176,7 +176,8 @@ $runPattern = if ($env:E2E_RUN) { $env:E2E_RUN } else { 'TestE2E' } # go test runs a test binary from its package directory; do the same. Push-Location $PSScriptRoot try { - Invoke-Native { & $testBin -test.v -test.timeout 55m -test.run $runPattern } 'the Windows E2E suite' + # Quoted: PowerShell splits a bare -test.v into -test and .v. + Invoke-Native { & $testBin '-test.v' '-test.timeout=55m' "-test.run=$runPattern" } 'the Windows E2E suite' } finally { Pop-Location From 1b180267e53bc4eeb892d6121ef961224651f8cd Mon Sep 17 00:00:00 2001 From: Qiheng He Date: Sat, 17 Oct 2026 21:50:38 +0800 Subject: [PATCH 4/4] test(e2e): cache the windows/arm64 docker engine build windows-11-arm spends about four minutes installing Go and building moby before the suite can pull anything. Keep the binaries in C:\vfox-docker\bin, restore that directory with actions/cache keyed on install-docker-arm64.ps1, and skip setup-go and the build on a hit. --- .github/workflows/e2e.yml | 12 +++++- docs/e2e.md | 3 ++ tests/e2e/windows/install-docker-arm64.ps1 | 48 ++++++++++++++-------- 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e333688..ba79252 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -104,9 +104,17 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - # The windows-11-arm runner ships neither Docker for Windows containers - # nor a Go new enough to build moby, so both are provisioned here. + # The windows-11-arm runner ships no Docker for Windows containers, so + # the engine is built from source (no arm64 binaries are published). + # The build is cached on the script that pins it; a hit skips both the + # Go install and the build. - if: matrix.runs-on == 'windows-11-arm' + id: docker-arm64 + uses: actions/cache@v6 + with: + path: C:\vfox-docker\bin + key: docker-windows-arm64-${{ hashFiles('tests/e2e/windows/install-docker-arm64.ps1') }} + - if: matrix.runs-on == 'windows-11-arm' && steps.docker-arm64.outputs.cache-hit != 'true' uses: actions/setup-go@v6 with: go-version: 1.27.1 diff --git a/docs/e2e.md b/docs/e2e.md index 0216375..7118928 100644 --- a/docs/e2e.md +++ b/docs/e2e.md @@ -246,6 +246,9 @@ pwsh -NoProfile -File .\tests\e2e\windows\run.ps1 On arm64, a host Go is needed only to build the engine (`install-docker-arm64.ps1`); `run.ps1` uses its own cached Go for the suite. +The script reuses an earlier build left in `C:\vfox-docker\bin`; CI restores +that directory with `actions/cache`, keyed on the script, so a cache hit +skips both `actions/setup-go` and the build. `$env:E2E_RUN` narrows the `-test.run` pattern (default `TestE2E`), e.g. `TestE2E/latest-default`. Scope the run down with diff --git a/tests/e2e/windows/install-docker-arm64.ps1 b/tests/e2e/windows/install-docker-arm64.ps1 index 6523c35..7495812 100644 --- a/tests/e2e/windows/install-docker-arm64.ps1 +++ b/tests/e2e/windows/install-docker-arm64.ps1 @@ -58,28 +58,40 @@ if (Test-Engine) { else { $mobyVersion = '29.8.1' $root = 'C:\vfox-docker' + # The built binaries land in $bin, which CI restores through + # actions/cache (keyed on this script): a hit skips the Go install and + # the build, which take about four minutes on windows-11-arm. + $bin = Join-Path $root 'bin' New-Item -ItemType Directory -Force -Path $root | Out-Null - Write-Output 'Building docker from source for windows/arm64 ...' - if (-not (Get-Command go -ErrorAction SilentlyContinue)) { throw 'FAIL go is not on the PATH' } - $env:GOPATH = Join-Path $root 'gopath' - - $moby = Join-Path $root 'moby' - Invoke-Native { git clone --depth 1 --branch "docker-v$mobyVersion" https://github.com/moby/moby.git $moby } 'the moby clone' - Invoke-Native { go install github.com/tc-hib/go-winres@v0.3.1 } 'the go-winres install' - $env:DOCKERCLI_VERSION = $mobyVersion - $env:VERSION = $mobyVersion - Push-Location $moby - try { - & .\hack\make.ps1 -Client -Daemon - } - finally { - Pop-Location + if ((Test-Path (Join-Path $bin 'dockerd.exe')) -and (Test-Path (Join-Path $bin 'docker.exe'))) { + Write-Output "PASS reusing the cached docker $mobyVersion build in $bin" } + else { + Write-Output 'Building docker from source for windows/arm64 ...' + + if (-not (Get-Command go -ErrorAction SilentlyContinue)) { throw 'FAIL go is not on the PATH' } + $env:GOPATH = Join-Path $root 'gopath' - $bin = Join-Path $moby 'bundles' - if (-not (Test-Path (Join-Path $bin 'dockerd.exe'))) { throw 'FAIL the moby build produced no dockerd.exe' } - if (-not (Test-Path (Join-Path $bin 'docker.exe'))) { throw 'FAIL the moby build produced no docker.exe' } + $moby = Join-Path $root 'moby' + Invoke-Native { git clone --depth 1 --branch "docker-v$mobyVersion" https://github.com/moby/moby.git $moby } 'the moby clone' + Invoke-Native { go install github.com/tc-hib/go-winres@v0.3.1 } 'the go-winres install' + $env:DOCKERCLI_VERSION = $mobyVersion + $env:VERSION = $mobyVersion + Push-Location $moby + try { + & .\hack\make.ps1 -Client -Daemon + } + finally { + Pop-Location + } + + $bundles = Join-Path $moby 'bundles' + if (-not (Test-Path (Join-Path $bundles 'dockerd.exe'))) { throw 'FAIL the moby build produced no dockerd.exe' } + if (-not (Test-Path (Join-Path $bundles 'docker.exe'))) { throw 'FAIL the moby build produced no docker.exe' } + New-Item -ItemType Directory -Force -Path $bin | Out-Null + Copy-Item -Path (Join-Path $bundles 'dockerd.exe'), (Join-Path $bundles 'docker.exe') -Destination $bin + } $env:PATH = "$bin;$env:PATH" if ($env:GITHUB_PATH) { Add-Content -Path $env:GITHUB_PATH -Value $bin }