Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5ec19d5
Run headless Orca with verified bundled Bun and SQLite workers
Sep 24, 2026
4b0b567
Fix cross-runtime integer safety, spawn cancellation and CI contracts
Sep 24, 2026
44bb9b9
Preserve bundled template dependencies and finish CI contract fixes
Sep 24, 2026
a6095dc
Qualify static and musl template payloads in Linux packaging
Sep 24, 2026
c4b27a9
Enforce SQLite foreign keys and qualify native Bun artifacts in CI
Sep 24, 2026
0a9f8eb
fix: extract Bun archives reliably on Node 24.16
Sep 24, 2026
8b571db
fix: release remote runtime install locks on every exit
Sep 24, 2026
9717db4
fix: refuse incomplete bundled runtime handoff
Sep 24, 2026
f84c4fe
fix: harden Bun shutdown, terminal flow control, and runtime deployment
Sep 25, 2026
9f15c38
fix: support BusyBox terminal process discovery
Sep 25, 2026
3508d49
fix: recover interrupted Bun terminal resumes
Sep 25, 2026
af42ae8
fix: confirm shell suspension before pausing terminal jobs
Sep 25, 2026
463b946
test: synchronize the Bun terminal fallback fixture
Sep 25, 2026
0fa0ace
test: tolerate delayed Windows fixture handle release
Sep 25, 2026
7ef4d09
fix: integrate bundled search with Bun artifact verification
Sep 25, 2026
b874f40
test: qualify headless shutdown and admission on every Bun target
Sep 25, 2026
6617441
Harden Bun terminal fallback and producer pause recovery
Sep 25, 2026
723b474
fix: preserve Bun terminal jobs and supervised runtime lifetime
Sep 25, 2026
4b3bd68
test: make Bun portability regressions work across process tables
Sep 25, 2026
e117ddc
fix: let bundled Windows runtimes drain after launcher loss
Sep 25, 2026
4f785cd
test: qualify Bun against current Git Bash shell inspection
Sep 25, 2026
fe65c6d
test: resolve the Bun gate in source-level Git Bash qualification
Sep 25, 2026
46befc2
test: wait for the foreground command before interrupting Git Bash
Sep 25, 2026
43bfec8
test: reuse Windows transient-lock cleanup for Bun launchers
Sep 25, 2026
1db09e0
fix: preserve Windows Bun terminals through foreground interrupts
Sep 25, 2026
c7af9fa
test: tolerate Windows PTY artifact cleanup locks
Sep 25, 2026
5a2d926
test: wait for inserted Markdown images to load
Sep 25, 2026
9e1134e
fix: degrade optional Bun probes and inspect fallback terminals
Sep 25, 2026
46f9851
test: preserve held-backup coverage on the Bun stack
Sep 25, 2026
3c9c674
test(mobile): hold pending frames across terminal disposal
Sep 26, 2026
825dfc9
fix(pty): hang up interactive Bun shells during graceful shutdown
Sep 26, 2026
5c5357d
test(pty): drain initial flood output before measuring producer pause
Sep 26, 2026
b913cd9
test(sqlite): coordinate concurrent backup writer startup
Sep 26, 2026
239c9ba
test(pty): observe native shell cleanup without replacing hangup hand…
Sep 26, 2026
4bba5e7
test(pty): reap shell jobs before fixture shutdown
Sep 26, 2026
19d4489
refactor(bun): remove duplicate transition and cache bookkeeping
Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/bun-profile-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: Bun profile persistence

on:
pull_request:
paths:
- 'src/main/persistence/**'
- 'src/main/sqlite/**'
- 'src/main/worker-thread-entry-path.ts'
- 'src/main/orcad/**'
- 'src/main/daemon/pty-subprocess/**'
- 'src/main/providers/**'
- 'src/shared/**'
- 'config/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/actions/install-node-dependencies/**'
- '.github/workflows/bun-profile-tests.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: bun-profile-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
persistence:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04-arm, macos-14, macos-15-intel, windows-2022, windows-11-arm]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
ORCA_BACKGROUND_LAUNCH: '1'
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- name: Build the native Windows process reader
if: runner.os == 'Windows'
run: node config/scripts/build-windows-process-tree-relay-addon.mjs
- run: pnpm build:orcad
- run: pnpm test:bun:profile --artifact
- uses: actions/setup-node@v6
if: runner.arch == 'X64'
with:
node-version: '18'
- name: Verify Node 18 loads and hands off to bundled Bun
if: runner.arch == 'X64'
run: |
node out/orcad/orcad.js --orcad-smoke-load-check
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018

linux_glibc_floor:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.os }}
container: ubuntu:20.04
timeout-minutes: 20
env:
ORCA_BACKGROUND_LAUNCH: '1'
steps:
- name: Install Ubuntu 20.04 prerequisites
run: apt-get update && apt-get install -y build-essential ca-certificates git python3 unzip
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Trust the checked-out workspace
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- uses: ./.github/actions/install-node-dependencies
- run: pnpm build:orcad
- run: pnpm test:bun:profile --artifact

linux_musl:
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
ORCA_BACKGROUND_LAUNCH: '1'
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Verify native Alpine artifact and persistence
run: |
docker run --rm --init -i \
-e ORCA_BACKGROUND_LAUNCH=1 \
-v "$GITHUB_WORKSPACE:/work" -w /work \
node:24-alpine3.23 sh -s <<'BUN_QUALIFICATION'
set -eu
apk add --no-cache bash git libstdc++ python3 make g++
git config --global --add safe.directory /work
npm install -g "$(node -p "require('./package.json').packageManager.split('+')[0]")"
pnpm install --frozen-lockfile --ignore-scripts
pnpm build:orcad
pnpm test:bun:profile --artifact
BUN_QUALIFICATION
3 changes: 3 additions & 0 deletions config/electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ module.exports = {
// Why: these repo-only inputs are either bundled into out/ or copied via
// extraResources. Shipping them in app.asar bloats the desktop bundle.
'!src{,/**/*}',
'!out/orcad{,/**/*}',
'!out/orcad-template{,/**/*}',
'!out/.orcad-*{,/**/*}',
'!config{,/**/*}',
'!docs{,/**/*}',
'!mobile{,/**/*}',
Expand Down
176 changes: 176 additions & 0 deletions config/scripts/build-orcad-bun.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env node

import { createHash } from 'node:crypto'
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { basename, join, resolve } from 'node:path'
import { orcadBunRuntimeFilename } from '../../src/shared/orcad-artifacts.ts'
import {
ORCAD_BUN_RELEASE_ASSETS,
ORCAD_BUN_VERSION,
orcadBunReleaseUrl
} from '../../src/shared/orcad-bun-runtime.ts'
import { runProcessSync } from './script-child-process.mjs'
import { getZipExtractorCommand } from './zip-extractor-command.mjs'

const root = resolve(import.meta.dirname, '../..')
const cacheRoot = join(root, 'out', '.orcad-bun-runtime', `v${ORCAD_BUN_VERSION}`)

export function currentTarget() {
if (process.platform === 'darwin') {
return `darwin-${process.arch}`
}
if (process.platform === 'win32') {
return `win32-${process.arch}`
}
if (process.platform !== 'linux') {
throw new Error(`Unsupported Bun platform: ${process.platform}`)
}
const glibc = process.report?.getReport()?.header?.glibcVersionRuntime
return `linux-${process.arch}-${glibc ? 'glibc' : 'musl'}`
}

function argument(name) {
const index = process.argv.indexOf(name)
return index === -1 ? null : process.argv[index + 1]
}

async function download(url, destination) {
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(120_000) })
if (!response.ok) {
await response.body?.cancel()
throw new Error(`Bun download failed: ${response.status} ${response.statusText}`)
}
writeFileSync(destination, new Uint8Array(await response.arrayBuffer()))
}

function sha256(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex')
}

export function bunExecutableName(target) {
return target.startsWith('win32-') ? 'bun.exe' : 'bun'
}

export function findBunExecutable(rootDir, target) {
const expected = bunExecutableName(target)
const entries = readdirSync(rootDir, { recursive: true, withFileTypes: true })
const entry = entries.find((candidate) => candidate.isFile() && candidate.name === expected)
if (!entry) {
throw new Error(`Downloaded archive contained no ${expected}`)
}
return join(entry.parentPath, entry.name)
}

function verifyRuntime(path) {
const result = runProcessSync({ program: path, args: ['--version'] })
if (result.code !== 0 || result.stdout.trim() !== ORCAD_BUN_VERSION) {
throw new Error(
`Expected Bun ${ORCAD_BUN_VERSION} at ${path}, got ${result.stdout.trim() || result.stderr.trim()}`
)
}
}

async function materializeRuntime(target, outputPath) {
const asset = ORCAD_BUN_RELEASE_ASSETS[target]
if (!asset) {
throw new Error(`Unsupported Bun target: ${target}`)
}
const cached = join(cacheRoot, target, orcadBunRuntimeFilename(target))
if (existsSync(cached) && sha256(cached) !== asset.executableSha256) {
rmSync(cached, { force: true })
}
if (!existsSync(cached)) {
const temporary = mkdtempSync(join(tmpdir(), 'orca-bun-download-'))
try {
const zipPath = join(temporary, basename(asset.filename))
await download(orcadBunReleaseUrl(asset), zipPath)
const actual = sha256(zipPath)
if (actual !== asset.sha256) {
throw new Error(`Bun checksum mismatch for ${asset.filename}: ${actual}`)
}
const extracted = join(temporary, 'extracted')
mkdirSync(extracted)
// Node 24.16 can leave extract-zip's stream promise unsettled with no active handles.
const command = getZipExtractorCommand(zipPath, extracted)
const result = runProcessSync({
program: command.file,
args: command.args,
timeoutMs: 120_000
})
if (result.code !== 0) {
throw new Error(
`Bun archive extraction failed with exit ${result.code}: ${result.stderr || result.stdout}`
)
}
mkdirSync(join(cacheRoot, target), { recursive: true })
copyFileSync(findBunExecutable(extracted, target), cached)
if (!target.startsWith('win32-')) {
chmodSync(cached, 0o755)
}
} finally {
rmSync(temporary, { recursive: true, force: true })
}
}
const executableHash = sha256(cached)
if (executableHash !== asset.executableSha256) {
throw new Error(`Bun executable checksum mismatch for ${target}: ${executableHash}`)
}
if (target === currentTarget()) {
verifyRuntime(cached)
}
mkdirSync(resolve(outputPath, '..'), { recursive: true })
if (resolve(cached) !== resolve(outputPath)) {
copyFileSync(cached, outputPath)
}
if (!target.startsWith('win32-')) {
chmodSync(outputPath, 0o755)
}
}

async function main() {
const target = argument('--target') ?? currentTarget()
const outputDir = argument('--out-dir')
const cachedRuntimePath = join(cacheRoot, target, orcadBunRuntimeFilename(target))
const runtimePath =
process.argv.includes('--runtime-only') && outputDir
? join(resolve(outputDir), orcadBunRuntimeFilename(target))
: cachedRuntimePath
await materializeRuntime(target, runtimePath)

if (process.argv.includes('--runtime-only')) {
process.stdout.write(`${runtimePath}\n`)
return
}
const result = runProcessSync({
program: process.execPath,
args: [join(root, 'config/scripts/build-orcad.mjs')],
cwd: root,
env: {
...process.env,
ORCAD_BUILD_TARGET: target,
ORCAD_BUILD_TARGET_IS_CURRENT: target === currentTarget() ? '1' : '0',
ORCAD_BUN_RUNTIME_PATH: runtimePath,
...(outputDir ? { ORCAD_OUT_DIR: resolve(outputDir) } : {})
},
stdio: 'inherit',
timeoutMs: null
})
if (result.code !== 0) {
process.exit(result.code ?? 1)
}
}

if (process.argv[1]?.endsWith('build-orcad-bun.mjs')) {
await main()
}
39 changes: 39 additions & 0 deletions config/scripts/build-orcad-bun.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, describe, expect, it } from 'vitest'
import { bunExecutableName, findBunExecutable } from './build-orcad-bun.mjs'

const temporaryDirs = []

afterEach(() => {
for (const dir of temporaryDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})

function archiveTree(filename) {
const root = mkdtempSync(join(tmpdir(), 'orcad-bun-archive-'))
temporaryDirs.push(root)
const nested = join(root, 'bun-release')
mkdirSync(nested)
writeFileSync(join(nested, filename), '')
return root
}

describe('orcad Bun archive extraction', () => {
it('selects bun.exe for a Windows target on a non-Windows builder', () => {
const root = archiveTree('bun.exe')
expect(findBunExecutable(root, 'win32-x64')).toBe(join(root, 'bun-release', 'bun.exe'))
})

it('selects bun for a POSIX target', () => {
const root = archiveTree('bun')
expect(findBunExecutable(root, 'linux-x64-glibc')).toBe(join(root, 'bun-release', 'bun'))
})

it('derives executable names from the target rather than the builder host', () => {
expect(bunExecutableName('win32-arm64')).toBe('bun.exe')
expect(bunExecutableName('darwin-arm64')).toBe('bun')
})
})
Loading
Loading