ci: record durable WSL lifecycle operations (#32603)

This commit is contained in:
Neil
2026-10-23 08:09:13 -07:00
committed by GitHub
parent 9aa208a4ab
commit db46593f55
6 changed files with 135 additions and 21 deletions
+1
View File
@@ -57,6 +57,7 @@ on:
- 'tests/e2e/helpers/compiled-orca-cli.ts'
- 'tests/e2e/helpers/windows-host-orcad-processes.ts'
- 'config/ci/windows-ssh-provider/**'
- 'config/ci/windows-lifecycle-receipt.ps1'
- '.github/workflows/ssh-windows-hosts.yml'
- '.github/actions/prepare-orcad-prebuilds/**'
- '.github/actions/restore-pnpm-verification/**'
+22 -13
View File
@@ -35,6 +35,8 @@ on:
- '!**/*.test.tsx'
- 'src/main/wsl/wsl-orcad-real-distro-lane.test.ts'
- '.github/workflows/wsl-orcad-e2e.yml'
- 'config/ci/wsl-lifecycle/**'
- 'config/ci/windows-lifecycle-receipt.ps1'
workflow_dispatch:
inputs:
qualification:
@@ -237,12 +239,17 @@ jobs:
with:
name: wsl-orcad-rootfs-${{ matrix.family }}
path: ${{ runner.temp }}/wsl-rootfs
- name: Verify WSL lifecycle receipts
run: ./config/ci/wsl-lifecycle/test-wsl-lifecycle-command.ps1
# 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: |
. config/ci/wsl-lifecycle/invoke-wsl-lifecycle-command.ps1
New-Item -ItemType Directory -Force $env:ORCA_WSL_ORCAD_LANE_RECEIPTS | Out-Null
$lifecycle = Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'import-lifecycle.json'
$cache = Join-Path $env:RUNNER_TEMP 'wsl-rootfs'
New-Item -ItemType Directory -Force $cache | Out-Null
if ('${{ matrix.family != 'ubuntu-24.04' }}' -eq 'true') {
@@ -265,10 +272,9 @@ jobs:
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" }
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage import -Distro $d.Name -Arguments @('--import', $d.Name, (Join-Path $env:RUNNER_TEMP $d.Name), $rootfs, '--version', '2')
"$($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" }
if (-not ((Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage verify-version -Distro $d.Name -Arguments @('--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; $install; id orca >/dev/null 2>&1 || useradd -m -s /bin/bash orca; " +
@@ -276,16 +282,14 @@ jobs:
"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
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage provision -Distro $d.Name -Arguments @('-d', $d.Name, '-u', 'root', '--exec', 'sh', '-c', $setup)
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage terminate -Distro $d.Name -Arguments @('--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" }
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage first-boot -Distro $d.Name -Arguments @('-d', $d.Name, '--exec', 'sh', '-c', 'echo "user=$(id -un) pid1=$(ps -p 1 -o comm=) kernel=$(uname -r)"')
"$($d.Name) boot_ms=$($sw.ElapsedMilliseconds)"
wsl.exe --terminate $d.Name
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage terminate -Distro $d.Name -Arguments @('--terminate', $d.Name)
}
wsl.exe --list --verbose
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage list -Arguments @('--list', '--verbose')
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
@@ -350,9 +354,14 @@ jobs:
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
. config/ci/wsl-lifecycle/invoke-wsl-lifecycle-command.ps1
$lifecycle = Join-Path $env:ORCA_WSL_ORCAD_LANE_RECEIPTS 'interop-lifecycle.json'
try {
$state = Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage interop-check -Distro $env:PLAIN_DISTRO -Arguments @('-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"
} finally {
Invoke-WslLifecycleCommand -Receipt $lifecycle -Stage terminate -Distro $env:PLAIN_DISTRO -Arguments @('--terminate', $env:PLAIN_DISTRO)
}
if ("$state".Trim() -ne 'present') { throw "WSLInterop was missing ($state)" }
- name: Collect orcad logs
if: failure()
+10
View File
@@ -0,0 +1,10 @@
function Write-WindowsLifecycleReceipt([hashtable]$Report,[string]$Receipt) {
try {
$bytes=[Text.UTF8Encoding]::new($false).GetBytes(($Report | ConvertTo-Json -Depth 6))
$temporary="$Receipt.pending"
$stream=[IO.FileStream]::new($temporary,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read)
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
[IO.File]::Move($temporary,$Receipt,$true)
return $true
} catch {return $false}
}
@@ -1,3 +1,5 @@
. (Join-Path $PSScriptRoot '../../windows-lifecycle-receipt.ps1')
function Assert-IsolatedWindowsSshCi([ValidateSet('arm64','x64')][string]$Arch) {
$os=@{arm64='Arm64';x64='X64'}[$Arch]
if($env:GITHUB_ACTIONS -ne 'true' -or $env:ORCA_ISOLATED_SSH_CI -ne '1' -or [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() -ne $os){throw "Requires isolated native $Arch GitHub runner"}
@@ -21,14 +23,8 @@ function Assert-WindowsSshStockShell {
function Write-WindowsSshReceiptStage([hashtable]$Report,[string]$Receipt,[string]$Stage) {
$timestamp=[DateTime]::UtcNow.ToString('o')
$Report.stages+=@{stage=$Stage;utc=$timestamp}
try {
$bytes=[Text.UTF8Encoding]::new($false).GetBytes(($Report | ConvertTo-Json -Depth 6))
$temporary="$Receipt.pending"
$stream=[IO.FileStream]::new($temporary,[IO.FileMode]::Create,[IO.FileAccess]::Write,[IO.FileShare]::Read)
try {$stream.Write($bytes,0,$bytes.Length);$stream.Flush($true)} finally {$stream.Dispose()}
[IO.File]::Move($temporary,$Receipt,$true)
$written=$true
} catch {$written=$false;Write-Warning 'Progress receipt could not be updated; cleanup must still run'}
$written=Write-WindowsLifecycleReceipt $Report $Receipt
if(-not $written){Write-Warning 'Progress receipt could not be updated; cleanup must still run'}
Write-Host "Native SSH stage: $Stage ($timestamp)"
return $written
}
@@ -0,0 +1,39 @@
. (Join-Path $PSScriptRoot '../windows-lifecycle-receipt.ps1')
$script:wslLifecycleReports=@{}
function Save-WslLifecycleReceipt([hashtable]$Report,[string]$Receipt) {
if([Text.Encoding]::UTF8.GetByteCount(($Report | ConvertTo-Json -Depth 6)) -gt 16384 -or -not (Write-WindowsLifecycleReceipt $Report $Receipt)){
Write-Warning 'WSL lifecycle receipt unavailable; preserve native command and cleanup behavior'
}
}
function Invoke-WslLifecycleCommand {
param(
[Parameter(Mandatory=$true)][string]$Receipt,
[Parameter(Mandatory=$true)][ValidatePattern('^[a-z][a-z0-9-]{0,63}$')][string]$Stage,
[ValidateLength(0,128)][string]$Distro='',
[Parameter(Mandatory=$true)][string[]]$Arguments
)
if(-not $script:wslLifecycleReports.ContainsKey($Receipt)){
$script:wslLifecycleReports[$Receipt]=@{version=1;sourceSha=$env:GITHUB_SHA;runId=$env:GITHUB_RUN_ID;runAttempt=$env:GITHUB_RUN_ATTEMPT;imageVersion=$env:ImageVersion;operations=@()}
}
$report=$script:wslLifecycleReports[$Receipt]
$operation=@{stage=$Stage;distro=$Distro;status='running';startedAt=[DateTime]::UtcNow.ToString('o');elapsedMs=0;exitCode=$null}
if($report.operations.Count -lt 32){$report.operations+=@($operation)}else{$report.truncated=$true}
Save-WslLifecycleReceipt $report $Receipt
Write-Host "WSL lifecycle: $Stage $Distro started"
$watch=[Diagnostics.Stopwatch]::StartNew()
try {
& wsl.exe @Arguments
$operation.exitCode=$LASTEXITCODE
if($LASTEXITCODE -ne 0){throw "WSL lifecycle $Stage failed for $Distro with exit $LASTEXITCODE"}
$operation.status='passed'
} catch {
$operation.status='failed'
throw
} finally {
$watch.Stop();$operation.elapsedMs=$watch.ElapsedMilliseconds;$operation.finishedAt=[DateTime]::UtcNow.ToString('o')
Save-WslLifecycleReceipt $report $Receipt
Write-Host "WSL lifecycle: $Stage $Distro $($operation.status) $($operation.elapsedMs)ms"
}
}
@@ -0,0 +1,59 @@
$ErrorActionPreference='Stop'
. (Join-Path $PSScriptRoot 'invoke-wsl-lifecycle-command.ps1')
$root=Join-Path ([IO.Path]::GetTempPath()) ('orca-wsl-lifecycle-'+[Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $root | Out-Null
$script:activeReceipt=$null
$script:observedRunning=$false
$script:failStart=$false
$originalExit=$env:ORCA_WSL_LIFECYCLE_TEST_EXIT
# Replace WSL servicing only; the real native process proves argv, stdout and exit handling.
function wsl.exe {
param([Parameter(ValueFromRemainingArguments=$true)][string[]]$NativeArguments)
if($script:activeReceipt){
$before=Get-Content -LiteralPath $script:activeReceipt -Raw | ConvertFrom-Json
if($before.operations[-1].status -ne 'running' -or $before.operations[-1].exitCode -ne $null){throw 'Operation was not durable before native launch'}
$script:observedRunning=$true
}
if($script:failStart){throw 'Injected launch refusal'}
& node -e 'process.stdout.write(JSON.stringify(process.argv.slice(1)));process.exit(Number(process.env.ORCA_WSL_LIFECYCLE_TEST_EXIT||0))' -- @NativeArguments
$global:LASTEXITCODE=$LASTEXITCODE
}
try {
$receipt=Join-Path $root 'success.json';$script:activeReceipt=$receipt
$arguments=@('--import','name with spaces','quote"inside','literal $HOME','line1'+"`n"+'line2','日本語')
$output=Invoke-WslLifecycleCommand -Receipt $receipt -Stage import -Distro 'orca-lane-plain' -Arguments $arguments
$actual=ConvertFrom-Json $output
if(($actual | ConvertTo-Json -Compress) -ne ($arguments | ConvertTo-Json -Compress)){throw 'Native argv or stdout changed'}
$completed=Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json
if(-not $script:observedRunning -or $completed.version -ne 1 -or $completed.operations.Count -ne 1 -or $completed.operations[0].status -ne 'passed' -or $completed.operations[0].exitCode -ne 0 -or -not $completed.operations[0].finishedAt -or $completed.operations[0].elapsedMs -lt 0){throw 'Successful lifecycle receipt incomplete'}
if((Get-Content -LiteralPath $receipt -Raw) -match 'literal \$HOME|NativeArguments'){throw 'Receipt exposed command arguments'}
$receipt=Join-Path $root 'failed.json';$script:activeReceipt=$receipt;$env:ORCA_WSL_LIFECYCLE_TEST_EXIT='7'
$failure=$null
try {Invoke-WslLifecycleCommand -Receipt $receipt -Stage provision -Distro 'orca-lane-plain' -Arguments @('--exec','sh') | Out-Null}catch{$failure=$_}
$failed=Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json
if(-not $failure -or $failure.Exception.Message -notmatch 'exit 7' -or $failed.operations[0].status -ne 'failed' -or $failed.operations[0].exitCode -ne 7){throw 'Native failure was suppressed or not recorded'}
$receipt=Join-Path $root 'launch-refused.json';$script:activeReceipt=$receipt;$script:failStart=$true
$failure=$null
try {Invoke-WslLifecycleCommand -Receipt $receipt -Stage terminate -Arguments @('--terminate','orca-lane-plain') | Out-Null}catch{$failure=$_}
$refused=Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json
if(-not $failure -or $failure.Exception.Message -ne 'Injected launch refusal' -or $refused.operations[0].status -ne 'failed' -or $refused.operations[0].exitCode -ne $null){throw 'Launch refusal was replaced or misreported'}
$script:activeReceipt=$null;$script:failStart=$false;$env:ORCA_WSL_LIFECYCLE_TEST_EXIT='0'
$missingParent=Join-Path $root 'absent/cleanup.json'
$output=Invoke-WslLifecycleCommand -Receipt $missingParent -Stage terminate -Arguments @('--terminate','orca-lane-plain') -WarningAction SilentlyContinue
if((ConvertFrom-Json $output)[0] -ne '--terminate'){throw 'Receipt failure suppressed cleanup'}
$receipt=Join-Path $root 'bounded.json'
foreach($iteration in 1..34){Invoke-WslLifecycleCommand -Receipt $receipt -Stage list -Arguments @('--list') | Out-Null}
$bounded=Get-Content -LiteralPath $receipt -Raw | ConvertFrom-Json
if($bounded.operations.Count -ne 32 -or -not $bounded.truncated -or (Get-Item -LiteralPath $receipt).Length -gt 16384){throw 'Lifecycle receipt was not bounded or truncation was hidden'}
Write-Host 'WSL lifecycle receipt checks passed: durable starts, native argv/stdout, exit failures, launch refusal, cleanup on receipt failure, bounded metadata'
} finally {
$env:ORCA_WSL_LIFECYCLE_TEST_EXIT=$originalExit
Remove-Item Function:wsl.exe -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}