fix(release): Windows x64 MSIX smoke runs on a GitHub-hosted runner and reports why a launch dies

Every Windows x64 MSIX chat smoke since the Blacksmith migration (91fae5f8cb)
failed: first on the GitHub-hosted guard, and since that guard was removed
(272adc7b6f) with Playwright's "Process failed to launch!", on Electron 40 and
44 alike. The same builds' arm64 MSIX, on a GitHub-hosted runner, chats; the
last green x64 smoke (Oct 14) ran on windows-latest-32-core. Move the x64 leg
back there, as f1b290e773 did for the install/update GUI legs.

The failure itself carried no evidence: when the app exits before its debugger
line, Playwright's Electron.launch leaves sibling stderr-watcher rejections
unhandled, Node dies on them, and the smoke's catch never writes its receipt or
Playwright's call log. The driver now logs those rejections (still failing the
run), so the receipt records the app's exit code and stderr, and the Windows
smoke saves Application-log errors from the run on failure.
This commit is contained in:
Teknium committed 2026-10-22 18:59:52 -07:00
1 parent 907e3224c4
commit 3ba2b0787e
4 files changed
+57 -1

No files matched your search

+4 -1
View File
@@ -245,7 +245,10 @@ jobs:
name: Windows install and chat
needs: validate
if: inputs.platform == 'win32'
runs-on: ${{ inputs.arch == 'arm64' && 'windows-latest-32-arm-core' || 'blacksmith-32vcpu-windows-2025' }}
# GitHub-hosted for both arches: on blacksmith Windows runners the installed MSIX app exits
# before Playwright attaches (every x64 smoke there failed; the same commit's arm64 MSIX on a
# GitHub-hosted runner chats), as the install/update GUI legs did (f1b290e7734).
runs-on: ${{ inputs.arch == 'arm64' && 'windows-latest-32-arm-core' || 'windows-latest-32-core' }}
# windows-11-arm runners are much slower (npm ci alone runs 12+ minutes and
# the MSIX leg exceeds 30 total); x64 legs complete in ~14.
timeout-minutes: 45
@@ -0,0 +1,37 @@
import { spawnSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { expect, test } from 'vitest'
const driver = path.resolve(import.meta.dirname, '../../tests/install/e2e-assets/desktop-smoke.ts')
test('an app that dies before Playwright attaches leaves a failed receipt carrying its exit code and stderr', (): void => {
const work = fs.mkdtempSync(path.join(os.tmpdir(), 'desktop-smoke-launch-'))
try {
const dir = (name: string): string => {
const target = path.join(work, name)
fs.mkdirSync(target)
return target
}
const out = path.join(work, 'out')
// Node as the "app": it rejects Playwright's Chromium switches and exits before any debugger line.
const result = spawnSync(process.execPath, [driver, '--exe', process.execPath, '--root', dir('root'), '--origin', 'source',
'--home', dir('home'), '--user-data', dir('user-data'), '--out', out, '--phase', 'installed',
'--expect-commit', '0'.repeat(40)], { cwd: work, encoding: 'utf8', timeout: 60_000 })
expect(result.status).toBe(1)
const receipt = JSON.parse(fs.readFileSync(path.join(out, 'desktop-chat-installed.json'), 'utf8'))
expect(receipt.status).toBe('failed')
expect(receipt.error).toContain('Process failed to launch!')
expect(receipt.error).toMatch(/<process did exit: exitCode=[1-9]\d*/)
expect(receipt.error).toMatch(/\]\[err\] .*bad option: --no-sandbox/)
} finally {
fs.rmSync(work, { recursive: true, force: true })
}
})
+10
View File
@@ -384,6 +384,16 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
'user-data': { type: 'string' }, out: { type: 'string' }, phase: { type: 'string' },
'expect-commit': { type: 'string' }, 'launch-spec': { type: 'string' }, 'mock-url': { type: 'string' },
} })
// When the app exits before printing its debugger line, Playwright's Electron.launch rejects
// the awaited watcher and leaves its sibling stderr watchers' rejections unhandled. Without a
// listener Node dies on those first, so the catch below never writes the failed receipt or the
// launch call log (the app's exit code and stderr). Still fail the run.
process.on('unhandledRejection', (reason: unknown): void => {
console.error(`unhandled rejection: ${redact(String(reason))}`)
process.exitCode = 1
})
try {
await runInstalledDesktopSmoke(optionsSchema.parse(values))
} catch (error) {
+6
View File
@@ -42,6 +42,7 @@ Start-Transcript -Path (Join-Path $Out 'native-install.log') | Out-Null
$attempted = $false
$installedFullName = $null
$failed = $false
$smokeStart = Get-Date
try {
$beforeHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Artifact).Hash.ToLowerInvariant()
@{ path=$Artifact; sha256=$beforeHash } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $Out 'artifact.json')
@@ -95,6 +96,11 @@ try {
} catch {
$failed = $true
$_ | Out-String | Set-Content -LiteralPath (Join-Path $Out 'native-install-error.txt')
# An app that dies before Playwright attaches leaves its faulting module and exception code
# (Application Error / Windows Error Reporting / SideBySide) only in the Application log.
Get-WinEvent -FilterHashtable @{ LogName='Application'; Level=1,2; StartTime=$smokeStart } -ErrorAction SilentlyContinue |
Format-List TimeCreated, Id, ProviderName, Message | Out-String -Width 4096 |
Set-Content -LiteralPath (Join-Path $Out 'native-app-error-events.txt')
Write-Error $_ -ErrorAction Continue
} finally {
try {