8080 - name : Test integration harness helpers
8181 run : npm run test:integration-harness
8282
83+ - name : Test workflow and CLI packaging guards
84+ run : npm run test:github-actions
85+
8386 - name : Typecheck client
8487 run : npm run --prefix packages/client typecheck
8588
@@ -289,12 +292,16 @@ jobs:
289292 SIMDECK_INTEGRATION_DEVICE_TYPE : iPhone SE (3rd generation)
290293
291294 integration-android :
292- name : Android emulator integration
293- runs-on : ubuntu-latest
294- timeout-minutes : 35
295+ name : Android emulator integration (${{ matrix.os }})
296+ runs-on : ${{ matrix.os }}
297+ timeout-minutes : 65
295298 needs :
296299 - client
297300 - packages
301+ strategy :
302+ fail-fast : false
303+ matrix :
304+ os : [ubuntu-latest, windows-latest]
298305
299306 steps :
300307 - uses : actions/checkout@v4
@@ -308,6 +315,7 @@ jobs:
308315 - uses : dtolnay/rust-toolchain@stable
309316
310317 - name : Enable KVM access
318+ if : runner.os == 'Linux'
311319 run : |
312320 echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
313321 sudo udevadm control --reload-rules
@@ -316,12 +324,13 @@ jobs:
316324 - name : Install root dependencies
317325 run : npm ci --ignore-scripts --force
318326
319- - name : Build Linux Android integration artifacts
327+ - name : Build Android integration artifacts
320328 run : |
321329 npm run build:cli
322330 npm run build:simdeck-test
323331
324- - name : Android emulator integration tests
332+ - name : Android emulator integration tests (Linux)
333+ if : runner.os == 'Linux'
325334 uses : reactivecircus/android-emulator-runner@v2
326335 with :
327336 api-level : 35
@@ -330,8 +339,182 @@ jobs:
330339 profile : pixel_6
331340 avd-name : SimDeck_Pixel_CI
332341 disable-animations : true
342+ emulator-options : -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect -grpc 8554
333343 script : npm run test:integration:android
334344 env :
335345 SIMDECK_INTEGRATION_ANDROID_AVD : SimDeck_Pixel_CI
336346 SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID : " 1"
337347 SIMDECK_INTEGRATION_VERBOSE : " 1"
348+
349+ - name : Create, boot, and test Android emulator (Windows)
350+ if : runner.os == 'Windows'
351+ shell : pwsh
352+ timeout-minutes : 45
353+ run : |
354+ $ErrorActionPreference = "Stop"
355+ $sdk = $env:ANDROID_HOME
356+ if (-not $sdk) {
357+ $sdk = $env:ANDROID_SDK_ROOT
358+ }
359+ if (-not $sdk) {
360+ $sdk = Join-Path $env:LOCALAPPDATA "Android\Sdk"
361+ }
362+ $env:ANDROID_HOME = $sdk
363+ $env:ANDROID_SDK_ROOT = $sdk
364+ $cmdlineTools = Get-ChildItem -Path (Join-Path $sdk "cmdline-tools") -Directory -ErrorAction SilentlyContinue |
365+ Sort-Object Name -Descending |
366+ Select-Object -First 1
367+ $toolsBin = if (Test-Path (Join-Path $sdk "cmdline-tools\latest\bin")) {
368+ Join-Path $sdk "cmdline-tools\latest\bin"
369+ } elseif ($cmdlineTools) {
370+ Join-Path $cmdlineTools.FullName "bin"
371+ } else {
372+ Join-Path $sdk "tools\bin"
373+ }
374+ $sdkmanager = Join-Path $toolsBin "sdkmanager.bat"
375+ $avdmanager = Join-Path $toolsBin "avdmanager.bat"
376+ $adb = Join-Path $sdk "platform-tools\adb.exe"
377+ $emulator = Join-Path $sdk "emulator\emulator.exe"
378+ $windowsApi = "35"
379+ $windowsPlatform = "platforms;android-$windowsApi"
380+ $windowsSystemImage = "system-images;android-$windowsApi;google_atd;x86_64"
381+
382+ $yesFile = Join-Path $env:RUNNER_TEMP "android-sdk-yes.txt"
383+ 1..200 | ForEach-Object { "y" } | Set-Content -Path $yesFile -Encoding ascii
384+ $noFile = Join-Path $env:RUNNER_TEMP "android-avd-no.txt"
385+ "no" | Set-Content -Path $noFile -Encoding ascii
386+
387+ function Invoke-AndroidToolWithInput($tool, $arguments, $inputFile) {
388+ $line = "`"$tool`" $arguments < `"$inputFile`""
389+ Write-Host "cmd /c $line"
390+ cmd /c $line
391+ if ($LASTEXITCODE -ne 0) {
392+ throw "$tool $arguments failed with exit code $LASTEXITCODE."
393+ }
394+ }
395+
396+ Write-Host "Accepting Android SDK licenses"
397+ Invoke-AndroidToolWithInput $sdkmanager "--licenses" $yesFile
398+ Write-Host "Installing Android SDK emulator packages"
399+ Invoke-AndroidToolWithInput $sdkmanager "--install `"platform-tools`" `"emulator`" `"$windowsPlatform`" `"$windowsSystemImage`"" $yesFile
400+ Write-Host "Checking Android emulator acceleration"
401+ & $emulator -accel-check
402+ $accelSupported = $LASTEXITCODE -eq 0
403+ if (-not $accelSupported) {
404+ Write-Host "Hosted Windows runner did not report VM acceleration; forcing software acceleration for the CI smoke emulator."
405+ }
406+ Write-Host "Creating Android AVD"
407+ Invoke-AndroidToolWithInput $avdmanager "create avd --force --name SimDeck_Pixel_CI --package `"$windowsSystemImage`" --device `"pixel_6`"" $noFile
408+
409+ $stdout = Join-Path $env:RUNNER_TEMP "simdeck-android-emulator.out.log"
410+ $stderr = Join-Path $env:RUNNER_TEMP "simdeck-android-emulator.err.log"
411+ $serial = "emulator-5554"
412+ $args = @(
413+ "-avd", "SimDeck_Pixel_CI",
414+ "-qt-hide-window",
415+ "-no-audio",
416+ "-no-boot-anim",
417+ "-no-snapshot-load",
418+ "-no-snapshot-save",
419+ "-wipe-data",
420+ "-gpu", "swiftshader_indirect",
421+ "-feature", "-Vulkan",
422+ "-grpc", "8554",
423+ "-port", "5554",
424+ "-no-metrics",
425+ "-skip-adb-auth",
426+ "-camera-back", "none",
427+ "-camera-front", "none",
428+ "-cores", "2",
429+ "-memory", "2048",
430+ "-verbose"
431+ )
432+ if ($accelSupported) {
433+ $args += @("-accel", "on")
434+ } else {
435+ $args += @("-accel", "off")
436+ }
437+ function Write-EmulatorDiagnostics {
438+ Write-Host "adb devices:"
439+ & $adb devices -l
440+ if (Test-Path $stdout) {
441+ Write-Host "emulator stdout tail:"
442+ Get-Content $stdout -Tail 80
443+ }
444+ if (Test-Path $stderr) {
445+ Write-Host "emulator stderr tail:"
446+ Get-Content $stderr -Tail 120
447+ }
448+ }
449+ Write-Host "Starting Android emulator"
450+ $process = Start-Process -FilePath $emulator -ArgumentList $args -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
451+ $process.Id | Out-File -FilePath emulator.pid -Encoding ascii
452+ $deviceDeadline = (Get-Date).AddMinutes(10)
453+ $deviceSeen = $false
454+ do {
455+ if ($process.HasExited) {
456+ Write-EmulatorDiagnostics
457+ throw "Android emulator exited early with code $($process.ExitCode)."
458+ }
459+ $devices = (& $adb devices)
460+ if ($devices -match "$serial\s+device") {
461+ $deviceSeen = $true
462+ break
463+ }
464+ Start-Sleep -Seconds 5
465+ } while ((Get-Date) -lt $deviceDeadline)
466+ if (-not $deviceSeen) {
467+ Write-EmulatorDiagnostics
468+ throw "Android emulator did not appear in adb before the timeout."
469+ }
470+ $deadline = (Get-Date).AddMinutes(20)
471+ do {
472+ if ($process.HasExited) {
473+ Write-EmulatorDiagnostics
474+ throw "Android emulator exited early with code $($process.ExitCode)."
475+ }
476+ $booted = (& $adb -s $serial shell getprop sys.boot_completed 2>$null | Out-String).Trim()
477+ if ($booted -eq "1") {
478+ break
479+ }
480+ Start-Sleep -Seconds 5
481+ } while ((Get-Date) -lt $deadline)
482+ if ($booted -ne "1") {
483+ Write-EmulatorDiagnostics
484+ throw "Android emulator did not boot before the timeout."
485+ }
486+ & $adb -s $serial shell settings put global window_animation_scale 0
487+ & $adb -s $serial shell settings put global transition_animation_scale 0
488+ & $adb -s $serial shell settings put global animator_duration_scale 0
489+ $env:SIMDECK_INTEGRATION_ANDROID_AVD = "SimDeck_Pixel_CI"
490+ $env:SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID = "1"
491+ $env:SIMDECK_INTEGRATION_VERBOSE = "1"
492+ npm run test:integration:android
493+ $testExitCode = $LASTEXITCODE
494+ if ($testExitCode -ne 0) {
495+ Write-EmulatorDiagnostics
496+ throw "Android integration tests failed with exit code $testExitCode."
497+ }
498+
499+ - name : Stop Android emulator (Windows)
500+ if : always() && runner.os == 'Windows'
501+ shell : pwsh
502+ run : |
503+ $sdk = $env:ANDROID_HOME
504+ if (-not $sdk) {
505+ $sdk = $env:ANDROID_SDK_ROOT
506+ }
507+ if ($sdk) {
508+ $adb = Join-Path $sdk "platform-tools\adb.exe"
509+ if (Test-Path $adb) {
510+ & $adb emu kill
511+ if ($LASTEXITCODE -ne 0) {
512+ Write-Host "No Android emulator accepted adb emu kill."
513+ $global:LASTEXITCODE = 0
514+ }
515+ }
516+ }
517+ if (Test-Path emulator.pid) {
518+ Stop-Process -Id (Get-Content emulator.pid) -Force -ErrorAction SilentlyContinue
519+ }
520+ exit 0
0 commit comments