mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
ci: record durable WSL lifecycle operations (#32603)
This commit is contained in:
@@ -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/**'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user