mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 16:03:26 +00:00
fix(ci): make accelerated checks runtime-safe
This commit is contained in:
@@ -2,10 +2,10 @@ name: Install Node dependencies
|
||||
description: Installs the Node toolchain and repository dependencies for Linux CI jobs.
|
||||
|
||||
inputs:
|
||||
ignore-scripts:
|
||||
description: Skip dependency lifecycle scripts when the job does not need native runtimes.
|
||||
native-runtime:
|
||||
description: Native runtime to prepare after the script-free install (none, node, or electron).
|
||||
required: false
|
||||
default: 'false'
|
||||
default: none
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -22,9 +22,22 @@ runs:
|
||||
node-version-file: package.json
|
||||
cache: pnpm
|
||||
|
||||
- name: Validate native runtime
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
|
||||
run: |
|
||||
case "$NATIVE_RUNTIME" in
|
||||
none|node|electron) ;;
|
||||
*)
|
||||
echo "::error::native-runtime must be none, node, or electron"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# pnpm's bundled gyp_main.py is not executable on fresh Linux runners.
|
||||
- name: Use external node-gyp
|
||||
if: inputs.ignore-scripts != 'true'
|
||||
if: inputs.native-runtime != 'none'
|
||||
shell: bash
|
||||
run: |
|
||||
npm install -g node-gyp@11.5.0
|
||||
@@ -40,15 +53,19 @@ runs:
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
env:
|
||||
IGNORE_SCRIPTS: ${{ inputs.ignore-scripts }}
|
||||
run: |
|
||||
install_args=(--no-frozen-lockfile --prefer-frozen-lockfile=false --os=current --os=win32 --cpu=current)
|
||||
if [ "$IGNORE_SCRIPTS" = "true" ]; then
|
||||
install_args+=(--ignore-scripts)
|
||||
elif [ "$IGNORE_SCRIPTS" != "false" ]; then
|
||||
echo "::error::ignore-scripts must be true or false"
|
||||
exit 2
|
||||
fi
|
||||
pnpm install "${install_args[@]}"
|
||||
pnpm install \
|
||||
--no-frozen-lockfile \
|
||||
--prefer-frozen-lockfile=false \
|
||||
--os=current \
|
||||
--os=win32 \
|
||||
--cpu=current \
|
||||
--ignore-scripts
|
||||
git diff --exit-code package.json pnpm-lock.yaml
|
||||
|
||||
- name: Prepare native runtime
|
||||
if: inputs.native-runtime != 'none'
|
||||
shell: bash
|
||||
env:
|
||||
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
|
||||
run: node config/scripts/ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"
|
||||
|
||||
@@ -24,8 +24,6 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
ignore-scripts: 'true'
|
||||
|
||||
- name: Lint
|
||||
run: pnpm exec oxlint --format github
|
||||
@@ -73,8 +71,6 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
ignore-scripts: 'true'
|
||||
|
||||
- run: pnpm typecheck
|
||||
|
||||
@@ -89,8 +85,6 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
ignore-scripts: 'true'
|
||||
|
||||
- name: Verify Git binary compatibility matrix
|
||||
run: |
|
||||
@@ -144,10 +138,11 @@ jobs:
|
||||
run: sudo apt-get update && sudo apt-get install -y zsh
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: node
|
||||
|
||||
- name: Test real shell contracts
|
||||
run: |
|
||||
node config/scripts/ensure-native-runtime.mjs --runtime=node
|
||||
pnpm exec vitest run --config config/vitest.config.ts \
|
||||
src/main/daemon/shell-ready.test.ts \
|
||||
src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
@@ -172,7 +167,7 @@ jobs:
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
ignore-scripts: 'true'
|
||||
native-runtime: node
|
||||
|
||||
- name: Install Electron package binary for tests
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
@@ -198,10 +193,6 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
ignore-scripts: 'true'
|
||||
|
||||
- name: Cache electron-builder downloads
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
@@ -212,16 +203,17 @@ jobs:
|
||||
restore-keys: |
|
||||
electron-builder-linux-
|
||||
|
||||
# Native preparation does not touch the bundle output directories.
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
with:
|
||||
native-runtime: electron
|
||||
|
||||
- name: Build package inputs
|
||||
run: |
|
||||
pnpm run ensure:electron-runtime &
|
||||
pids=("$!")
|
||||
|
||||
status=0
|
||||
pnpm run build:cli || status=1
|
||||
|
||||
scripts=(build:relay build:electron-vite:parallel)
|
||||
pids=()
|
||||
for script in "${scripts[@]}"; do
|
||||
pnpm run "$script" &
|
||||
pids+=("$!")
|
||||
|
||||
@@ -26,4 +26,10 @@ describe('Electron Vite output contract', () => {
|
||||
expect(external('@xterm/headless', undefined, false)).toBe(false)
|
||||
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
|
||||
})
|
||||
|
||||
it('isolates renderer entry side effects behind strict facades', () => {
|
||||
expect(electronViteConfig.renderer?.build?.rollupOptions?.preserveEntrySignatures).toBe(
|
||||
'strict'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,15 +49,18 @@ describe('PR workflow parallelism', () => {
|
||||
const shellStep = workflow.jobs.shell_contracts.steps.find(
|
||||
(step) => step.name === 'Test real shell contracts'
|
||||
)
|
||||
const shellInstall = workflow.jobs.shell_contracts.steps.find(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
|
||||
expect(workflow.jobs.test.steps.some((step) => step.name === 'Install zsh')).toBe(false)
|
||||
expect(workflow.jobs.shell_contracts.steps.some((step) => step.name === 'Install zsh')).toBe(
|
||||
true
|
||||
)
|
||||
expect(shellInstall.with['native-runtime']).toBe('node')
|
||||
for (const testFile of nativeShellContractFiles) {
|
||||
expect(shellStep.run).toContain(testFile)
|
||||
}
|
||||
expect(shellStep.run).toContain('ensure-native-runtime.mjs --runtime=node')
|
||||
})
|
||||
|
||||
it('keeps every real-zsh test in the dedicated shell lane', () => {
|
||||
@@ -74,7 +77,6 @@ describe('PR workflow parallelism', () => {
|
||||
)
|
||||
|
||||
expect(buildStep.run).toContain('scripts=(build:relay build:electron-vite:parallel)')
|
||||
expect(buildStep.run).toContain('pnpm run ensure:electron-runtime &')
|
||||
expect(buildStep.run).toContain('pnpm run "$script" &')
|
||||
expect(
|
||||
workflow.jobs.package.steps.find(
|
||||
@@ -94,41 +96,45 @@ describe('PR workflow parallelism', () => {
|
||||
expect(steps[nodeIndex].with.cache).toBe('pnpm')
|
||||
})
|
||||
|
||||
it('skips lifecycle scripts outside native-runtime test setup', () => {
|
||||
for (const jobName of [
|
||||
'static_analysis',
|
||||
'typecheck',
|
||||
'git_compatibility',
|
||||
'test',
|
||||
'package'
|
||||
]) {
|
||||
const installStep = workflow.jobs[jobName].steps.find(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
expect(installStep.with['ignore-scripts'], jobName).toBe('true')
|
||||
}
|
||||
|
||||
const shellInstall = workflow.jobs.shell_contracts.steps.find(
|
||||
it('restores Electron downloads before preparing the package runtime', () => {
|
||||
const steps = workflow.jobs.package.steps
|
||||
const cacheIndex = steps.findIndex((step) => step.name === 'Cache electron-builder downloads')
|
||||
const installIndex = steps.findIndex(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
expect(shellInstall.with).toBeUndefined()
|
||||
|
||||
expect(cacheIndex).toBeLessThan(installIndex)
|
||||
})
|
||||
|
||||
it('prepares each native runtime before its consumers start', () => {
|
||||
const installFor = (jobName) =>
|
||||
workflow.jobs[jobName].steps.find(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
|
||||
for (const jobName of ['static_analysis', 'typecheck', 'git_compatibility']) {
|
||||
expect(installFor(jobName).with, jobName).toBeUndefined()
|
||||
}
|
||||
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
|
||||
expect(installFor('test').with['native-runtime']).toBe('node')
|
||||
expect(installFor('package').with['native-runtime']).toBe('electron')
|
||||
|
||||
expect(
|
||||
dependencyAction.runs.steps.find((step) => step.name === 'Use external node-gyp').if
|
||||
).toBe("inputs.ignore-scripts != 'true'")
|
||||
).toBe("inputs.native-runtime != 'none'")
|
||||
const dependencyInstall = dependencyAction.runs.steps.find(
|
||||
(step) => step.name === 'Install dependencies'
|
||||
)
|
||||
expect(dependencyInstall.env.IGNORE_SCRIPTS).toBe('${{ inputs.ignore-scripts }}')
|
||||
expect(dependencyInstall.run).toContain(
|
||||
'--no-frozen-lockfile --prefer-frozen-lockfile=false --os=current --os=win32 --cpu=current'
|
||||
)
|
||||
expect(dependencyInstall.run).toContain('install_args+=(--ignore-scripts)')
|
||||
expect(workflow.jobs.test.steps.find((step) => step.name === 'Test shard').run).not.toContain(
|
||||
'ensure-native-runtime'
|
||||
expect(dependencyInstall.run).toContain('--no-frozen-lockfile')
|
||||
expect(dependencyInstall.run).toContain('--ignore-scripts')
|
||||
const prepareRuntime = dependencyAction.runs.steps.find(
|
||||
(step) => step.name === 'Prepare native runtime'
|
||||
)
|
||||
expect(prepareRuntime.if).toBe("inputs.native-runtime != 'none'")
|
||||
expect(prepareRuntime.run).toContain('ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"')
|
||||
})
|
||||
|
||||
it('reuses native preparation only after the concurrent runtime gate', () => {
|
||||
it('reuses native preparation after the dependency action gate', () => {
|
||||
const buildStep = workflow.jobs.package.steps.find(
|
||||
(step) => step.name === 'Build package inputs'
|
||||
)
|
||||
@@ -136,7 +142,7 @@ describe('PR workflow parallelism', () => {
|
||||
(step) => step.name === 'Package unpacked app'
|
||||
)
|
||||
|
||||
expect(buildStep.run).toContain('pnpm run ensure:electron-runtime &')
|
||||
expect(buildStep.run).not.toContain('ensure:electron-runtime')
|
||||
expect(packageStep.env.ORCA_REUSE_PREPARED_NATIVE_RUNTIME).toBe('1')
|
||||
})
|
||||
|
||||
|
||||
@@ -19,6 +19,36 @@ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
const selectedFiles = new Set(['web-index.html'])
|
||||
const visitedEntries = new Set()
|
||||
|
||||
function assertEntryIsolation() {
|
||||
const entryKeys = new Set(
|
||||
Object.entries(manifest)
|
||||
.filter(([, entry]) => entry?.isEntry === true)
|
||||
.map(([key]) => key)
|
||||
)
|
||||
|
||||
for (const sourceEntry of entryKeys) {
|
||||
const visited = new Set()
|
||||
const pending = [sourceEntry]
|
||||
while (pending.length > 0) {
|
||||
const key = pending.pop()
|
||||
if (visited.has(key)) {
|
||||
continue
|
||||
}
|
||||
visited.add(key)
|
||||
const entry = manifest[key]
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
throw new Error(`Renderer manifest is missing entry: ${key}`)
|
||||
}
|
||||
for (const dependency of [...(entry.imports ?? []), ...(entry.dynamicImports ?? [])]) {
|
||||
if (entryKeys.has(dependency) && dependency !== sourceEntry) {
|
||||
throw new Error(`Renderer entry ${sourceEntry} executes entry ${dependency}`)
|
||||
}
|
||||
pending.push(dependency)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addOutputPath(outputPath) {
|
||||
if (
|
||||
typeof outputPath !== 'string' ||
|
||||
@@ -107,6 +137,7 @@ async function minifyWebOutput() {
|
||||
)
|
||||
}
|
||||
|
||||
assertEntryIsolation()
|
||||
visitManifestEntry('web-index.html')
|
||||
includeReferencedOutputs()
|
||||
|
||||
|
||||
@@ -98,4 +98,20 @@ describe('renderer web client projection', () => {
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('Renderer manifest is missing entry: web-index.html')
|
||||
})
|
||||
|
||||
it('rejects renderer entries that execute another entry root', () => {
|
||||
const root = createRendererFixture()
|
||||
const manifestPath = join(root, 'out/renderer/.vite/manifest.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
manifest['web-index.html'].dynamicImports.push('index.html')
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest))
|
||||
|
||||
const result = spawnSync(process.execPath, [scriptPath], {
|
||||
cwd: root,
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('Renderer entry web-index.html executes entry index.html')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -277,6 +277,9 @@ export const electronViteConfig: UserConfig = {
|
||||
// stay listed — overriding input otherwise drops electron-vite's default
|
||||
// renderer entry.
|
||||
rollupOptions: {
|
||||
// Why: shared chunks must never import an HTML entry whose module mounts
|
||||
// a different React root.
|
||||
preserveEntrySignatures: 'strict',
|
||||
input: {
|
||||
index: resolve('src/renderer/index.html'),
|
||||
popout: resolve('src/renderer/popout.html'),
|
||||
|
||||
Reference in New Issue
Block a user