Skip to content

ci(wsl): real WSL2 lane for WSL via orcad (C10) - #27921

Merged
nwparker merged 6 commits into
mainfrom
nwparker/wsl-c10-ci-lane
Oct 13, 2026
Merged

nwparker merged 6 commits into
mainfrom
nwparker/wsl-c10-ci-lane

Conversation

@nwparker

@nwparker nwparker commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​475 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​475
Prod 1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​220 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​220

Stacked on #27916.

ELI5

Orca is learning to run its server inside a WSL Linux distro on Windows. This adds a CI check that does that for real on a GitHub Windows machine: it creates two fresh Ubuntu distros, starts Orca's server in each, talks to it, breaks it on purpose, and checks Orca notices and recovers.

What Changed

  • New workflow .github/workflows/wsl-orcad-e2e.yml (not a required check). It runs on PRs that touch src/main/wsl/**, src/main/ssh/orcad-*, src/main/ssh/remote-install-transport.ts or the workflow itself.
    • An Ubuntu job builds only the linux-x64-glibc orcad template. That is the one artifact the distro needs, and its node-pty slot builds only on Linux. No Electron build.
    • The windows-2025 job first installs a checksum-pinned, cached WSL 3.0.1 MSI. The image ships WSL 2.7.14, and on that version stopping a systemd distro drops the VM-wide WSLInterop entry (binfmt config cleared on all distro when distro with systemd is terminated microsoft/WSL#13885). The lane caught this on its first run. It installs the MSI rather than running wsl --update, because wsl --update calls the GitHub API and got a 403 from the runner.
    • The job then imports a checksum-pinned, cached Ubuntu 24.04 rootfs twice as WSL2: once with systemd off and once with systemd on. Each gets a non-root default user. If the import fails, or the distro is not version 2, the job fails. There is no WSL1 fallback. The import runs in the background while dependencies install.
  • New gated test src/main/wsl/wsl-orcad-real-distro-lane.test.ts, skipped unless ORCA_RUN_WSL_ORCAD_LANE=1 on Windows. It runs in two processes:
    • launch, per distro: connect through the liveness supervisor (C4 deploy and launch, C5 bridge). Over the bridge it then calls status.get, runs repo.add on a fresh git init, and creates a terminal whose output token it reads back. Then it kills orcad with kill -9 and expects reconnecting(orcad-exited) followed by ready with a census and a new pid. Next it runs wsl --terminate and expects distro-stopped with terminals: exited, and nothing boots the distro again. Finally it reconnects. After the systemd distro is terminated, it checks that WSLInterop still exists.
    • survive, at least 60 s after the launch process exited: both distros must still be Running, and the holders must be adopted as live. It reconnects, then tears down. Teardown must leave no guest bridge processes, all holders released, and no wsl.exe bridge or holder processes. It checks WSLInterop once more after the systemd distro stops.
    • A missing WSLInterop entry is re-registered as root, and the lane still fails.
  • Every run writes JSON receipts with timings to the job summary and uploads them with the logs. On failure it also uploads orcad's logs from each distro.

Why

This is the C10 step of WSL via orcad. The spike (#27783) showed hosted windows-2025 runs WSL2 reliably, so this uses a hosted runner rather than a self-hosted one. It is a gated vitest, like orcad-windows-host-lane.test.ts, rather than a Playwright spec, because none of this needs the app UI. That keeps the lane to one template build plus a Node install. Running the test as two processes is what proves the holder outlives the process that spawned it.

Linked Issue

N/A (internal plan step C10)

Visual Proof

N/A: CI only, no UI change.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

The new lane is the test. It passed on two consecutive runs (run 38229195911, attempts 1 and 2). Timings are in a comment below. Typecheck passes locally (pnpm tc:node).

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@nwparker

Copy link
Copy Markdown
Contributor Author

Lane results: green on two consecutive runs (run 38229195911, attempts 1 and 2; head 4cf275d)

attempt 1 attempt 2
Job wall time (template + Windows) ~5.6 min ~5.5 min
Windows job 4m31s 4m31s
WSL 3.0.1 MSI install 31 s (download) 37 s (cache hit)
Import + provision of both distros (in the background) 58 s 54 s
Phase 1 (both distros) 72 s 70 s
Phase 2 (60 s hold + teardown) 64 s 63 s

Each distro in phase 1 (plain / systemd, attempt 2): first connect took 12.1 / 13.2 s. Killing orcad was detected in 322 / 309 ms, and the relaunch with a census (liveSessions 1, the terminal survived) took 6.3 / 6.9 s. wsl --terminate was detected as distro-stopped (terminals exited) in 175 / 708 ms. The reboot connect took 6.9 / 10.4 s. WSLInterop was present at all three checks. Phase 2 found both distros still Running 60 s after phase 1 exited, and both holders were adopted as live.

Iterations before green:

  1. The template artifact was missing .claude-plugin (upload-artifact skips hidden files). Fixed by setting include-hidden-files.
  2. WSLInterop disappeared after a systemd distro terminate on the image's WSL 2.7.14 (binfmt config cleared on all distro when distro with systemd is terminated microsoft/WSL#13885). The lane restored it and failed, as designed. Fixed by installing WSL 3.0.1.
  3. wsl --update --web-download got a 403 from the GitHub API on the runner. Fixed by installing a pinned, cached MSI.
  4. The holder's release() returned unverifiable instead of exited (see below).

Open issue (C3): WslDistroHolder.release() returns unverifiable intermittently. It happened in 2 of 3 phase-2 runs, including attempt 2 here. The cause is that stopRecordedHolder stops polling once the state is anything other than live. A holder that is mid-exit can still be in the process table without a readable command line, which classifies as unverifiable. Polling holderState() shows it reaching exited within seconds, so the lane now waits for that state rather than requiring release() itself to return exited. The fix probably belongs in C3: keep polling while the state is unverifiable, until the deadline.

@nwparker
nwparker force-pushed the nwparker/wsl-c6-liveness branch from 9ac0ba7 to 2b419a3 Compare October 12, 2026 10:39
@nwparker
nwparker force-pushed the nwparker/wsl-c10-ci-lane branch from 4cf275d to b597049 Compare October 12, 2026 10:39
@nwparker
nwparker force-pushed the nwparker/wsl-c10-ci-lane branch from b597049 to d9df88c Compare October 12, 2026 12:00
@nwparker
nwparker force-pushed the nwparker/wsl-c6-liveness branch from 2b419a3 to caf2132 Compare October 12, 2026 12:00
@nwparker
nwparker force-pushed the nwparker/wsl-c10-ci-lane branch from ff9a098 to b80ef82 Compare October 12, 2026 16:25
@nwparker
nwparker force-pushed the nwparker/wsl-c6-liveness branch 2 times, most recently from 7d5d2c4 to 4e4aa55 Compare October 13, 2026 13:14
@nwparker
nwparker changed the base branch from nwparker/wsl-c6-liveness to main October 13, 2026 13:53
@nwparker
nwparker marked this pull request as ready for review October 13, 2026 13:53
@nwparker
nwparker force-pushed the nwparker/wsl-c10-ci-lane branch from b80ef82 to 7c21149 Compare October 13, 2026 13:53
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The pull request adds a manually triggered and pull-request workflow for testing orcad on plain and systemd-enabled Ubuntu 24.04 WSL2 distros. The workflow builds a Linux x64 glibc template, installs pinned WSL and rootfs versions, and provisions both distros. A Windows-only, opt-in integration test checks deployment, runtime RPC, recovery after process and distro termination, and survival across separate test processes. The workflow records receipts and collects orcad logs on failure.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 7c211

The lane is mergeable with a bounded teardown concern: guest commands can briefly leave a WSL client running after completion.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 1 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and directly matches the changes. It includes the ELI5 summary, changed files and behavior, rationale, testing results, visual-proof rationale, scope notes, and checklist s…
Title check Passed The title is concise, specific, and accurately identifies the main change: a real WSL2 CI lane for WSL via orcad.
Full details: Docstring Coverage

Explanation

Docstring coverage is 27.78% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 1 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 19b2b436-444d-40dc-baf1-f2dc721aec23
📥 Commits

Reviewing files that changed from the base of the PR and between 4280eb1 and 7c21149.

📒 Files selected for processing (2)
  • .github/workflows/wsl-orcad-e2e.yml
  • src/main/wsl/wsl-orcad-real-distro-lane.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Comment on lines +106 to +108
function guest(distro: string, command: string): Promise<string> {
return execCommand(transportFor(distro), command, { timeoutMs: 60_000 })
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C5 'createWslCommandLoopPool|dispose' --type=ts -g '*wsl-command-loop*'

Repository: stablyai/orca

Length of output: 7445


🏁 Script executed:

set -eu
printf '%s\n' '--- guest and transport call sites ---'
rg -n -F -- 'function transportFor' src/main/wsl/wsl-orcad-real-distro-lane.test.ts
rg -n -F -- 'function guest' src/main/wsl/wsl-orcad-real-distro-lane.test.ts
rg -n -F -- 'createWslOrcadHostTransport' src/main/wsl src/main
printf '%s\n' '--- lane excerpt ---'
nl -ba src/main/wsl/wsl-orcad-real-distro-lane.test.ts | sed -n '70,125p'
printf '%s\n' '--- transport declarations and implementation references ---'
rg -n -F -- 'createWslOrcadHostTransport' src
rg -n -F -- 'execWithClosedStdin' src/main
rg -n -F -- 'function execCommand' src/main
printf '%s\n' '--- candidate source files ---'
rg --files src/main | rg 'wsl|ssh-relay-exec-command'

Repository: stablyai/orca

Length of output: 26577


🏁 Script executed:

set -eu
printf '%s\n' '--- host transport ---'
nl -ba src/main/wsl/wsl-orcad-host-transport.ts | sed -n '1,180p'
printf '%s\n' '--- exec command ---'
nl -ba src/main/ssh/ssh-relay-exec-command.ts | sed -n '1,135p'
printf '%s\n' '--- command-loop pool ---'
nl -ba src/main/wsl/wsl-command-loop-pool.ts | sed -n '1,145p'
printf '%s\n' '--- transport type/disposal usage ---'
rg -n -F -- 'WslOrcadHostTransport' src/main/wsl src/main/ssh
rg -n -F -- '.dispose()' src/main/wsl/wsl-orcad-* src/main/wsl/*transport* src/main/ssh

Repository: stablyai/orca

Length of output: 24592


🏁 Script executed:

set -eu
printf '%s\n' '--- guest call sites and helper ---'
rg -n -F -- 'guest(' src/main/wsl/wsl-orcad-real-distro-lane.test.ts
rg -n -F -- 'expectNoGuestBridges' src/main/wsl/wsl-orcad-real-distro-lane.test.ts
printf '%s\n' '--- lane control flow ---'
nl -ba src/main/wsl/wsl-orcad-real-distro-lane.test.ts | sed -n '240,480p'
printf '%s\n' '--- loop idle/end behavior ---'
nl -ba src/main/wsl/wsl-command-loop.ts | sed -n '1,190p'

Repository: stablyai/orca

Length of output: 18896


Dispose the per-command WSL transport.

guest() creates a new command-loop pool for each command. Without disposal, the pool can keep its wsl.exe loop alive for up to the 5-second idle timeout. This can delay teardown and leave a WSL client running after the command completes.

Suggested fix
--- "a/src/main/wsl/wsl-orcad-real-distro-lane.test.ts"
+++ "b/src/main/wsl/wsl-orcad-real-distro-lane.test.ts"
@@ -103,9 +103,14 @@
 }
 
 /** As the distro's default user, through the same transport orcad's deploy uses. */
-function guest(distro: string, command: string): Promise<string> {
-  return execCommand(transportFor(distro), command, { timeoutMs: 60_000 })
+async function guest(distro: string, command: string): Promise<string> {
+  const transport = transportFor(distro)
+  try {
+    return await execCommand(transport, command, { timeoutMs: 60_000 })
+  } finally {
+    transport.dispose()
+  }
 }
 
 async function wslExe(args: string[], timeoutMs = 60_000) {
   const result = await runProcess({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function guest(distro: string, command: string): Promise<string> {
return execCommand(transportFor(distro), command, { timeoutMs: 60_000 })
}
async function guest(distro: string, command: string): Promise<string> {
const transport = transportFor(distro)
try {
return await execCommand(transport, command, { timeoutMs: 60_000 })
} finally {
transport.dispose()
}
}

@nwparker
nwparker merged commit fc7dff8 into main Oct 13, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant