Deep Review: 20260728-180750-pr-5342

Date2026-07-28 18:07
Repolima-vm/lima
Round3 (of PR 5342)
Author@jandubois
PR#5342 — CI: factor Windows setup into composite actions; add plain-Windows jobs
Branchci-plain-windows-jobs
Commits244c0ca0 CI: factor Windows setup into composite actions; add plain-Windows jobs
Review SHA244c0ca03969f4729469612d6af5f5d202241d4c
ReviewersClaude Opus 5 (effort: xhigh), Codex GPT 5.6 Sol (effort: xhigh), Gemini 3.1 Pro (effort: default), Gemini 3.5 Flash (effort: default)
VerdictMerge as-is — no Critical or Important findings; merge after #5299, per the PR's own ordering note
Wall-clock time32 min 0 s


Executive Summary

Round 3 closes every round-2 finding, and the delta holds up under checking. Both new sshutil tests ran and passed on windows-2025 at this SHA, the verification round 2 could not run. Four suggestions remain, none blocking. Round 3 is where this PR converged, so the suggestions below are optional.

Four reviewers ran against 244c0ca0. Codex found the one hole in the plain-host verification layers; Claude found the other three suggestions; both Gemini models returned near-clean. During consolidation I fetched this SHA's own CI job logs, reproduced the registry regex miss, read Git for Windows' installer source to settle it, counted the template symlinks in the tree, and compile-checked the Windows test file. The two plain jobs still fail at limactl copy, exactly as the PR description predicts.


Critical Issues

None.


Important Issues

None.


Suggestions

S1. Layer 4 cannot see Git for Windows Codex GPT 5.6 Sol
      foreach ($root in $uninstallRoots) {
        if (-not (Test-Path $root)) { continue }
        $keys = Get-ChildItem $root -ErrorAction SilentlyContinue
        foreach ($k in $keys) {
          $props = Get-ItemProperty $k.PSPath -ErrorAction SilentlyContinue
          if ($props -and $props.DisplayName -and
              ($props.DisplayName -match '(?i)(msys|git for windows|cygwin|mingw)')) {
            $fail += ("registry: {0} -> DisplayName='{1}'" -f $k.PSPath, $props.DisplayName)
          }
        }
      }

Git for Windows registers its uninstall entry with DisplayName set to the bare string Git. Its installer defines #define APP_NAME 'Git' and UninstallDisplayName={#APP_NAME}, and no [Registry] section overrides that. I ran the pattern against both strings: Git does not match, MSYS2 64bit does. So the layer added to catch a stale registration cannot catch the one product this action uninstalls by name.

The installer's own QueryUninstallValues probes four roots for Microsoft\Windows\CurrentVersion\Uninstall\Git_is1; action.yml:148-151 lists three, omitting the HKCU WOW6432Node one.

      }
      $sh = Get-Command sh -ErrorAction SilentlyContinue
      if ($sh) { $fail += "binary: sh -> $($sh.Source)" }

      # --- Layer 4: registry uninstall keys ---
      $uninstallRoots = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
        'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
      )
      foreach ($root in $uninstallRoots) {
        if (-not (Test-Path $root)) { continue }
        $keys = Get-ChildItem $root -ErrorAction SilentlyContinue
        foreach ($k in $keys) {

Impact stays low. Layers 1 and 3 already fail on a leftover C:\Program Files\Git or a git on PATH, so Layer 4 is the backstop.

Fix:

         'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
+        'HKCU:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
         'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
-              ($props.DisplayName -match '(?i)(msys|git for windows|cygwin|mingw)')) {
+              ($props.DisplayName -match '(?i)(msys|^git$|git for windows|cygwin|mingw)')) {
S2. The MSYS2 jobs assert nothing about the Cygwin ssh they install Claude Opus 5
    run: | # zizmor: ignore[github-env]
      $ErrorActionPreference = 'Stop'
      Add-Content -Path $env:GITHUB_PATH -Value 'C:\msys64\usr\bin'
      # rsync: without it the copy test skips its rsync backend, so the Windows
      # path conversion that backend performs goes unexercised.
      # openssh: keeps a Cygwin ssh ahead of %SystemRoot%\System32\OpenSSH, so
      # these jobs cover the Cygwin toolchain and the plain jobs the native one.
      & 'C:\msys64\usr\bin\pacman.exe' -Sy --noconfirm openbsd-netcat diffutils openssh rsync socat w3m
      if ($LASTEXITCODE -ne 0) { throw "pacman failed: $LASTEXITCODE" }

This comment states a two-sided coverage split, and only the native side is enforced: windows_plain_host/action.yml:172-175 fails the job when any of the four native binaries is missing.

      # file not found`; missing scp.exe or ssh-keygen.exe makes Lima fall
      # back to a bare ssh lookup and fail later elsewhere; missing
      # sftp-server.exe drops reverse-sshfs to the builtin driver, so the job
      # stops covering what it was added to cover.
      $opensshDir = Join-Path $env:SystemRoot 'System32\OpenSSH'
      foreach ($exe in 'ssh.exe', 'scp.exe', 'ssh-keygen.exe', 'sftp-server.exe') {
        $exePath = Join-Path $opensshDir $exe
        if (-not (Test-Path $exePath)) { $fail += "missing: $exePath" }
      }

      if ($fail.Count -gt 0) {
        Write-Host "Plain-Windows verification FAILED:" -ForegroundColor Red
        $fail | ForEach-Object { Write-Host "  $_" }
        Write-Error "Runner is not in the expected plain-Windows state; see the list above."

pickCompleteSSHOnWindows skips a PATH directory that lacks scp.exe or ssh-keygen.exe (pkg/sshutil/sshutil.go:95), then falls back to %SystemRoot%\System32\OpenSSH. So if MSYS2 ever splits its openssh package, C:\msys64\usr\bin stops qualifying, Lima selects the native ssh, and Windows tests (WSL2) stays green while covering the toolchain the plain jobs already own. The integration step runs limactl without --debug, so no log line names the binary it picked.

		}
		sshPath := filepath.Join(dir, "ssh.exe")
		if _, err := os.Stat(sshPath); err != nil {
			continue
		}
		if missing := missingSiblings(dir, "scp.exe", "ssh-keygen.exe"); len(missing) > 0 {
			logrus.Debugf("skipping ssh at %#q: missing %v in same directory (likely MinGit or another partial install)", sshPath, missing)
			continue
		}
		return sshPath
	}

Fix: mirror Layer 5 after the pacman call, asserting ssh.exe, scp.exe and ssh-keygen.exe under C:\msys64\usr\bin.

S3. Add QEMU to PATH re-bakes the whole PATH through $GITHUB_ENV Claude Opus 5
    # install dir here, extending the scrubbed PATH= that windows_plain_host
    # published through $GITHUB_ENV.
    - name: Add QEMU to PATH
      shell: pwsh
      run: |
        Add-Content -Path $env:GITHUB_ENV -Value "PATH=C:\Program Files\QEMU;$env:PATH"
    - uses: ./.github/actions/windows_plain_build
    - uses: ./.github/actions/windows_plain_smoke_test
      with:
        template: .\templates\default.yaml
        lima-home-suffix: plain-qemu

$env:PATH in this step already carries the $GITHUB_PATH entries setup-go registered, and the runner prepends that list again on top of whatever $GITHUB_ENV sets. Later steps therefore see each of those entries twice. The sibling action does the same job in one line through $GITHUB_PATH (windows_msys2_prep/action.yml:18).

    # The value written to $GITHUB_PATH is the static literal C:\msys64\usr\bin,
    # which is the canonical pattern for adding a directory to subsequent
    # steps' PATH. No PR-controlled data reaches the env file.
    run: | # zizmor: ignore[github-env]
      $ErrorActionPreference = 'Stop'
      Add-Content -Path $env:GITHUB_PATH -Value 'C:\msys64\usr\bin'
      # rsync: without it the copy test skips its rsync backend, so the Windows
      # path conversion that backend performs goes unexercised.
      # openssh: keeps a Cygwin ssh ahead of %SystemRoot%\System32\OpenSSH, so
      # these jobs cover the Cygwin toolchain and the plain jobs the native one.
      & 'C:\msys64\usr\bin\pacman.exe' -Sy --noconfirm openbsd-netcat diffutils openssh rsync socat w3m

I could not measure the duplication. The PATH: dumps in job 90453290927 show the $GITHUB_ENV value rather than the effective PATH, and they list setup-go's directory once. The cost is a repeated entry, with no functional effect either way, so this is a consistency fix.

Fix, which also lets the three-line comment above it shrink:

-        Add-Content -Path $env:GITHUB_ENV -Value "PATH=C:\Program Files\QEMU;$env:PATH"
+        Add-Content -Path $env:GITHUB_PATH -Value 'C:\Program Files\QEMU'
S4. The migration advice names no Windows recipe Claude Opus 5
title: Breaking changes
weight: 20
---

## v2.3.0
- The experimental `_LIMA_WINDOWS_EXTRA_PATH` environment variable was removed. It prepended directories to
  `limactl.exe`'s own PATH on Windows hosts, leaving the calling shell's PATH untouched. Put them on `PATH`
  instead, or set `PATH` for the single call if you need to keep them out of your shell.

## v2.2.0
- The default [`socket_vmnet` group](../config/network/vmnet.md) in `networks.yaml` was changed from `everyone` to
  `admin`. A non-admin user must set `group` to a group they belong to (e.g. `staff`).
- The default VM driver on Windows hosts was changed from `wsl2` to `qemu`,

Neither Windows shell scopes a PATH assignment to one command. In PowerShell $env:PATH = ... is process-wide and outlives the call; cmd.exe's set behaves the same. A reader following the last clause literally changes the session PATH, which is the outcome the same sentence offers to avoid. The deleted docs sold the variable on exactly this property.

Fix: name a child process, the only form that scopes the change.

-  instead, or set `PATH` for the single call if you need to keep them out of your shell.
+  instead, or run limactl in a child shell to keep them out of your own:
+  `pwsh -Command "$env:PATH = 'C:\...;' + $env:PATH; limactl start"`.

Design Observations

Concerns

Strengths


Testing Assessment

Ranked by risk:

  1. Which ssh.exe each job selects is asserted nowhere. S2 covers the MSYS2 half. The plain half is acknowledged in the new test's own doc comment. The Go-level selection rules are now well covered; the CI-level configuration is not.
  2. Both plain jobs still fail at limactl copy pending #5299, which the PR description predicts and round 1 declined. At this SHA both are completed/failure while every other Windows job passes.
  3. TestNewSSHExeFallsBackToPATH carries one inert assertion Claude Opus 5. got.Exe equals sshExe whether NewSSHExe fell through to exec.LookPath or pickCompleteSSHOnWindows wrongly returned the partial install, since both read the same PATH. The preceding pickCompleteSSHOnWindows() == "" assertion is what pins the behaviour, and it does so correctly, so the test is sound.
  4. windows_plain_templates' guards and the $null guards remain unexercised, both declined in prior rounds.

The round's largest gain closes a round-2 limitation. Round 2 could not run sshutil_windows_test.go at all, because the file builds only on Windows. At this SHA job 90453290796 step 7 (go test -v ./...) reports completed/success, and its log shows --- PASS: TestPickCompleteSSHOnWindows/incomplete_native_install_disqualifies_it_too and --- PASS: TestNewSSHExeFallsBackToPATH at lines 6048 and 6050. Neither test carries a skip that fires on windows-2025.

Executed during consolidation, all at the review SHA: GOOS=windows go vet ./pkg/sshutil and GOOS=windows go test -c ./pkg/sshutil, both clean; go build ./cmd/limactl, clean. I fetched the raw job logs for 90453290796 and 90453290927, read per-step conclusions through the jobs API while the run was still going, reproduced the Layer 4 regex against Git and MSYS2 64bit, downloaded Git for Windows' install.iss, counted the template symlinks at both review SHAs, and read the pinned sshocker@v0.3.11 source. git status in all four agent worktrees showed only the supplied .scratch/, plus a sshutil.test.exe in Flash's, which corroborates its cross-compile claim.


Documentation Assessment

All three description additions from this round hold up. windows_plain_build/action.yml:9-11 claims the unversioned build passes default.yaml's minimumLimaVersion, and the chain checks out end to end: pkg/version/version.go:7 defaults Version to "<unknown>", versionutil.compare returns 1 when Parse fails on it, GreaterEqual at pkg/limayaml/validate.go:46 therefore passes, and templates/default.yaml:340 is the only one of the two plain-job templates carrying the key at all. windows_msys2_prep's new sentence about the Make step matches Makefile:223. Layer 5's rewritten comment is accurate on all three arms, which I traced through pickCompleteSSHOnWindows and NewSSHExe.

The narrowed windows-plain-qemu header is the most improved of the four. Its claim that sshocker treats the failed mount confirmation as non-fatal is exactly what reversesshfs.go:252-255 does: a // not a fatal error comment, a warning, then return nil.

breaking.md's new ## v2.3.0 heading is placed correctly. v2.2.0 is the newest tag, no v2.3.0 tag exists, and the heading sits above ## v2.2.0 in the file's newest-first order. The wording is S4. The removal from environment-variables.md leaves no dangling reference anywhere in the tree.


Commit Structure

One commit, DCO-signed, amending the round-2 SHA rather than stacking a fixup. The message still matches the tree, and name: "Windows tests (WSL2)" remains byte-identical to the merge-base, so the job-id rename leaves required status checks alone. Nothing to squash or split.


Acknowledged Limitations

Declined in prior rounds


Agent Performance Retro

Claude Opus 5

Produced three of the four surviving suggestions and both design concerns, and its delta table cited exact CI log lines rather than summarising them. One of those citations mattered: it quoted --- PASS for both new tests at specific line numbers, and when my own grep of the same log came back empty I nearly recorded the quote as unsupported. GitHub serves that log as binary, so grep needs -a; the lines are exactly where Claude said. Two of its factual claims are wrong, both in Strengths rather than in findings. It reported 27 template symlinks where the tree holds 24 and the log prints 25 resolution lines, and it described a two-hop opensuse.yaml → opensuse-leap.yaml → opensuse-leap-16.yaml chain that the tree does not contain. Its $RUNNER_TEMP observation framed a harmless pre-checkout write as a latent bug.

Codex GPT 5.6 Sol

One finding, and no other reviewer reached it. It got to the Layer 4 regex gap by fetching Git for Windows' installer source instead of reasoning about what the pattern looks like it covers, which is the same move that won it round 2's only Important finding. Both halves survived checking, including the omitted HKCU WOW6432Node root: the installer's own QueryUninstallValues probes exactly four locations and the action lists three. It marked three files clean that turned out to hold suggestions, the same cost its brevity carried last round.

Gemini 3.1 Pro

Clean at every severity for the second round running. Unlike round 2 it attached real evidence to its verdicts, tracing compare() through versionutil.go and checking Layer 5's three arms against sshutil.go, and both conclusions match what I derived independently. Its Acknowledged Limitations section still restates the injected decline list item for item with no measurement attached, which is the one pattern the round-3 prompt asked agents to avoid.

Gemini 3.5 Flash

Re-raised its round-2 REG_EXPAND_SZ claim, this time as a standalone suggestion with a patch. The patch is self-defeating: it restores the registry value kind while writing back a string its own premise says was read fully expanded, so the expanded paths it objects to survive the fix. Flash did do work the others skipped, cross-compiling the Windows test binary and leaving sshutil.test.exe in its worktree as evidence. Against that, its delta table marked the registry row PASS on the strength of unnamed "Registry/API research".

Summary

Claude Opus 5Codex GPT 5.6 SolGemini 3.1 ProGemini 3.5 Flash
Duration20m 41s14m 34s2m 53s3m 13s
Findings3S1Snonenone
Tool calls73 (Bash 62, Read 11)39 (shell 36, plan 2, run 1)12 (runshellcommand 12)25 (readfile 11, runshellcommand 6, grepsearch 4)
Design observations6313
False positives0000
Unique insights3100
Files reviewed12121212
Coverage misses1344
Totals3S1Snonenone
Downgraded0000
Dropped0001

Reconciliation. Gemini 3.5 Flash S1 REG_EXPAND_SZ: suggestion → Acknowledged Limitations. Round 3 already addressed the underlying concern with the destructive-runner warning the author added for it, and the patch offered this round does not preserve what it claims to preserve. No other severities changed.

The two Gemini models share a family, so their converging on a near-clean result is one opinion rather than two. Between them they produced nothing that survived as a finding. Claude and Codex split the round the same way they split round 2: Claude on volume, with three suggestions and the design concerns but two loose facts in its supporting prose, and Codex on a single finding reached by reading a primary source. Codex's is the one a reviewer could not have derived from the diff alone.


Review Process Notes

Skill improvements

Repo context updates