diff --git a/.dockerignore b/.dockerignore index 5e65fbb..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 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 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 302b23c..ba79252 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 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,33 +93,35 @@ 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: + # 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: 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 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/.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..7118928 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,12 +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. 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 -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 @@ -203,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 @@ -233,20 +241,21 @@ 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. Scope the run down with -`$env:VFOX_VERSION = 'latest'; $env:MIRROR = 'default'` in front of the -compose command, as on the other platforms. +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 +`$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 1452dc3..879527d 100644 --- a/tests/e2e/windows/Dockerfile +++ b/tests/e2e/windows/Dockerfile @@ -12,13 +12,23 @@ # 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. + +# 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} SHELL ["powershell", "-NoProfile", "-Command"] -COPY tests/e2e/windows/bootstrap-tools.ps1 C:\bootstrap-tools.ps1 -RUN powershell -NoProfile -ExecutionPolicy Bypass -File 'C:\bootstrap-tools.ps1' +# 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 @@ -28,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/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 deleted file mode 100644 index 7d04931..0000000 --- a/tests/e2e/windows/bootstrap-tools.ps1 +++ /dev/null @@ -1,53 +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" } -} - -$IsArm64 = $env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -$pwshArch = if ($IsArm64) { 'arm64' } else { 'x64' } -$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" -$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' -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' } -if (-not (Test-Path 'C:\Program Files\Git\cmd\git.exe')) { throw 'FAIL git.exe is missing after the Git installer' } -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 e9ac888..0000000 --- a/tests/e2e/windows/compose.amd64.yaml +++ /dev/null @@ -1,61 +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 -# (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 -# 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-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 f771f67..0000000 --- a/tests/e2e/windows/compose.arm64.yaml +++ /dev/null @@ -1,61 +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 -# (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 -# 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-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 6b17837..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,14 +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 +// buildImage replicates `docker build --platform $platform // --build-arg BASE_IMAGE=$baseImage -f tests/e2e/windows/Dockerfile -// -t $image $repoRoot`. +// -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() @@ -117,7 +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 }, }, }) @@ -133,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) @@ -167,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/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 } diff --git a/tests/e2e/windows/run.ps1 b/tests/e2e/windows/run.ps1 new file mode 100644 index 0000000..d78758f --- /dev/null +++ b/tests/e2e/windows/run.ps1 @@ -0,0 +1,184 @@ +<# + 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 { + # 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 +} diff --git a/tests/e2e/windows/setup.go b/tests/e2e/windows/setup.go index dfe5c3f..d64bd60 100644 --- a/tests/e2e/windows/setup.go +++ b/tests/e2e/windows/setup.go @@ -29,9 +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 = "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 @@ -158,49 +158,26 @@ 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). -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" - } - 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...) - - 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" )