ci: reuse immutable package setup across shutdown checks (#20368)

This commit is contained in:
Neil
2026-09-12 01:23:05 -07:00
committed by GitHub
parent 7b53b5abd1
commit de8e5421ff
4 changed files with 204 additions and 67 deletions
+1 -10
View File
@@ -775,18 +775,9 @@ jobs:
[[ "$rpm_marker" == rpm ]] || { echo "Expected rpm marker, got: $rpm_marker"; exit 1; }
- name: Verify headless serve signal shutdown
run: node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage
- name: Verify extracted launcher serve signal shutdown
run: >-
node config/scripts/run-headless-serve-shutdown-docker.mjs
--appimage dist/orca-linux.AppImage --entrypoint launcher
- name: Verify AppImage CLI registration and serve signal shutdown
run: >-
node config/scripts/run-headless-serve-shutdown-docker.mjs
--appimage dist/orca-linux.AppImage --entrypoint appimage
--signal-target serving-electron --int-delivery pid
--appimage dist/orca-linux.AppImage --all-entrypoints
# A default container reproduces the hostile AppImage launch environment.
- name: Verify Linux CLI launch contract
@@ -0,0 +1,140 @@
import { createHash } from 'node:crypto'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { spawnSync } = vi.hoisted(() => ({ spawnSync: vi.fn() }))
vi.mock('node:child_process', () => ({ spawnSync }))
let directory
let artifact
let originalArgv
let originalExitCode
const commands = () => spawnSync.mock.calls.map(([, args]) => args)
const signalRuns = () => commands().filter((args) => ['INT', 'TERM'].includes(args.at(-1)))
const succeeded = { status: 0, stdout: '', stderr: '' }
async function run(...options) {
process.argv = ['node', 'runner', '--appimage', artifact, ...options]
await import('./run-headless-serve-shutdown-docker.mjs')
}
beforeEach(() => {
vi.resetModules()
spawnSync.mockReset().mockReturnValue(succeeded)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
directory = mkdtempSync(join(tmpdir(), 'orca-shutdown-matrix-'))
artifact = join(directory, 'original.AppImage')
writeFileSync(artifact, 'original package bytes')
originalArgv = process.argv
originalExitCode = process.exitCode
})
afterEach(() => {
process.argv = originalArgv
process.exitCode = originalExitCode
rmSync(directory, { recursive: true, force: true })
vi.restoreAllMocks()
})
describe('packaged shutdown matrix', () => {
it('shares extraction but isolates every entrypoint and signal', async () => {
await run('--all-entrypoints')
expect(commands().filter((args) => args[0] === 'build')).toHaveLength(1)
const startup = commands().filter((args) =>
args.includes('/usr/local/bin/run-appimage-desktop-startup-case')
)
const extraction = commands().filter((args) =>
args.some((arg) => arg.includes('120s /input/orca.AppImage --appimage-extract'))
)
expect(startup).toHaveLength(1)
expect(extraction).toHaveLength(1)
expect(commands().indexOf(startup[0])).toBeLessThan(commands().indexOf(extraction[0]))
expect(signalRuns()).toHaveLength(6)
const names = new Set()
for (const [index, args] of signalRuns().entries()) {
const entrypoint = ['app', 'launcher', 'appimage'][Math.floor(index / 2)]
expect(args).toContain(`ORCA_TEST_ENTRYPOINT=${entrypoint}`)
expect(args).toContain(
`ORCA_SIGNAL_TARGET=${entrypoint === 'appimage' ? 'serving-electron' : 'app'}`
)
expect(args).toContain(
`ORCA_INT_DELIVERY=${entrypoint === 'appimage' ? 'pid' : 'foreground-process-group'}`
)
expect(args.at(-1)).toBe(index % 2 === 0 ? 'INT' : 'TERM')
expect(args).toContain(`${artifact}:/input/orca.AppImage:ro`)
expect(args.some((arg) => arg.endsWith(':/artifacts:ro'))).toBe(true)
expect(args).toContain('--rm')
names.add(args[args.indexOf('--name') + 1])
}
expect(names.size).toBe(6)
const evidence = console.log.mock.calls
.map(([line]) => line)
.filter((line) => line.startsWith('{'))
.map(JSON.parse)
expect(evidence).toHaveLength(3)
expect(
evidence.every(
(entry) =>
entry.sha256 === createHash('sha256').update('original package bytes').digest('hex')
)
).toBe(true)
expect(
commands()
.slice(-2)
.map((args) => args.slice(0, 2))
).toEqual([
['volume', 'rm'],
['image', 'rm']
])
})
it('attributes failures and still attempts later cases before cleanup', async () => {
spawnSync.mockImplementation((_, args) =>
args.at(-1) === 'INT' ? { ...succeeded, status: 7 } : succeeded
)
await expect(run('--all-entrypoints')).rejects.toThrow(
'app:INT:7, launcher:INT:7, appimage:INT:7'
)
expect(signalRuns()).toHaveLength(6)
expect(commands().at(-2).slice(0, 2)).toEqual(['volume', 'rm'])
})
it('cleans setup resources without running cases after failed extraction', async () => {
spawnSync.mockImplementation((_, args) =>
args.some((arg) => arg.includes('120s /input/orca.AppImage --appimage-extract'))
? { ...succeeded, status: 9 }
: succeeded
)
await expect(run('--all-entrypoints')).rejects.toThrow('docker run failed')
expect(signalRuns()).toHaveLength(0)
expect(
commands()
.slice(-2)
.map((args) => args.slice(0, 2))
).toEqual([
['volume', 'rm'],
['image', 'rm']
])
})
it('preserves individual launcher overlay invocations', async () => {
await run('--entrypoint', 'launcher', '--launcher-exec-overlay')
expect(signalRuns()).toHaveLength(2)
expect(signalRuns().every((args) => args.includes('ORCA_TEST_ENTRYPOINT=launcher'))).toBe(true)
expect(
commands().some((args) =>
args.some((arg) => arg.includes("sed -i 's/^ELECTRON_RUN_AS_NODE=1"))
)
).toBe(true)
})
it('rejects ambiguous matrix overrides before invoking Docker', async () => {
await expect(run('--all-entrypoints', '--entrypoint', 'launcher')).rejects.toThrow(
'cannot be combined'
)
expect(spawnSync).not.toHaveBeenCalled()
})
})
@@ -56,12 +56,6 @@ describe('headless serve shutdown PR gate', () => {
const packageStep = steps.find((step) => step.name === 'Package unpacked app')
const markerStep = steps.find((step) => step.name === 'Verify root-package marker payloads')
const shutdownStep = steps.find((step) => step.name === 'Verify headless serve signal shutdown')
const launcherShutdownStep = steps.find(
(step) => step.name === 'Verify extracted launcher serve signal shutdown'
)
const appImageShutdownStep = steps.find(
(step) => step.name === 'Verify AppImage CLI registration and serve signal shutdown'
)
expect(workflow.jobs.package['timeout-minutes']).toBe(90)
expect(packageStep.run).toContain('--linux AppImage deb rpm --x64 --publish never')
@@ -69,19 +63,13 @@ describe('headless serve shutdown PR gate', () => {
expect(markerStep.run).toContain('rpm2cpio')
expect(steps.indexOf(markerStep)).toBeGreaterThan(steps.indexOf(packageStep))
expect(shutdownStep.run).toBe(
'node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage'
'node config/scripts/run-headless-serve-shutdown-docker.mjs --appimage dist/orca-linux.AppImage --all-entrypoints'
)
expect(launcherShutdownStep.run).toContain(
'node config/scripts/run-headless-serve-shutdown-docker.mjs'
)
expect(launcherShutdownStep.run).toContain('--entrypoint launcher')
expect(appImageShutdownStep.run).toContain('--entrypoint appimage')
expect(appImageShutdownStep.run).toContain('--signal-target serving-electron')
expect(appImageShutdownStep.run).toContain('--int-delivery pid')
expect(steps.indexOf(shutdownStep)).toBeGreaterThan(steps.indexOf(packageStep))
expect(steps.indexOf(shutdownStep)).toBeGreaterThan(steps.indexOf(markerStep))
expect(steps.indexOf(launcherShutdownStep)).toBeGreaterThan(steps.indexOf(shutdownStep))
expect(steps.indexOf(appImageShutdownStep)).toBeGreaterThan(steps.indexOf(launcherShutdownStep))
expect(
steps.filter((step) => step.run?.includes('run-headless-serve-shutdown-docker.mjs'))
).toHaveLength(1)
})
it('keeps readiness polling finite and leak-free', () => {
@@ -11,6 +11,22 @@ const signalTarget = valueAfter('--signal-target') ?? 'app'
const entrypoint = valueAfter('--entrypoint') ?? 'app'
const intDelivery = valueAfter('--int-delivery') ?? 'foreground-process-group'
const launcherExecOverlay = args.includes('--launcher-exec-overlay')
const allEntrypoints = args.includes('--all-entrypoints')
if (
allEntrypoints &&
['--entrypoint', '--signal-target', '--int-delivery', '--launcher-exec-overlay'].some((flag) =>
args.includes(flag)
)
) {
fail('--all-entrypoints cannot be combined with individual case options')
}
const cases = allEntrypoints
? [
{ entrypoint: 'app', signalTarget: 'app', intDelivery: 'foreground-process-group' },
{ entrypoint: 'launcher', signalTarget: 'app', intDelivery: 'foreground-process-group' },
{ entrypoint: 'appimage', signalTarget: 'serving-electron', intDelivery: 'pid' }
]
: [{ entrypoint, signalTarget, intDelivery }]
if (!appImageArg) {
fail('Usage: run-headless-serve-shutdown-docker.mjs --appimage /path/to/orca.AppImage')
}
@@ -98,50 +114,52 @@ try {
].join(' && ')
])
console.log(
JSON.stringify({
type: 'appimage_under_test',
appImage,
sha256,
platform,
signalTarget,
entrypoint,
intDelivery,
launcherExecOverlay
})
)
const failedSignals = []
for (const signal of ['INT', 'TERM']) {
const result = docker(
[
'run',
'--rm',
'--init',
'--platform',
for (const { entrypoint, signalTarget, intDelivery } of cases) {
console.log(
JSON.stringify({
type: 'appimage_under_test',
appImage,
sha256,
platform,
'--shm-size',
'256m',
'--name',
`orca-headless-serve-shutdown-${signal.toLowerCase()}-${suffix}`,
'-e',
`ORCA_SIGNAL_TARGET=${signalTarget}`,
'-e',
`ORCA_TEST_ENTRYPOINT=${entrypoint}`,
'-e',
`ORCA_INT_DELIVERY=${intDelivery}`,
'-v',
`${appImage}:/input/orca.AppImage:ro`,
'-v',
`${artifactVolume}:/artifacts:ro`,
image,
signal
],
{ allowFailure: true }
signalTarget,
entrypoint,
intDelivery,
launcherExecOverlay
})
)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.status !== 0) {
failedSignals.push(`${signal}:${result.status}`)
for (const signal of ['INT', 'TERM']) {
const result = docker(
[
'run',
'--rm',
'--init',
'--platform',
platform,
'--shm-size',
'256m',
'--name',
`orca-headless-serve-shutdown-${entrypoint}-${signal.toLowerCase()}-${suffix}`,
'-e',
`ORCA_SIGNAL_TARGET=${signalTarget}`,
'-e',
`ORCA_TEST_ENTRYPOINT=${entrypoint}`,
'-e',
`ORCA_INT_DELIVERY=${intDelivery}`,
'-v',
`${appImage}:/input/orca.AppImage:ro`,
'-v',
`${artifactVolume}:/artifacts:ro`,
image,
signal
],
{ allowFailure: true }
)
process.stdout.write(result.stdout)
process.stderr.write(result.stderr)
if (result.status !== 0) {
failedSignals.push(`${entrypoint}:${signal}:${result.status}`)
}
}
}
if (failedSignals.length > 0) {