Files
orca/.github/workflows/wsl-orcad-e2e.yml
T
Neil b5cb7b5655 feat(wsl-mover): the move driver runs a WSL move end to end, with production gate facts and journaled restore folders
One entry point (and preview/start/cancel/listUnfinished IPC for D1b-9) plans the move, gates
terminals on the hook server's agent status, the close-tab process probe, running automation runs
and live dispatches, pauses and proves exit, stages, sends blobs, commits only from
blobs-transferred (#29825), and finishes on the desktop. Any failure resets to zero or goes forward
when the host committed. Restore folders and the automations to turn back on are kept in the
journal; the host turns those automations back on after commit, retried until it confirms.
A real-PTY test pauses daemon shells, and runs on a real distro in the WSL orcad lane.
2026-10-20 23:33:12 -07:00

376 lines
19 KiB
YAML

name: WSL orcad E2E
# WSL via orcad (step C10) on real WSL2 distros: a GitHub-hosted windows-2025 runner imports a
# pinned Ubuntu 24.04 rootfs twice (systemd off and on), plus Debian 12 and Fedora 41 (systemd off,
# rootfs exported from their container images), and runs
# src/main/wsl/wsl-orcad-real-distro-lane.test.ts against both: deploy and launch, the wsl.exe
# stdio bridge, runtime RPC, liveness recovery, holder survival, structured chat and teardown. Not a required check.
# Hosted WSL2 was proven in #27783: WSL 2.7.14, import 13-18 s, cold boot 8-14 s.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
# This lane drives host RPC, not UI; pr.yml owns mobile bundle/render checks.
paths:
- 'src/main/wsl/**'
- 'src/main/runtime/**'
- 'src/main/native-chat/**'
- 'src/main/claude/**'
- 'src/main/claude-accounts/**'
- 'src/main/codex/**'
- 'src/main/acp/**'
- 'src/main/pi/**'
- 'src/main/orcad/**'
- 'src/main/server/**'
- 'src/main/persistence/**'
- 'src/main/ipc/*wsl*'
- 'src/shared/**'
- 'config/scripts/*orcad*'
- 'package.json'
- 'pnpm-lock.yaml'
- 'src/main/ssh/orcad-*'
- 'src/main/managed-server/**'
- 'src/packages/process-host/**'
# Unit tests never run in this lane; only the real distro test below does.
- '!**/*.test.ts'
- '!**/*.test.tsx'
- 'src/main/wsl/wsl-orcad-real-distro-lane.test.ts'
- 'src/main/persistence/moving-wsl-projects/wsl-move-real-pause.test.ts'
- '.github/workflows/wsl-orcad-e2e.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: wsl-orcad-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# wsl.exe prints UTF-16LE to pipes unless told otherwise.
WSL_UTF8: '1'
ROOTFS_URL: https://releases.ubuntu.com/24.04.4/ubuntu-24.04.4-wsl-amd64.wsl
ROOTFS_SHA256: 9b2f7730dc68227dd04a9f3e5eab86ad85caf556b8606ad94f1f29ff5c4fd3f5
WSL_MSI_URL: https://github.com/microsoft/WSL/releases/download/3.0.1/wsl.3.0.1.0.x64.msi
WSL_MSI_SHA256: 28b1a0d013640a2ac95898ea705fa186e5b4ff767a1c1b49257161bc106599c6
PLAIN_DISTRO: orca-lane-plain
jobs:
# The distro is linux-x64-glibc; its node-pty slot only builds on Linux, and nothing else is needed.
template:
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
name: Build the linux-x64-glibc orcad template
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- uses: ./.github/actions/build-mobile-web-bundle
- name: Build the template
run: |
pnpm build:orcad-prebuilds
pnpm build:orcad-prebuilds --require-slots linux-x64-glibc
node config/scripts/build-orcad-template.mjs --targets linux-x64-glibc
- uses: actions/upload-artifact@v7
with:
name: wsl-orcad-template
path: out/orcad-template/
# The template carries dot-directories (`.claude-plugin`) its manifest checksums.
include-hidden-files: true
retention-days: 1
if-no-files-found: error
# Container images ship no systemd, so these families run the systemd-off distro only.
container-rootfs:
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
name: Export ${{ matrix.family }} rootfs
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- family: debian-12
image: debian:12
- family: fedora-41
image: fedora:41
steps:
- name: Export the image filesystem
run: |
docker pull --platform linux/amd64 '${{ matrix.image }}'
echo "digest: $(docker image inspect --format '{{index .RepoDigests 0}}' '${{ matrix.image }}')"
id="$(docker create --platform linux/amd64 '${{ matrix.image }}')"
mkdir -p rootfs
docker export "$id" > rootfs/rootfs.tar
docker rm "$id" >/dev/null
ls -l rootfs/rootfs.tar
- uses: actions/upload-artifact@v7
with:
name: wsl-orcad-rootfs-${{ matrix.family }}
path: rootfs/rootfs.tar
retention-days: 1
if-no-files-found: error
wsl2:
name: ${{ matrix.name }}
needs: [template, container-rootfs]
runs-on: windows-2025
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: WSL2 distros (systemd off and on)
family: ubuntu-24.04
container: false
systemd-distro: orca-lane-systemd
install: command -v git >/dev/null || (apt-get update -qq && apt-get install -y -qq git)
- name: WSL2 distro (debian-12)
family: debian-12
container: true
systemd-distro: ''
install: apt-get update -qq && apt-get install -y -qq git procps
- name: WSL2 distro (fedora-41)
family: fedora-41
container: true
systemd-distro: ''
install: dnf -y -q install git procps-ng shadow-utils util-linux findutils tar gzip
env:
ORCA_BACKGROUND_LAUNCH: '1'
ORCA_WSL_ORCAD_LANE_RECEIPTS: ${{ github.workspace }}\test-results\wsl-orcad-lane
SYSTEMD_DISTRO: ${{ matrix.systemd-distro }}
ORCA_WSL_ORCAD_LANE_SYSTEMD_DISTRO: ${{ matrix.systemd-distro }}
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
# WSL before 3.0.1 drops the VM-wide WSLInterop entry when a systemd distro stops
# (microsoft/WSL#13885). Images from 20261007 ship 3.0.1, where reinstalling the MSI fails
# with 1603, so install only when older; `wsl --update` gets rate-limited by the GitHub API.
- name: Check the installed WSL version
id: wsl-version
run: |
"ImageOS=$env:ImageOS ImageVersion=$env:ImageVersion"
$details = (wsl.exe --version) -join "`n"
$details
$match = [regex]::Match($details, '(?m)^WSL version:\s*(\d+(?:\.\d+){2,3})\s*$')
# Inbox WSL has no --version, so an unreadable version means the package is needed.
$installed = if ($LASTEXITCODE -eq 0 -and $match.Success) { [version]$match.Groups[1].Value } else { [version]'0.0.0' }
$needsInstall = $installed -lt [version]'3.0.1'
"Installed WSL $installed; minimum 3.0.1; install needed: $needsInstall"
"needs-install=$($needsInstall.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT
- name: Restore the pinned WSL package
id: wsl-msi-cache
if: steps.wsl-version.outputs.needs-install == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/wsl-msi
key: wsl-orcad-msi-${{ env.WSL_MSI_SHA256 }}
- name: Install WSL 3.0.1
if: steps.wsl-version.outputs.needs-install == 'true'
timeout-minutes: 8
run: |
$dir = Join-Path $env:RUNNER_TEMP 'wsl-msi'
New-Item -ItemType Directory -Force $dir | Out-Null
$msi = Join-Path $dir 'wsl.msi'
if (-not (Test-Path $msi)) { & "$env:WINDIR\System32\curl.exe" --fail --location --silent --show-error --output $msi $env:WSL_MSI_URL }
if ((Get-FileHash $msi -Algorithm SHA256).Hash.ToLowerInvariant() -ne $env:WSL_MSI_SHA256) {
Remove-Item $msi; throw 'WSL package checksum mismatch'
}
# Kept beside the receipts so the always() upload carries it.
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
$log = Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'wsl-msi-install.log'
$install = Start-Process msiexec.exe -ArgumentList @('/i', $msi, '/qn', '/norestart', '/l*v', $log) -Wait -PassThru
if ($install.ExitCode -notin 0, 3010) {
if (Test-Path $log) {
$lines = @(Get-Content $log)
$failed = ($lines | Select-String -SimpleMatch 'Return value 3' | Select-Object -First 1).LineNumber
if ($failed) { '--- msiexec log before the failing action'; $lines[[math]::Max(0, $failed - 40)..($failed - 1)] }
'--- msiexec log tail'; $lines | Select-Object -Last 60
}
throw "WSL install failed: $($install.ExitCode)"
}
wsl.exe --shutdown
- name: Save the pinned WSL package
if: steps.wsl-version.outputs.needs-install == 'true' && steps.wsl-msi-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ runner.temp }}/wsl-msi
key: wsl-orcad-msi-${{ env.WSL_MSI_SHA256 }}
- name: Verify WSL is at least 3.0.1
run: |
$details = (wsl.exe --version) -join "`n"
if ($LASTEXITCODE -ne 0) { throw "Could not read the WSL version: $details" }
$details
$match = [regex]::Match($details, '(?m)^WSL version:\s*(\d+(?:\.\d+){2,3})\s*$')
if (-not $match.Success) { throw 'Could not parse the WSL version' }
if ([version]$match.Groups[1].Value -lt [version]'3.0.1') { throw "WSL $($match.Groups[1].Value) is older than 3.0.1" }
- name: Restore the pinned rootfs
id: rootfs-cache
if: ${{ !matrix.container }}
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/wsl-rootfs
key: wsl-orcad-rootfs-${{ env.ROOTFS_SHA256 }}
- uses: actions/download-artifact@v8
if: ${{ matrix.container }}
with:
name: wsl-orcad-rootfs-${{ matrix.family }}
path: ${{ runner.temp }}/wsl-rootfs
# Runs beside the dependency install; the lane waits for it below.
- name: Import the distros as WSL2
id: import
background: true
timeout-minutes: 10
run: |
$cache = Join-Path $env:RUNNER_TEMP 'wsl-rootfs'
New-Item -ItemType Directory -Force $cache | Out-Null
if ('${{ matrix.container }}' -eq 'true') {
$rootfs = Join-Path $cache 'rootfs.tar'
} else {
$rootfs = Join-Path $cache 'ubuntu.wsl'
if (-not (Test-Path $rootfs)) { Invoke-WebRequest $env:ROOTFS_URL -OutFile $rootfs }
if ((Get-FileHash $rootfs -Algorithm SHA256).Hash.ToLowerInvariant() -ne $env:ROOTFS_SHA256) {
Remove-Item $rootfs; throw 'rootfs checksum mismatch'
}
}
$distros = @(@{ Name = $env:PLAIN_DISTRO; Systemd = 'false' })
if ($env:SYSTEMD_DISTRO) { $distros += @{ Name = $env:SYSTEMD_DISTRO; Systemd = 'true' } }
foreach ($d in $distros) {
$sw = [Diagnostics.Stopwatch]::StartNew()
# No WSL1 fallback: the lane exists to prove WSL2.
wsl.exe --import $d.Name (Join-Path $env:RUNNER_TEMP $d.Name) $rootfs --version 2
if ($LASTEXITCODE -ne 0) { throw "WSL2 import of $($d.Name) failed: $LASTEXITCODE" }
"$($d.Name) import_ms=$($sw.ElapsedMilliseconds)"
if (-not ((wsl.exe --list --verbose) -match "$($d.Name)\s+\S+\s+2\s*$")) { throw "$($d.Name) is not a WSL2 distro" }
# A non-root default user, as on a real install; git for the lane's repo.
# The env exports go to the file a login bash reads first; Fedora's skel ships .bash_profile.
$setup = "set -e; ${{ matrix.install }}; id orca >/dev/null 2>&1 || useradd -m -s /bin/bash orca; " +
"printf '[boot]\nsystemd=$($d.Systemd)\n[user]\ndefault=orca\n' > /etc/wsl.conf; " +
"rc=/home/orca/.profile; for f in /home/orca/.bash_profile /home/orca/.bash_login; do " +
"if [ -f `$f ]; then rc=`$f; break; fi; done; " +
"printf 'export ORCA_BACKGROUND_LAUNCH=1\nexport USERPROFILE=`$HOME\nexport APPDATA=`$HOME/.config\nexport LOCALAPPDATA=`$HOME/.local/share\nexport XDG_CONFIG_HOME=`$HOME/.config\nexport XDG_DATA_HOME=`$HOME/.local/share\nexport XDG_CACHE_HOME=`$HOME/.cache\nexport XDG_STATE_HOME=`$HOME/.local/state\nexport CLAUDE_CONFIG_DIR=`$HOME/.claude\nexport CODEX_HOME=`$HOME/.codex\n' >> `$rc"
wsl.exe -d $d.Name -u root --exec sh -c $setup
if ($LASTEXITCODE -ne 0) { throw "provisioning $($d.Name) failed: $LASTEXITCODE" }
wsl.exe --terminate $d.Name
$sw.Restart()
wsl.exe -d $d.Name --exec sh -c 'echo "user=$(id -un) pid1=$(ps -p 1 -o comm=) kernel=$(uname -r)"'
if ($LASTEXITCODE -ne 0) { throw "first boot of $($d.Name) failed: $LASTEXITCODE" }
"$($d.Name) boot_ms=$($sw.ElapsedMilliseconds)"
wsl.exe --terminate $d.Name
}
wsl.exe --list --verbose
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- uses: actions/download-artifact@v8
with:
name: wsl-orcad-template
path: ${{ runner.temp }}/orcad-template
- wait: import
# Saved even when the lane fails; the import step verified its checksum.
- name: Save the pinned rootfs
if: ${{ !matrix.container && steps.rootfs-cache.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v5
with:
path: ${{ runner.temp }}/wsl-rootfs
key: wsl-orcad-rootfs-${{ env.ROOTFS_SHA256 }}
- name: Isolate the desktop test home
run: |
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
$files = @('.claude/settings.json', '.claude.json', '.codex/hooks.json', '.codex/config.toml') | ForEach-Object { Join-Path $env:USERPROFILE $_ }
$hashes = @($files | ForEach-Object {
@{ Path = $_; Hash = $(if (Test-Path $_) { (Get-FileHash $_ -Algorithm SHA256).Hash } else { 'absent' }) }
})
$hashes | ConvertTo-Json | Set-Content (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'real-home-hashes.json')
$homeDir = Join-Path $env:RUNNER_TEMP 'wsl-orcad-desktop-home'
foreach ($name in @('HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME', 'XDG_STATE_HOME', 'CLAUDE_CONFIG_DIR', 'CODEX_HOME')) {
$path = Join-Path $homeDir $name
New-Item -ItemType Directory -Force $path | Out-Null
"$name=$path" >> $env:GITHUB_ENV
}
- name: 'Phase 1: deploy, RPC, kill orcad, terminate distro'
timeout-minutes: 20
env:
ORCA_RUN_WSL_ORCAD_LANE: '1'
ORCA_WSL_ORCAD_LANE_PHASE: launch
ORCA_ORCAD_TEMPLATE_PATH: ${{ runner.temp }}/orcad-template
run: |
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/wsl/wsl-orcad-real-distro-lane.test.ts --reporter=verbose 2>&1 | Tee-Object (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'launch.log')
if ($global:LASTEXITCODE -ne 0) { throw "launch phase failed: $global:LASTEXITCODE" }
# A new process: the holders must have kept both distros Running since phase 1 exited.
- name: 'Phase 2: holders survive, chat, reconnect, teardown'
timeout-minutes: 15
env:
ORCA_RUN_WSL_ORCAD_LANE: '1'
ORCA_WSL_ORCAD_LANE_PHASE: survive
ORCA_ORCAD_TEMPLATE_PATH: ${{ runner.temp }}/orcad-template
run: |
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/wsl/wsl-orcad-real-distro-lane.test.ts --reporter=verbose 2>&1 | Tee-Object (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'survive.log')
if ($global:LASTEXITCODE -ne 0) { throw "survive phase failed: $global:LASTEXITCODE" }
# The WSL move's pause on real distro shells: Sleep ends them, the daemon proves the exit,
# the journal keeps each pane's folder, and a new shell opens there.
- name: 'WSL move: pause real distro terminals'
if: ${{ !cancelled() && steps.import.outcome == 'success' }}
timeout-minutes: 10
env:
ORCA_WSL_MOVE_REAL_PAUSE_DISTRO: ${{ env.PLAIN_DISTRO }}
run: |
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
node node_modules/vitest/vitest.mjs run --config config/vitest.config.ts src/main/persistence/moving-wsl-projects/wsl-move-real-pause.test.ts --reporter=verbose 2>&1 | Tee-Object (Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'wsl-move-real-pause.log')
if ($global:LASTEXITCODE -ne 0) { throw "WSL move real pause failed: $global:LASTEXITCODE" }
wsl.exe --terminate $env:PLAIN_DISTRO
- name: Real desktop agent configuration is unchanged
if: always()
run: |
$baseline = Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'real-home-hashes.json'
if (Test-Path $baseline) {
foreach ($file in (Get-Content $baseline -Raw | ConvertFrom-Json)) {
$hash = if (Test-Path $file.Path) { (Get-FileHash $file.Path -Algorithm SHA256).Hash } else { 'absent' }
if ($hash -ne $file.Hash) { throw "Real agent configuration changed: $($file.Path)" }
}
}
# Belt and braces for the runner: a missing VM-wide interop entry is restored, then fails.
- name: WSLInterop is intact
if: ${{ !cancelled() && steps.import.outcome == 'success' }}
timeout-minutes: 3
run: |
$state = wsl.exe -d $env:PLAIN_DISTRO -u root --exec sh -c 'if [ -e /proc/sys/fs/binfmt_misc/WSLInterop ]; then echo present; else echo :WSLInterop:M::MZ::/init:P > /proc/sys/fs/binfmt_misc/register && echo restored; fi'
"WSLInterop: $state"
wsl.exe --terminate $env:PLAIN_DISTRO
if ("$state".Trim() -ne 'present') { throw "WSLInterop was missing ($state)" }
- name: Collect orcad logs
if: failure()
timeout-minutes: 3
run: |
$out = $env:ORCA_WSL_ORCAD_LANE_RECEIPTS
New-Item -ItemType Directory -Force $out | Out-Null
foreach ($d in @($env:PLAIN_DISTRO, $env:SYSTEMD_DISTRO) | Where-Object { $_ }) {
wsl.exe -d $d --exec sh -c 'for f in $(find "$HOME/.orca-remote" -name "*.log*" -type f 2>/dev/null); do echo "=== $f"; tail -n 400 "$f"; done' > (Join-Path $out "$d-orcad-logs.txt") 2>&1
}
wsl.exe --list --verbose
- name: Summary
if: always()
run: |
$dir = $env:ORCA_WSL_ORCAD_LANE_RECEIPTS
$lines = @("### WSL orcad lane (${{ matrix.family }}) — $env:ImageOS $env:ImageVersion", '')
foreach ($phase in 'launch', 'survive') {
$file = Join-Path $dir "$phase.json"
if (Test-Path $file) { $lines += "#### $phase", '```json', (Get-Content $file -Raw), '```' }
}
$lines | Add-Content $env:GITHUB_STEP_SUMMARY
- uses: actions/upload-artifact@v7
if: always()
with:
name: wsl-orcad-lane-receipts-${{ matrix.family }}
path: test-results/wsl-orcad-lane/
retention-days: 7
if-no-files-found: ignore