mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
ci: reuse immutable package setup across shutdown checks (#20368)
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user