mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
The package job builds three Docker images whose apt-get update/install hit archive.ubuntu.com with no retry, timeout, or mirror fallback. When the mirror is mid-sync every build dies in one of three ways: - per-package fetch stalls (~64 s each, `Ign:` lines) until the runner's 10-minute docker build timeout fires: https://github.com/stablyai/orca/actions/runs/33935104546/job/101221447425 - `apt-get update` exit 100 with `Hash Sum mismatch` on noble-updates/restricted/Packages.gz: https://github.com/stablyai/orca/actions/runs/33935104546/job/101226099525 - `apt-get update` exit 100 with `File has unexpected size ... Mirror sync in progress?`: https://github.com/stablyai/orca/actions/runs/33935244026/job/101231083497 Each Dockerfile now retries `apt-get update` up to five times with Acquire::Retries and a 30 s HTTP timeout, clearing /var/lib/apt/lists between attempts so a half-synced index is never reused, and passes the same acquire options to `apt-get install`. Each runner script retries the whole `docker build` once when the first attempt fails or times out.
271 lines
8.9 KiB
JavaScript
Executable File
271 lines
8.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Exercise packaged CLI paths under the hostile Linux conditions from #11609/#12530/#13719/#14229.
|
|
import { execFileSync } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
const commandArgs = process.argv.slice(2)
|
|
const appImageArg = valueAfter('--appimage')
|
|
const appImage = appImageArg ? resolve(appImageArg) : null
|
|
const platform = valueAfter('--platform')
|
|
const dockerPlatformArgs = platform ? ['--platform', platform] : []
|
|
|
|
const suffix = `${process.pid}-${Date.now()}`
|
|
const artifactVolume = `orca-cli-contract-artifact-${suffix}`
|
|
const tagArchitecture = platform?.split('/')[1] ?? process.arch
|
|
const tag = `orca-cli-launch-contract:ubuntu-24.04-${tagArchitecture}-${suffix}`
|
|
const base = 'ubuntu@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90'
|
|
const containers = new Set()
|
|
let artifactVolumeCreated = false
|
|
const CASE_TIMEOUT_MS = 90_000
|
|
const BUILD_TIMEOUT_MS = 10 * 60_000
|
|
const STAGING_TIMEOUT_MS = 5 * 60_000
|
|
const DOCKER_TIMEOUT_MS = 2 * 60_000
|
|
const CLEANUP_TIMEOUT_MS = 30_000
|
|
|
|
// Exact statuses reject silent no-op launches as well as crashes.
|
|
const CASES = [
|
|
{
|
|
name: 'nofuse-userns-bundled-help',
|
|
expectStatus: 0,
|
|
expectOutput: 'Usage: orca <command>',
|
|
why: 'The bundled launcher must run with no FUSE, no display, and userns restricted (#11609, #12530).'
|
|
},
|
|
{
|
|
name: 'nofuse-userns-bundled-version',
|
|
expectStatus: 0,
|
|
expectOutput: /^\d+\.\d+\.\d+/m,
|
|
why: 'A deployment must be able to read the installed version without a display (#13719).'
|
|
},
|
|
{
|
|
name: 'nofuse-userns-bundled-status',
|
|
// No runtime is running; the CLI must report that itself.
|
|
expectStatus: 1,
|
|
expectOutput: 'appRunning',
|
|
why: 'A command that needs the runtime must report its absence, not abort.'
|
|
},
|
|
{
|
|
name: 'nofuse-userns-bundled-skills',
|
|
expectStatus: 0,
|
|
// Why: the rendered help header, not a bare 'skills' — the case name contains that word.
|
|
expectOutput: 'Usage: orca skills',
|
|
why: 'skills is a pure-text command that must never need Chromium (#14229).'
|
|
},
|
|
{
|
|
name: 'nofuse-userns-bundled-worktree',
|
|
expectStatus: 1,
|
|
expectOutput: "Orca is not running. Run 'orca open' first.",
|
|
why: 'A runtime-dependent command must report the missing runtime, not abort.'
|
|
},
|
|
{
|
|
name: 'nofuse-nosandbox-direct-binary-skills',
|
|
expectStatus: 0,
|
|
expectOutput: 'Usage: orca skills',
|
|
why: 'A direct binary launch that reaches JavaScript must run the command, not boot a GUI (#14229).'
|
|
},
|
|
{
|
|
name: 'nofuse-nosandbox-direct-binary-gui',
|
|
// A missing display is an expected diagnosis, not a crash.
|
|
expectStatus: 1,
|
|
expectOutput: 'needs a usable display server',
|
|
why: 'A desktop launch with no display must diagnose it instead of dying in uv_close (#13719).'
|
|
},
|
|
{
|
|
name: 'stale-display-nosandbox-direct-binary-gui',
|
|
expectStatus: 1,
|
|
expectOutput: 'needs a usable display server',
|
|
why: 'A stale DISPLAY value must diagnose the unreachable endpoint instead of dying in uv_close (#13719).'
|
|
}
|
|
]
|
|
|
|
try {
|
|
if (!appImage) {
|
|
fail(
|
|
'Usage: run-linux-cli-launch-contract-docker.mjs --appimage /path/to/orca-linux.AppImage [--platform linux/amd64|linux/arm64]'
|
|
)
|
|
}
|
|
if (commandArgs.includes('--platform') && !platform) {
|
|
fail('Missing value for --platform')
|
|
}
|
|
if (platform !== null && platform !== 'linux/amd64' && platform !== 'linux/arm64') {
|
|
fail(`Unsupported --platform: ${platform}`)
|
|
}
|
|
if (!existsSync(appImage)) {
|
|
fail(`AppImage not found: ${appImage}`)
|
|
}
|
|
docker(['volume', 'create', artifactVolume], { timeoutMs: DOCKER_TIMEOUT_MS })
|
|
artifactVolumeCreated = true
|
|
buildImage()
|
|
stageArtifacts()
|
|
runContract()
|
|
console.log('\nLinux CLI launch contract passed.')
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : String(error))
|
|
process.exitCode = 1
|
|
} finally {
|
|
for (const container of containers) {
|
|
docker(['rm', '-f', container], { allowFailure: true, timeoutMs: CLEANUP_TIMEOUT_MS })
|
|
}
|
|
if (artifactVolumeCreated) {
|
|
docker(['volume', 'rm', artifactVolume], {
|
|
allowFailure: true,
|
|
timeoutMs: CLEANUP_TIMEOUT_MS
|
|
})
|
|
}
|
|
docker(['image', 'rm', tag], { allowFailure: true, timeoutMs: CLEANUP_TIMEOUT_MS })
|
|
}
|
|
|
|
function runContract() {
|
|
const failures = []
|
|
for (const testCase of CASES) {
|
|
const output = runCase(testCase.name)
|
|
const statusMatch = /^RESULT status=(\d+)/m.exec(output)
|
|
if (!statusMatch) {
|
|
failures.push(`${testCase.name}: ${firstLine(output)}\n ${testCase.why}`)
|
|
console.log(` FAIL ${testCase.name} — ${firstLine(output)}`)
|
|
continue
|
|
}
|
|
const status = Number(statusMatch[1])
|
|
// Why: the harness echoes `RESULT status=N case=<name>`, so a case whose name contains the
|
|
// expected substring would assert against the harness's own line instead of the CLI's output.
|
|
const commandOutput = output
|
|
.split('\n')
|
|
.filter((line) => !/^(?:RESULT|CRASHED|PRECONDITION_FAILED) /.test(line))
|
|
.join('\n')
|
|
const matchesOutput =
|
|
typeof testCase.expectOutput === 'string'
|
|
? commandOutput.includes(testCase.expectOutput)
|
|
: testCase.expectOutput.test(commandOutput)
|
|
if (status !== testCase.expectStatus || !matchesOutput) {
|
|
failures.push(
|
|
`${testCase.name}: expected status ${testCase.expectStatus} and ${testCase.expectOutput}, ` +
|
|
`got status ${status}\n ${testCase.why}`
|
|
)
|
|
console.log(` FAIL ${testCase.name} — status ${status}`)
|
|
continue
|
|
}
|
|
console.log(` ok ${testCase.name} (status ${status})`)
|
|
}
|
|
if (failures.length > 0) {
|
|
fail(`Linux CLI launch contract failed:\n - ${failures.join('\n - ')}`)
|
|
}
|
|
}
|
|
|
|
function runCase(caseName) {
|
|
const container = `orca-cli-contract-${caseName}-${suffix}`
|
|
containers.add(container)
|
|
// FUSE and extra capabilities would invalidate the test conditions.
|
|
return docker(
|
|
[
|
|
'run',
|
|
...dockerPlatformArgs,
|
|
'--name',
|
|
container,
|
|
'--rm',
|
|
'-v',
|
|
`${artifactVolume}:/artifacts`,
|
|
tag,
|
|
caseName
|
|
],
|
|
{ allowFailure: true, capture: true, timeoutMs: CASE_TIMEOUT_MS }
|
|
)
|
|
}
|
|
|
|
function buildImage() {
|
|
console.log(`Building ${tag}…`)
|
|
const buildArgs = [
|
|
'build',
|
|
...dockerPlatformArgs,
|
|
'--build-arg',
|
|
`BASE_IMAGE=${base}`,
|
|
'-f',
|
|
'config/docker/cli-launch-contract/Dockerfile',
|
|
'-t',
|
|
tag,
|
|
'config/docker/cli-launch-contract'
|
|
]
|
|
// Why: apt fetches from archive.ubuntu.com stall or fail mid-sync; a second build usually lands on a healthy index.
|
|
try {
|
|
docker(buildArgs, { timeoutMs: BUILD_TIMEOUT_MS })
|
|
} catch (error) {
|
|
console.error(
|
|
`${error instanceof Error ? error.message : String(error)}\nRetrying docker build once…`
|
|
)
|
|
docker(buildArgs, { timeoutMs: BUILD_TIMEOUT_MS })
|
|
}
|
|
}
|
|
|
|
// Extract unprivileged so chrome-sandbox is not root-owned setuid.
|
|
function stageArtifacts() {
|
|
console.log('Staging the AppImage payload…')
|
|
const container = `orca-cli-contract-stage-${suffix}`
|
|
containers.add(container)
|
|
docker(
|
|
[
|
|
'run',
|
|
...dockerPlatformArgs,
|
|
'--name',
|
|
container,
|
|
'--rm',
|
|
'-v',
|
|
`${artifactVolume}:/artifacts`,
|
|
'-v',
|
|
`${appImage}:/input/orca-linux.AppImage:ro`,
|
|
'--entrypoint',
|
|
'bash',
|
|
tag,
|
|
'-lc',
|
|
[
|
|
'set -euo pipefail',
|
|
'cp /input/orca-linux.AppImage /artifacts/orca-linux.AppImage',
|
|
'chmod +x /artifacts/orca-linux.AppImage',
|
|
'chown -R orca:orca /artifacts',
|
|
// Use the AppImage runtime's no-FUSE extraction path.
|
|
'cd /artifacts && runuser --user orca -- ./orca-linux.AppImage --appimage-extract >/dev/null',
|
|
'test -x /artifacts/squashfs-root/resources/bin/orca-ide'
|
|
].join(' && ')
|
|
],
|
|
{ timeoutMs: STAGING_TIMEOUT_MS }
|
|
)
|
|
}
|
|
|
|
function docker(args, options = {}) {
|
|
try {
|
|
const output = execFileSync('docker', args, {
|
|
encoding: 'utf8',
|
|
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
timeout: options.timeoutMs ?? DOCKER_TIMEOUT_MS,
|
|
killSignal: 'SIGTERM'
|
|
})
|
|
return output ?? ''
|
|
} catch (error) {
|
|
const timedOut = error instanceof Error && 'code' in error && error.code === 'ETIMEDOUT'
|
|
if (timedOut) {
|
|
const message = `docker ${args.join(' ')} timed out after ${options.timeoutMs ?? DOCKER_TIMEOUT_MS}ms`
|
|
if (!options.allowFailure) {
|
|
fail(message)
|
|
}
|
|
return message
|
|
}
|
|
if (!options.allowFailure) {
|
|
fail(
|
|
`docker ${args.join(' ')} failed: ${error instanceof Error ? error.message : String(error)}`
|
|
)
|
|
}
|
|
return `${error?.stdout ?? ''}${error?.stderr ?? ''}`
|
|
}
|
|
}
|
|
|
|
function firstLine(value) {
|
|
return (value ?? '').trim().split('\n')[0] || '(no output)'
|
|
}
|
|
|
|
function valueAfter(flag) {
|
|
const index = commandArgs.indexOf(flag)
|
|
return index === -1 ? null : (commandArgs[index + 1] ?? null)
|
|
}
|
|
|
|
function fail(message) {
|
|
throw new Error(message)
|
|
}
|