Reduce redundant CI runs, pnpm uploads, and fixture startups (#23145)

* Reduce redundant CI runs, store uploads, and fixture processes

* Avoid repeating draft-independent mobile checks on readiness

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
This commit is contained in:
OrcaWin
2026-09-26 01:17:51 -07:00
committed by GitHub
co-authored by m4air
parent 2afb9a385b
commit d17a17684b
22 changed files with 1089 additions and 73 deletions
@@ -50,8 +50,9 @@ runs:
uses: actions/setup-node@v6
with:
node-version-file: package.json
cache: pnpm
cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false
- name: Setup requested Node.js
id: requested-node
@@ -59,8 +60,31 @@ runs:
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: pnpm
cache: ${{ github.event_name != 'pull_request' && 'pnpm' || '' }}
cache-dependency-path: ${{ inputs.cache-dependency-path }}
package-manager-cache: false
# PR-local stores compete with reusable build caches for the repository quota.
- name: Resolve pnpm download store
id: pnpm-store
if: github.event_name == 'pull_request'
shell: bash
env:
LOCKFILE_HASH: ${{ hashFiles(inputs.cache-dependency-path) }}
run: |
test -n "$LOCKFILE_HASH"
cache_path="$(pnpm store path --silent)"
test -n "$cache_path"
printf 'path=%s\n' "$cache_path" >> "$GITHUB_OUTPUT"
printf 'arch=%s\n' "$(node -p 'require("node:os").arch()')" >> "$GITHUB_OUTPUT"
# Match setup-node's key and path so existing default-branch stores remain reusable.
- name: Restore pnpm download store without saving
if: github.event_name == 'pull_request'
uses: actions/cache/restore@v5
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}
- name: Validate native runtime
shell: bash
+39 -7
View File
@@ -3,17 +3,17 @@ 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/**'
- 'src/**'
- 'config/**'
- 'native/**'
- 'tests/**'
- 'resources/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'tsconfig.json'
- '.npmrc'
- '.pnpmfile.cjs'
- '.github/actions/install-node-dependencies/**'
- '.github/workflows/bun-profile-tests.yml'
workflow_dispatch:
@@ -26,7 +26,33 @@ concurrency:
cancel-in-progress: true
jobs:
changes:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
should_run: ${{ steps.scope.outputs.should_run }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 2
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
- name: Detect Bun build and test inputs
id: scope
shell: bash
run: |
# Compare the tested merge with its base, retaining both sides of renames.
if git diff --name-only --no-renames -z HEAD^1 HEAD > "$RUNNER_TEMP/bun-changes"; then
node config/scripts/bun-profile-change-scope.mjs "$RUNNER_TEMP/bun-changes"
else
echo 'should_run=true' >> "$GITHUB_OUTPUT"
fi
persistence:
needs: changes
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
strategy:
fail-fast: false
matrix:
@@ -56,6 +82,9 @@ jobs:
node out/orcad/orcad.js --orcad-profile-state-preflight 00000000-0000-4000-8000-000000000018
linux_glibc_floor:
needs: changes
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
strategy:
fail-fast: false
matrix:
@@ -78,6 +107,9 @@ jobs:
- run: pnpm test:bun:profile --artifact
linux_musl:
needs: changes
# Missing/failed detection runs the full matrix; manual runs remain unconditional.
if: ${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}
strategy:
fail-fast: false
matrix:
-1
View File
@@ -6,7 +6,6 @@ on:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- 'mobile/**'
# Mobile launch contracts exercise the real host dispatcher and durable receipt store.
-1
View File
@@ -6,7 +6,6 @@ on:
- opened
- synchronize
- reopened
- ready_for_review
concurrency:
group: pr-test-loc-${{ github.event.pull_request.number }}
+30 -14
View File
@@ -1,4 +1,5 @@
name: PR Checks
run-name: 'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
on:
pull_request:
@@ -27,23 +28,28 @@ jobs:
# Reuse one lightweight checkout for detection and the always-required guards.
runs-on: ubuntu-slim
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
should_run: ${{ steps.filter.outputs.should_run }}
native_cache_changed: ${{ steps.filter.outputs.native_cache_changed }}
reused_run_id: ${{ steps.readiness.outputs.run_id }}
# A proven success masks required work only; advisory routing still uses the full diff.
native_cache_changed: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.native_cache_changed }}
mobile_dependencies: ${{ steps.filter.outputs.mobile_dependencies }}
mobile_web_app: ${{ steps.filter.outputs.mobile_web_app }}
static_analysis: ${{ steps.filter.outputs.static_analysis }}
typecheck: ${{ steps.filter.outputs.typecheck }}
git_compatibility: ${{ steps.filter.outputs.git_compatibility }}
codex_index_heal_contract: ${{ steps.filter.outputs.codex_index_heal_contract }}
xterm_patch_sync: ${{ steps.filter.outputs.xterm_patch_sync }}
shell_contracts: ${{ steps.filter.outputs.shell_contracts }}
test: ${{ steps.filter.outputs.test }}
orcad_browser: ${{ steps.filter.outputs.orcad_browser }}
cross-version-wire: ${{ steps.filter.outputs.cross-version-wire }}
managed_hook_node18: ${{ steps.filter.outputs.managed_hook_node18 }}
package: ${{ steps.filter.outputs.package }}
package_windows: ${{ steps.filter.outputs.package_windows }}
mobile_web_app: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.mobile_web_app }}
static_analysis: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.static_analysis }}
typecheck: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.typecheck }}
git_compatibility: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.git_compatibility }}
codex_index_heal_contract: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.codex_index_heal_contract }}
xterm_patch_sync: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.xterm_patch_sync }}
shell_contracts: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.shell_contracts }}
test: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.test }}
orcad_browser: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.orcad_browser }}
cross-version-wire: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.cross-version-wire }}
managed_hook_node18: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node18 }}
package: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package }}
package_windows: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package_windows }}
e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }}
test_files: ${{ steps.e2e_filter.outputs.test_files }}
ssh_source_changed: ${{ steps.e2e_filter.outputs.ssh_source_changed }}
@@ -66,6 +72,7 @@ jobs:
/config/scripts/check-readme-local-links.mjs
/config/scripts/pr-code-change-scope.mjs
/config/scripts/pr-e2e-source-routing.mjs
/config/scripts/pr-ready-check-reuse.mjs
sparse-checkout-cone-mode: false
persist-credentials: false
@@ -79,6 +86,15 @@ jobs:
- name: Check README local links
run: node config/scripts/check-readme-local-links.mjs
# Readiness changes eligibility for advisory tests, not the already-tested source.
- name: Find identical successful required checks
id: readiness
if: github.event.action == 'ready_for_review'
env:
GH_TOKEN: ${{ github.token }}
PR_CHECK_WORKFLOW_SHA: ${{ github.workflow_sha }}
run: node config/scripts/pr-ready-check-reuse.mjs
- name: Classify changed paths
id: filter
env:
+6 -5
View File
@@ -5,7 +5,8 @@ import { build } from 'esbuild'
import {
buildOrcadEntry,
externalNativeAddons,
ORCAD_EXTERNAL_MODULES
ORCAD_EXTERNAL_MODULES,
ORCAD_CHILD_ENTRY_POINTS
} from './orcad-entry-build.mjs'
import { createRequire } from 'node:module'
import {
@@ -44,14 +45,14 @@ const OUT_DIR = process.env.ORCAD_OUT_DIR
// Why beside orcad.js: the watcher runs in a forked child so a native @parcel/watcher
// fault crashes that child instead of the server, and `resolveWatcherProcessEntryPath`
// looks for it in the app root. A deployment has no desktop out/main to fall back to.
const WATCHER_ENTRY = join(ROOT, 'src/main/ipc/parcel-watcher-process-entry.ts')
const WATCHER_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.watcher)
const WATCHER_OUT_FILE = join(OUT_DIR, 'parcel-watcher-process-entry.js')
// Why beside orcad.js: orcad forks the terminal daemon so PTYs outlive the runtime process,
// and `getDaemonEntryPath()` probes the app root for this exact filename. Without it every
// orcad restart would SIGKILL every running terminal.
const DAEMON_ENTRY = join(ROOT, 'src/main/daemon/daemon-entry.ts')
const DAEMON_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.daemon)
const DAEMON_OUT_FILE = join(OUT_DIR, 'daemon-entry.js')
const PTY_GATE_ENTRY = join(ROOT, 'src/main/daemon/pty-subprocess/windows-bun-pty-gate-entry.ts')
const PTY_GATE_ENTRY = join(ROOT, ORCAD_CHILD_ENTRY_POINTS.ptyGate)
const PTY_GATE_OUT_FILE = join(OUT_DIR, 'windows-bun-pty-gate-entry.js')
const OUT_FILE = join(OUT_DIR, 'orcad.js')
const BUILD_TARGET = process.env.ORCAD_BUILD_TARGET
@@ -179,7 +180,7 @@ const childResults = await Promise.all([
buildForkedChild(PTY_GATE_ENTRY, PTY_GATE_OUT_FILE),
...['writer', 'backup'].map((role) =>
buildForkedChild(
join(ROOT, `src/main/persistence/profile-state/profile-state-${role}-worker-entry.ts`),
join(ROOT, ORCAD_CHILD_ENTRY_POINTS[role]),
join(OUT_DIR, `profile-state-${role}-worker-entry.js`)
)
)
+127
View File
@@ -0,0 +1,127 @@
import { build } from 'esbuild'
import { appendFileSync, globSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import {
externalNativeAddons,
ORCAD_CHILD_ENTRY_POINTS,
ORCAD_ENTRY_POINT
} from './orcad-entry-build.mjs'
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
const ROOT = resolve(import.meta.dirname, '../..')
const BUILD_SCRIPTS = [
'config/scripts/build-orcad-bun.mjs',
'config/scripts/build-orcad.mjs',
'config/scripts/build-windows-process-tree-relay-addon.mjs',
'config/scripts/run-bun-profile-tests.mjs',
'config/vitest.config.ts',
'config/scripts/happy-dom-offscreen-canvas.ts',
'config/scripts/happy-dom-mutation-observer-retention.ts',
'config/scripts/vitest-host-ports-setup.ts'
]
const ALWAYS_FILES = new Set([
'package.json',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'.npmrc',
'.pnpmfile.cjs',
'tsconfig.json',
'.github/workflows/bun-profile-tests.yml',
'config/scripts/bun-profile-change-scope.mjs',
'config/scripts/bun-profile-change-scope.test.mjs'
])
const ALWAYS_PREFIXES = [
'.github/actions/install-node-dependencies/',
// These areas also contain worker paths and fixtures opened without an import.
'src/main/persistence/',
'src/main/sqlite/',
'src/main/orcad/',
'src/main/daemon/pty-subprocess/',
'src/main/providers/',
'config/patches/',
'config/tsconfig',
'native/',
'resources/licenses/ripgrep/'
]
export function discoverBunProfileTests(root = ROOT) {
const selectors = bunProfileTestPaths({ artifact: true })
return globSync(
['src/**/*.test.{ts,tsx}', 'config/scripts/**/*.test.{ts,mjs}', 'tests/e2e/**/*.unit.test.ts'],
{ cwd: root }
)
.map((file) => file.replaceAll('\\', '/'))
.filter((file) => selectors.some((selector) => file.includes(selector)))
.sort()
}
export async function collectBunProfileInputs({ root = ROOT, entryPoints } = {}) {
const entries = entryPoints ?? [
ORCAD_ENTRY_POINT,
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
...BUILD_SCRIPTS,
...discoverBunProfileTests(root)
]
const result = await build({
absWorkingDir: root,
entryPoints: entries,
bundle: true,
write: false,
outdir: resolve(root, '.bun-profile-scope'),
platform: 'node',
format: 'esm',
splitting: true,
packages: 'external',
loader: { '.svg': 'empty', '.png': 'empty', '.webp': 'empty', '.css': 'empty' },
plugins: [externalNativeAddons],
metafile: true,
logLevel: 'silent'
})
if (result.warnings.length > 0) {
throw new Error(result.warnings.map((warning) => warning.text).join('\n'))
}
return new Set(
Object.keys(result.metafile.inputs).map((file) =>
file.replaceAll('\\', '/').replace(/\?.*$/, '')
)
)
}
export async function classifyBunProfileChanges(changedFiles, collect = collectBunProfileInputs) {
if (changedFiles.length === 0) {
return { shouldRun: true, reason: 'No complete changed-file evidence' }
}
const selectors = bunProfileTestPaths({ artifact: true })
const forced = changedFiles.find(
(file) =>
ALWAYS_FILES.has(file) ||
ALWAYS_PREFIXES.some((prefix) => file.startsWith(prefix)) ||
selectors.some((selector) => file.includes(selector))
)
if (forced) {
return { shouldRun: true, reason: `Build or CI input changed: ${forced}` }
}
try {
const inputs = await collect()
const matched = changedFiles.find((file) => inputs.has(file))
return {
shouldRun: Boolean(matched),
reason: matched ? `Runtime or test dependency changed: ${matched}` : 'No Bun inputs changed'
}
} catch (error) {
return { shouldRun: true, reason: `Dependency graph unavailable: ${String(error)}` }
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const changedFiles = readFileSync(process.argv[2], 'utf8').split('\0').filter(Boolean)
const result = await classifyBunProfileChanges(changedFiles)
console.log(result.reason)
const output = `should_run=${String(result.shouldRun)}\n`
if (process.env.GITHUB_OUTPUT) {
appendFileSync(process.env.GITHUB_OUTPUT, output)
} else {
process.stdout.write(output)
}
}
@@ -0,0 +1,151 @@
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
classifyBunProfileChanges,
collectBunProfileInputs,
discoverBunProfileTests
} from './bun-profile-change-scope.mjs'
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs'
const temporaryDirs = []
afterEach(() => {
for (const root of temporaryDirs.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function moduleTree(files) {
const root = mkdtempSync(join(tmpdir(), 'bun-profile-scope-'))
temporaryDirs.push(root)
for (const [file, source] of Object.entries(files)) {
const path = join(root, file)
mkdirSync(join(path, '..'), { recursive: true })
writeFileSync(path, source)
}
return root
}
it('follows static imports, re-exports, dynamic imports and require without executing source', async () => {
const root = moduleTree({
'entry.ts': `import './first'; export * from './exports'; import('./dynamic'); require('./required'); throw Error('never execute')`,
'first.ts': `import './nested/leaf'`,
'exports.ts': 'export const value = 1',
'dynamic.ts': 'export const value = 2',
'required.ts': 'module.exports = 3',
'nested/leaf.ts': 'export const value = 4',
'unrelated.ts': 'throw Error("unrelated")'
})
const inputs = await collectBunProfileInputs({ root, entryPoints: ['entry.ts'] })
expect([...inputs].sort()).toEqual([
'dynamic.ts',
'entry.ts',
'exports.ts',
'first.ts',
'nested/leaf.ts',
'required.ts'
])
})
it('runs the matrix when a dependency is deleted or graph analysis fails', async () => {
const root = moduleTree({ 'entry.ts': `import './deleted'` })
const result = await classifyBunProfileChanges(['deleted.ts'], () =>
collectBunProfileInputs({ root, entryPoints: ['entry.ts'] })
)
expect(result.shouldRun).toBe(true)
expect(result.reason).toContain('Dependency graph unavailable')
expect((await classifyBunProfileChanges([])).shouldRun).toBe(true)
})
it.each([
['tests/e2e/daemon-running-work-probe.unit.test.ts'],
['config/scripts/zip-extractor-command.test.mjs'],
['config/scripts/zip-extractor-command.test.mjs', 'config/scripts/renamed-command.test.mjs']
])(
'runs deleted or renamed selected tests even when absent from the graph: %j',
async (...files) => {
expect((await classifyBunProfileChanges(files, async () => new Set())).shouldRun).toBe(true)
}
)
it.each([
'package.json',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'.npmrc',
'tsconfig.json',
'config/tsconfig.node.json',
'config/patches/node-pty@1.1.0.patch',
'native/windows-registry/src/addon.cc',
'.github/actions/install-node-dependencies/action.yml',
'.github/workflows/bun-profile-tests.yml',
'src/main/persistence/profile-state/new-worker.ts'
])('always selects build, native and dynamically opened inputs: %s', async (file) => {
expect((await classifyBunProfileChanges([file], async () => new Set())).shouldRun).toBe(true)
})
describe('the actual Bun build and profile-test dependency graph', () => {
let inputs
beforeAll(async () => {
inputs = await collectBunProfileInputs()
}, 60_000)
it.each([
'config/scripts/ci-shard-timings.json',
'config/scripts/mobile-web-app-terminal-render.test.mjs',
'src/main/ssh/ssh-relay-upload-stage-commands.test.ts',
'src/main/menu/register-app-menu.ts'
])('skips unrelated work: %s', async (file) => {
expect((await classifyBunProfileChanges([file], async () => inputs)).shouldRun).toBe(false)
})
it.each([
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
'src/shared/keybindings/definitions-core-1.ts',
'src/main/runtime/orca-runtime.ts',
'src/main/windows/windows-process-table.ts',
'src/main/worker-thread-entry-path.ts',
'config/scripts/zip-extractor-command.mjs',
'config/scripts/windows-process-tree-gyp-rebuild.mjs',
'config/scripts/profile-state-worker-smoke.mjs',
'config/scripts/vitest-host-ports-setup.ts',
'tests/e2e/daemon-running-work-probe.unit.test.ts'
])('retains the full matrix for a real runtime, worker or test input: %s', async (file) => {
expect(inputs.has(file)).toBe(true)
expect((await classifyBunProfileChanges([file], async () => inputs)).shouldRun).toBe(true)
})
it('retains all selected tests and uses the same selectors as the Bun runner', () => {
const tests = discoverBunProfileTests()
expect(tests.length).toBeGreaterThan(80)
expect(tests.every((file) => inputs.has(file))).toBe(true)
expect(
bunProfileTestPaths().every((selector) => tests.some((file) => file.includes(selector)))
).toBe(true)
const runner = readFileSync(new URL('./run-bun-profile-tests.mjs', import.meta.url), 'utf8')
expect(runner).toContain('testArgs.length > 0 ? testArgs : bunProfileTestPaths({ artifact })')
})
})
it('keeps all ten platform jobs and runs them when detection is skipped or fails', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/bun-profile-tests.yml', import.meta.url), 'utf8')
)
expect(workflow.on).toHaveProperty('workflow_dispatch')
expect(workflow.jobs.changes.if).toBe("github.event_name == 'pull_request'")
expect(workflow.jobs.changes.steps[0].with['fetch-depth']).toBe(2)
expect(workflow.jobs.changes.steps[0].with['persist-credentials']).toBe(false)
const detect = workflow.jobs.changes.steps.find((step) => step.id === 'scope')
expect(detect.run).toContain('git diff --name-only --no-renames -z HEAD^1 HEAD')
let count = 0
for (const jobName of ['persistence', 'linux_glibc_floor', 'linux_musl']) {
const job = workflow.jobs[jobName]
expect(job.needs).toBe('changes')
expect(job.if).toBe("${{ !cancelled() && needs.changes.outputs.should_run != 'false' }}")
count += job.strategy.matrix.os.length
}
expect(count).toBe(10)
})
+24
View File
@@ -0,0 +1,24 @@
export function bunProfileTestPaths({ artifact = false } = {}) {
return [
'src/main/persistence/profile-state',
'src/main/persistence/loading-store/profile-state',
'src/main/sqlite',
'src/main/orcad/orcad-entry.test.ts',
'src/main/orcad/orcad-push-startup.test.ts',
...(artifact
? [
'src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-job-control.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-process-suspension.test.ts',
'src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts',
'src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts',
'tests/e2e/daemon-running-work-probe.unit.test.ts',
'src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts',
'src/main/providers/local-pty-bun-artifact.integration.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/orcad/orcad-bun-launcher.integration.test.ts',
'config/scripts/zip-extractor-command.test.mjs'
]
: [])
]
}
@@ -10,8 +10,9 @@ describe('CI dependency download caches', () => {
it('scopes desktop stores to the root lockfile and lets mixed installs opt in', () => {
expect(action.inputs['cache-dependency-path'].default).toBe('pnpm-lock.yaml')
for (const step of action.runs.steps.filter((step) => step.uses === 'actions/setup-node@v6')) {
expect(step.with.cache).toBe('pnpm')
expect(step.with.cache).toBe("${{ github.event_name != 'pull_request' && 'pnpm' || '' }}")
expect(step.with['cache-dependency-path']).toBe('${{ inputs.cache-dependency-path }}')
expect(step.with['package-manager-cache']).toBe(false)
}
const install = action.runs.steps.find((step) => step.name === 'Install dependencies')
expect(install.if).toBeUndefined()
@@ -27,6 +28,28 @@ describe('CI dependency download caches', () => {
'mobile/pnpm-lock.yaml'
])
})
it('restores PR stores with setup-node keys without registering a post-job save', () => {
const resolve = action.runs.steps.find((step) => step.id === 'pnpm-store')
const restore = action.runs.steps.find(
(step) => step.name === 'Restore pnpm download store without saving'
)
expect(resolve.if).toBe("github.event_name == 'pull_request'")
expect(restore.if).toBe(resolve.if)
expect(restore.uses).toBe('actions/cache/restore@v5')
expect(restore.with.path).toBe('${{ steps.pnpm-store.outputs.path }}')
expect(restore.with.key).toBe(
'node-cache-${{ runner.os }}-${{ steps.pnpm-store.outputs.arch }}-pnpm-${{ hashFiles(inputs.cache-dependency-path) }}'
)
expect(restore.with['restore-keys']).toBeUndefined()
expect(resolve.env.LOCKFILE_HASH).toBe('${{ hashFiles(inputs.cache-dependency-path) }}')
expect(action.runs.steps.indexOf(resolve)).toBeLessThan(action.runs.steps.indexOf(restore))
expect(action.runs.steps.indexOf(restore)).toBeLessThan(
action.runs.steps.findIndex((step) => step.name === 'Install dependencies')
)
const saves = action.runs.steps.filter((step) => step.uses === 'actions/cache/save@v5')
expect(saves).toEqual([])
})
})
describe('release install targets', () => {
+154
View File
@@ -0,0 +1,154 @@
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import {
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { parseArgs } from 'node:util'
import { resolvePnpmCliInvocation } from './pnpm-cli-invocation.mjs'
import { runProcessSync } from './script-child-process.mjs'
const { values } = parseArgs({
options: { samples: { type: 'string', default: '3' }, output: { type: 'string' } }
})
const samples = Number(values.samples)
assert(Number.isInteger(samples) && samples > 0 && samples <= 10, '--samples must be 1–10')
const repository = resolve(import.meta.dirname, '../..')
const temporary = mkdtempSync(join(tmpdir(), 'orca-ci-pnpm-store-'))
const checkout = join(temporary, 'checkout')
const store = join(temporary, 'store')
const archive = join(temporary, 'store.tar.zst')
const uncompressed = join(temporary, 'store.tar')
const manifests = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
const pnpm = resolvePnpmCliInvocation()
const results = []
function command(program, args, cwd = checkout) {
const start = performance.now()
const result = runProcessSync({
program,
args,
cwd,
env: { ...process.env, CI: 'true', ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 300_000
})
assert.equal(result.code, 0, `${program}: ${result.stdout}\n${result.stderr}`)
return { milliseconds: performance.now() - start, stdout: result.stdout.trim() }
}
function pnpmCommand(args) {
return command(pnpm.command, [...pnpm.prefixArgs, ...args])
}
function digest() {
const hash = createHash('sha256')
for (const file of manifests) {
hash.update(readFileSync(join(checkout, file)))
}
return hash.digest('hex')
}
function install() {
return pnpmCommand(['install', '--frozen-lockfile', '--ignore-scripts', '--store-dir', store])
.milliseconds
}
function transferArchive(restore) {
const start = performance.now()
// Windows BSD tar needs a separate zstd process, as in the Actions cache toolkit.
if (process.platform === 'win32') {
if (restore) {
command('zstd', ['-d', '-f', 'store.tar.zst', '-o', 'store.tar'], temporary)
command('tar', ['-xf', 'store.tar', '-C', store], temporary)
} else {
command('tar', ['--format=posix', '-cf', 'store.tar', '-C', store, '.'], temporary)
command('zstd', ['-T0', '-f', 'store.tar', '-o', 'store.tar.zst'], temporary)
}
rmSync(uncompressed)
} else {
command(
'tar',
restore
? ['-xf', archive, '--use-compress-program', 'zstd -d', '-C', store]
: ['--format=posix', '-cf', archive, '--use-compress-program', 'zstd -T0', '-C', store, '.']
)
}
return performance.now() - start
}
try {
mkdirSync(checkout)
for (const file of [...manifests, 'native/windows-registry/package.json', 'config/patches']) {
const target = join(checkout, file)
mkdirSync(resolve(target, '..'), { recursive: true })
cpSync(join(repository, file), target, { recursive: true })
}
const sourceDigest = digest()
const pnpmVersion = pnpmCommand(['--version']).stdout
const zstdVersion = command('zstd', ['--version']).stdout
const tarVersion = command('tar', ['--version']).stdout
for (let sample = 0; sample < samples; sample++) {
const policies = sample % 2 === 0 ? ['save', 'restore-only'] : ['restore-only', 'save']
for (const policy of policies) {
rmSync(join(checkout, 'node_modules'), { recursive: true, force: true })
rmSync(store, { recursive: true, force: true })
const installMs = install()
assert.equal(digest(), sourceDigest, 'frozen install changed a manifest')
let archiveMs = 0
let archiveBytes = 0
if (policy === 'save') {
archiveMs = transferArchive(false)
archiveBytes = statSync(archive).size
}
const result = {
sample,
policy,
installMs,
archiveMs,
archiveBytes,
totalMs: installMs + archiveMs
}
results.push(result)
console.error(JSON.stringify(result))
}
}
// Both policies restore identical bytes on a hit; measure that common cost separately.
rmSync(join(checkout, 'node_modules'), { recursive: true, force: true })
rmSync(store, { recursive: true, force: true })
mkdirSync(store)
const restoreMs = transferArchive(true)
const hitInstallMs = install()
assert.equal(digest(), sourceDigest)
const report = {
platform: process.platform,
arch: process.arch,
node: process.version,
pnpm: pnpmVersion,
tar: tarVersion,
zstd: zstdVersion,
sourceDigest,
results,
hit: { restoreMs, installMs: hitInstallMs, totalMs: restoreMs + hitInstallMs },
scope:
'Desktop script-free frozen install, fresh isolated store per miss; alternating policy order.',
limit:
'Measures local install/archive CPU and disk. Excludes GitHub cache transfer and remote service time.'
}
const json = `${JSON.stringify(report, null, 2)}\n`
if (values.output) {
writeFileSync(resolve(values.output), json)
}
console.log(json)
} finally {
rmSync(temporary, { recursive: true, force: true })
}
@@ -7,6 +7,7 @@ import { describe, expect, it } from 'vitest'
const action = parse(readFileSync('.github/actions/install-node-dependencies/action.yml', 'utf8'))
const installScript = action.runs.steps.find((step) => step.name === 'Install dependencies').run
const storeScript = action.runs.steps.find((step) => step.id === 'pnpm-store').run
function run(command, args, options = {}) {
return spawnSync(command, args, { encoding: 'utf8', ...options })
@@ -35,7 +36,10 @@ function createFixture() {
).toBe(0)
const pnpm = join(bin, 'pnpm')
writeFileSync(pnpm, '#!/bin/sh\nexit 0\n')
writeFileSync(
pnpm,
'#!/bin/sh\nif [ "$1" = store ]; then printf "%s\\n" "$PNPM_TEST_STORE_PATH"; fi\nexit 0\n'
)
chmodSync(pnpm, 0o755)
return { bin, detachedCwd, root, workspace }
}
@@ -52,6 +56,50 @@ function executeInstallScript(fixture) {
}
describe('install-node-dependencies action', () => {
it.each(['/home/runner/pnpm store/v11', 'C:\\Users\\runner\\pnpm store\\v11'])(
'preserves setup-node store path %s and lowercase architecture',
(storePath) => {
const fixture = createFixture()
const output = join(fixture.root, 'github-output')
try {
const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], {
env: {
...process.env,
GITHUB_OUTPUT: output,
LOCKFILE_HASH: 'lockfile-digest',
PNPM_TEST_STORE_PATH: storePath,
PATH: `${fixture.bin}${delimiter}${process.env.PATH}`
}
})
expect(result.status, result.stderr || result.stdout).toBe(0)
expect(readFileSync(output, 'utf8')).toBe(`path=${storePath}\narch=${process.arch}\n`)
} finally {
rmSync(fixture.root, { recursive: true, force: true })
}
}
)
it.each([
['', 'store'],
['lockfile-digest', '']
])('rejects a missing lockfile hash or store path (%s, %s)', (hash, storePath) => {
const fixture = createFixture()
try {
const result = run('bash', ['-e', '-o', 'pipefail', '-c', storeScript], {
env: {
...process.env,
GITHUB_OUTPUT: join(fixture.root, 'github-output'),
LOCKFILE_HASH: hash,
PNPM_TEST_STORE_PATH: storePath,
PATH: `${fixture.bin}${delimiter}${process.env.PATH}`
}
})
expect(result.status).toBe(1)
} finally {
rmSync(fixture.root, { recursive: true, force: true })
}
})
it('skips the lockfile diff when a job container has no Git metadata', () => {
const fixture = createFixture()
try {
@@ -116,8 +116,12 @@ it('leaves the switch out of every other workflow, so only a release can set it'
const MOBILE_WORKFLOWS = ['mobile.yml', 'mobile-android-release.yml', 'mobile-ios-release.yml']
/** Paths under which a Metro or Expo build cache lives, in the spellings a workflow would use. */
const BUNDLER_CACHE_PATHS = ['metro-cache', '.expo', 'node_modules/.cache']
/** The one restored path these workflows compute in a script, and so this test cannot read. */
const REVIEWED_COMPUTED_PATH = '${{ steps.electron-package-cache.outputs.cache-root }}'
/** Store/archive paths computed by scripts rather than declared in the workflows. */
const REVIEWED_COMPUTED_PATHS = [
'${{ steps.electron-package-cache.outputs.cache-root }}',
'${{ steps.pnpm-store.outputs.path }}',
"${{ github.event_name != 'pull_request' && 'pnpm' || '' }} store"
]
/** Every step a workflow runs, descending into the repository's own composite actions. */
function stepsIncludingComposites(file) {
@@ -175,7 +179,7 @@ describe('what the mobile jobs restore from cache', () => {
it('reads every restored path, rather than passing one it cannot evaluate', () => {
const computed = MOBILE_CACHE_RESTORES.filter(({ paths }) => paths.includes('${{'))
expect(computed.map(({ paths }) => paths)).toEqual(computed.map(() => REVIEWED_COMPUTED_PATH))
expect(computed.filter(({ paths }) => !REVIEWED_COMPUTED_PATHS.includes(paths))).toEqual([])
})
it('restores no Metro or Expo build cache, which would decide the shell before the env does', () => {
+10 -1
View File
@@ -3,6 +3,15 @@ import { join } from 'node:path'
const root = join(import.meta.dirname, '..', '..')
export const ORCAD_ENTRY_POINT = 'src/main/orcad/main.ts'
export const ORCAD_CHILD_ENTRY_POINTS = {
watcher: 'src/main/ipc/parcel-watcher-process-entry.ts',
daemon: 'src/main/daemon/daemon-entry.ts',
ptyGate: 'src/main/daemon/pty-subprocess/windows-bun-pty-gate-entry.ts',
writer: 'src/main/persistence/profile-state/profile-state-writer-worker-entry.ts',
backup: 'src/main/persistence/profile-state/profile-state-backup-worker-entry.ts'
}
export const ORCAD_EXTERNAL_MODULES = [
'electron',
'node-pty',
@@ -32,7 +41,7 @@ const jsoncParserEsm = {
export function buildOrcadEntry(outfile) {
return build({
entryPoints: [join(root, 'src/main/orcad/main.ts')],
entryPoints: [join(root, ORCAD_ENTRY_POINT)],
bundle: true,
platform: 'node',
target: 'node18',
+5 -2
View File
@@ -517,9 +517,12 @@ describe('PR Checks skip wiring', () => {
expect(classify.run).toContain('--merge-base "$BASE_SHA" "$HEAD_SHA"')
expect(classify.run).toContain('node config/scripts/pr-code-change-scope.mjs')
expect(classify.run).toContain('tee -a "$GITHUB_OUTPUT"')
for (const jobName of ['should_run', 'native_cache_changed', ...expensiveJobs]) {
expect(prWorkflow.jobs.code_paths.outputs.should_run).toBe(
'${{ steps.filter.outputs.should_run }}'
)
for (const jobName of ['native_cache_changed', ...expensiveJobs]) {
expect(prWorkflow.jobs.code_paths.outputs[jobName], jobName).toBe(
`\${{ steps.filter.outputs.${jobName} }}`
`\${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.${jobName} }}`
)
}
})
@@ -0,0 +1,66 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { runProcess } from '../../src/shared/child-process/run-process'
import { PR_CHECK_JOBS } from './pr-code-change-scope.mjs'
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
const gate = workflow.jobs.verify.steps.find((step) => step.name === 'Require successful checks')
const variable = (job) => job.replaceAll('-', '_').toUpperCase()
function requiredResults(shouldRun) {
return Object.fromEntries(
PR_CHECK_JOBS.flatMap((job) => [
[variable(job), shouldRun ? 'success' : 'skipped'],
[`${variable(job)}_SHOULD_RUN`, String(shouldRun)]
])
)
}
async function verify(results) {
return runProcess({
program: 'bash',
args: ['-c', gate.run],
env: {
...process.env,
ORCA_BACKGROUND_LAUNCH: '1',
CODE_PATHS: 'success',
SHOULD_RUN: 'true',
...results
},
timeoutMs: 10_000
})
}
// The aggregate runs as Bash on Linux; Windows has no required Bash installation.
describe.skipIf(process.platform === 'win32')(
'readiness reuse through the real aggregate gate',
() => {
it('accepts completed required checks or skips authorized by exact-source evidence', async () => {
expect((await verify(requiredResults(true))).code).toBe(0)
expect((await verify(requiredResults(false))).code).toBe(0)
})
it('rejects every missing, failed or cancelled required result when reuse is unavailable', async () => {
for (const job of PR_CHECK_JOBS) {
for (const result of ['', 'skipped', 'failure', 'cancelled']) {
const verdict = await verify({ ...requiredResults(true), [variable(job)]: result })
expect(verdict.code, `${job}: ${result}`).toBe(1)
}
}
})
it('requires the detector to succeed even when every downstream job is skipped', async () => {
for (const result of ['', 'skipped', 'failure', 'cancelled']) {
expect((await verify({ ...requiredResults(false), CODE_PATHS: result })).code).toBe(1)
}
})
it('rejects unexpected downstream execution when the proven plan requires skips', async () => {
for (const job of PR_CHECK_JOBS) {
const verdict = await verify({ ...requiredResults(false), [variable(job)]: 'success' })
expect(verdict.code, job).toBe(1)
}
})
}
)
+99
View File
@@ -0,0 +1,99 @@
import { appendFileSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
export function prCheckRunTitle({ number, sourceSha, workflowSha }) {
return `PR ${number} | source ${sourceSha} | workflow ${workflowSha}`
}
export function reusablePrCheckRun(runs, identity) {
if (
!Number.isSafeInteger(identity.number) ||
identity.number < 1 ||
![identity.sourceSha, identity.workflowSha, identity.headSha].every((sha) =>
/^[a-f0-9]{40}$/.test(sha ?? '')
)
) {
return undefined
}
return runs.find(
(run) =>
Number.isSafeInteger(run.id) &&
run.id > 0 &&
String(run.id) !== identity.runId &&
run.path === '.github/workflows/pr.yml' &&
run.event === 'pull_request' &&
run.status === 'completed' &&
run.conclusion === 'success' &&
run.head_sha === identity.headSha &&
run.display_title === prCheckRunTitle(identity)
)
}
export async function lookupReadyCheckRun(env, event, request = fetch) {
if (
env.GITHUB_EVENT_NAME !== 'pull_request' ||
event.action !== 'ready_for_review' ||
event.pull_request?.draft !== false
) {
return undefined
}
const identity = {
number: event.pull_request?.number,
headSha: event.pull_request?.head?.sha,
sourceSha: env.GITHUB_SHA,
workflowSha: env.PR_CHECK_WORKFLOW_SHA,
runId: env.GITHUB_RUN_ID
}
const repository = env.GITHUB_REPOSITORY
if (!/^[\w.-]+\/[\w.-]+$/.test(repository ?? '') || !env.GH_TOKEN) {
return undefined
}
const url = new URL(
`/repos/${repository}/actions/workflows/pr.yml/runs`,
env.GITHUB_API_URL ?? 'https://api.github.com'
)
url.search = new URLSearchParams({
event: 'pull_request',
head_sha: identity.headSha ?? '',
status: 'success',
per_page: '20'
}).toString()
try {
const response = await request(url, {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${env.GH_TOKEN}`,
'User-Agent': 'orca-pr-ready-check-reuse',
'X-GitHub-Api-Version': '2022-11-28'
},
signal: AbortSignal.timeout(10_000)
})
if (!response.ok) {
throw new Error(`Run lookup returned HTTP ${response.status}`)
}
const result = await response.json()
if (!Array.isArray(result.workflow_runs)) {
throw new Error('Run lookup omitted workflow runs')
}
return reusablePrCheckRun(result.workflow_runs, identity)
} catch (error) {
console.log(`No reusable result: ${error.message}; running the required checks.`)
return undefined
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
let run
try {
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))
run = await lookupReadyCheckRun(process.env, event)
} catch (error) {
console.log(`No reusable result: ${error.message}; running the required checks.`)
}
appendFileSync(process.env.GITHUB_OUTPUT, `reused=${Boolean(run)}\nrun_id=${run?.id ?? ''}\n`)
console.log(
run
? `Required checks already passed for this source and workflow: ${run.html_url}`
: 'No identical successful run found; running the required checks.'
)
}
@@ -0,0 +1,166 @@
import { readFileSync } from 'node:fs'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { parse } from 'yaml'
import { PR_CHECK_JOBS } from './pr-code-change-scope.mjs'
import {
lookupReadyCheckRun,
prCheckRunTitle,
reusablePrCheckRun
} from './pr-ready-check-reuse.mjs'
const identity = {
number: 42,
sourceSha: 'a'.repeat(40),
workflowSha: 'b'.repeat(40),
headSha: 'c'.repeat(40),
runId: '456'
}
const passed = {
id: 123,
path: '.github/workflows/pr.yml',
event: 'pull_request',
status: 'completed',
conclusion: 'success',
head_sha: identity.headSha,
display_title: prCheckRunTitle(identity)
}
const event = {
action: 'ready_for_review',
pull_request: { number: 42, draft: false, head: { sha: identity.headSha } }
}
const env = {
GITHUB_EVENT_NAME: 'pull_request',
GITHUB_SHA: identity.sourceSha,
PR_CHECK_WORKFLOW_SHA: identity.workflowSha,
GITHUB_RUN_ID: identity.runId,
GITHUB_REPOSITORY: 'stablyai/orca',
GH_TOKEN: 'read-only-test-token'
}
const response = (runs) => ({ ok: true, json: async () => ({ workflow_runs: runs }) })
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
afterEach(() => vi.restoreAllMocks())
describe('ready-for-review required check reuse', () => {
it('reuses a completed success only for the identical PR, merge source and workflow', () => {
expect(reusablePrCheckRun([passed], identity)).toBe(passed)
expect(reusablePrCheckRun([], identity)).toBeUndefined()
for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha', 'runId']) {
const changed = key === 'number' ? 43 : key === 'runId' ? '123' : 'd'.repeat(40)
expect(reusablePrCheckRun([passed], { ...identity, [key]: changed }), key).toBeUndefined()
}
for (const key of ['number', 'sourceSha', 'workflowSha', 'headSha']) {
expect(reusablePrCheckRun([passed], { ...identity, [key]: undefined }), key).toBeUndefined()
}
})
it.each([
{ status: 'in_progress' },
{ status: 'queued' },
{ conclusion: 'failure' },
{ conclusion: 'cancelled' },
{ conclusion: 'skipped' },
{ event: 'push' },
{ path: '.github/workflows/other.yml' },
{ display_title: 'Previous workflow without source provenance' },
{ id: -1 }
])('rejects unusable evidence %j', (change) => {
expect(reusablePrCheckRun([{ ...passed, ...change }], identity)).toBeUndefined()
})
it('bounds the read-only lookup to this workflow and PR head', async () => {
const request = vi.fn(async () => response([passed]))
expect(await lookupReadyCheckRun(env, event, request)).toBe(passed)
expect(request).toHaveBeenCalledTimes(1)
const [url, options] = request.mock.calls[0]
expect(url.origin).toBe('https://api.github.com')
expect(url.pathname).toBe('/repos/stablyai/orca/actions/workflows/pr.yml/runs')
expect(Object.fromEntries(url.searchParams)).toEqual({
event: 'pull_request',
head_sha: identity.headSha,
status: 'success',
per_page: '20'
})
expect(options.method).toBeUndefined()
expect(options.signal).toBeInstanceOf(AbortSignal)
})
it('runs ordinary pushes and draft checks without consulting old results', async () => {
const request = vi.fn()
for (const action of ['opened', 'synchronize', 'reopened', 'converted_to_draft']) {
expect(await lookupReadyCheckRun(env, { ...event, action }, request)).toBeUndefined()
}
expect(
await lookupReadyCheckRun(
env,
{ ...event, pull_request: { ...event.pull_request, draft: true } },
request
)
).toBeUndefined()
expect(await lookupReadyCheckRun({ ...env, GH_TOKEN: '' }, event, request)).toBeUndefined()
expect(
await lookupReadyCheckRun({ ...env, GITHUB_EVENT_NAME: 'push' }, event, request)
).toBeUndefined()
expect(request).not.toHaveBeenCalled()
})
it('falls back to full checks when the API is unavailable or evidence is malformed', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {})
for (const request of [
async () => ({ ok: false, status: 403 }),
async () => ({ ok: false, status: 429 }),
async () => ({ ok: true, json: async () => ({}) }),
async () => {
throw new Error('Network unavailable')
}
]) {
expect(await lookupReadyCheckRun(env, event, request)).toBeUndefined()
}
expect(await lookupReadyCheckRun(env, event, async () => response([]))).toBeUndefined()
})
it('keeps required skips conditional on proof and leaves advisory routing eligible', () => {
expect(workflow['run-name']).toBe(
'PR ${{ github.event.pull_request.number }} | source ${{ github.sha }} | workflow ${{ github.workflow_sha }}'
)
expect(workflow.on.pull_request.types).toContain('ready_for_review')
const detector = workflow.jobs.code_paths
expect(detector.permissions).toEqual({ contents: 'read', actions: 'read' })
const readiness = detector.steps.find((step) => step.id === 'readiness')
expect(readiness.if).toBe("github.event.action == 'ready_for_review'")
expect(readiness.env.PR_CHECK_WORKFLOW_SHA).toBe('${{ github.workflow_sha }}')
expect(readiness.run).toBe('node config/scripts/pr-ready-check-reuse.mjs')
expect(detector.steps[0].with['sparse-checkout']).toContain(
'/config/scripts/pr-ready-check-reuse.mjs'
)
for (const job of ['native_cache_changed', ...PR_CHECK_JOBS]) {
expect(detector.outputs[job]).toBe(
`\${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.${job} }}`
)
}
expect(detector.outputs.should_run).toBe('${{ steps.filter.outputs.should_run }}')
for (const name of [
'test_files',
'ssh_source_changed',
'native_ime_source_changed',
'wsl_source_changed'
]) {
expect(detector.outputs[name]).toBe(`\${{ steps.e2e_filter.outputs.${name} }}`)
}
expect(detector.steps.find((step) => step.id === 'e2e_filter').if).toBe(
"github.event.pull_request.draft != true && steps.filter.outputs.should_run == 'true'"
)
expect(workflow.jobs.verify.if).toBe('${{ !cancelled() }}')
expect(workflow.jobs.verify.needs).toEqual(['code_paths', ...PR_CHECK_JOBS])
})
it.each(['pr-test-loc.yml', 'mobile.yml'])(
'does not rerun draft-independent %s on readiness changes',
(file) => {
const source = readFileSync(`.github/workflows/${file}`, 'utf8')
const independent = parse(source)
expect(independent.on.pull_request.types).toEqual(['opened', 'synchronize', 'reopened'])
expect(source).not.toContain('pull_request.draft')
}
)
})
@@ -269,11 +269,20 @@ describe('PR workflow parallelism', () => {
expect(steps[pnpmIndex].uses).toBe('pnpm/setup@v2')
expect(steps[pnpmIndex].with.version).toBeUndefined()
expect(steps[pnpmIndex].with.install).toBe(false)
expect(steps[nodeIndex].with.cache).toBe('pnpm')
const saveOutsidePrs = "${{ github.event_name != 'pull_request' && 'pnpm' || '' }}"
expect(steps[nodeIndex].with.cache).toBe(saveOutsidePrs)
expect(steps[nodeIndex].if).toBe("inputs.node-version == ''")
expect(steps[requestedNodeIndex].if).toBe("inputs.node-version != ''")
expect(steps[requestedNodeIndex].with['node-version']).toBe('${{ inputs.node-version }}')
expect(steps[requestedNodeIndex].with.cache).toBe('pnpm')
expect(steps[requestedNodeIndex].with.cache).toBe(saveOutsidePrs)
const restoreIndex = steps.findIndex(
(step) => step.name === 'Restore pnpm download store without saving'
)
expect(restoreIndex).toBeGreaterThan(requestedNodeIndex)
expect(restoreIndex).toBeLessThan(
steps.findIndex((step) => step.name === 'Install dependencies')
)
expect(steps[restoreIndex].uses).toBe('actions/cache/restore@v5')
})
it('uses the repository package-manager version for every direct pnpm setup', () => {
+2 -24
View File
@@ -12,6 +12,7 @@ import {
} from '../../src/shared/orcad-profile-preflight.ts'
import { currentTarget } from './build-orcad-bun.mjs'
import { runProcessSync } from './script-child-process.mjs'
import { bunProfileTestPaths } from './bun-profile-test-paths.mjs'
const root = resolve(import.meta.dirname, '../..')
const target = currentTarget()
@@ -69,28 +70,5 @@ run(runtimePath, [
'run',
'--config',
'config/vitest.config.ts',
...(testArgs.length > 0
? testArgs
: [
'src/main/persistence/profile-state',
'src/main/persistence/loading-store/profile-state',
'src/main/sqlite',
'src/main/orcad/orcad-entry.test.ts',
'src/main/orcad/orcad-push-startup.test.ts',
...(artifact
? [
'src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-job-control.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-process-suspension.test.ts',
'src/main/daemon/pty-subprocess-spawn-file-foreground.test.ts',
'src/main/daemon/pty-subprocess/spawn-file-foreground-rejected-agents.test.ts',
'tests/e2e/daemon-running-work-probe.unit.test.ts',
'src/main/daemon/pty-subprocess/windows-bun-pty-gate.integration.test.ts',
'src/main/providers/local-pty-bun-artifact.integration.test.ts',
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
'src/main/orcad/orcad-bun-launcher.integration.test.ts',
'config/scripts/zip-extractor-command.test.mjs'
]
: [])
])
...(testArgs.length > 0 ? testArgs : bunProfileTestPaths({ artifact }))
])
+37
View File
@@ -1,5 +1,42 @@
# CI efficiency and runner capacity
## Four follow-up changes
- Keep the readiness event, but reuse required checks only after an Actions API
lookup proves that the same PR head, tested merge commit, and workflow commit
already completed successfully. A changed base, missing proof, failed lookup,
or still-running check falls back to the full checks. Advisory tests retain
their normal readiness routing. The mobile and line-count workflows have no
draft-dependent work, so they no longer run again when a draft becomes ready.
- Route the Bun matrix using the actual headless build and selected tests'
transitive imports, with conservative inclusion for dynamic workers, native
inputs, fixtures, and toolchain changes. A graph failure runs the full matrix;
manual dispatch still runs all ten platform jobs. The shared test selectors
retain the same 85 files. Unrelated shard timings and mobile-test tooling can
skip the matrix; shared shortcut definitions remain real runtime dependencies
and still run it. Building the graph does not execute the imported modules.
- Restore pnpm stores on PRs using setup-node's existing key and store path,
without publishing more PR-private copies. Non-PR setup-node caching and
native/TypeScript caches keep their existing behavior. A missing main store
still installs with the frozen lockfile. The mixed root/mobile store may miss
repeatedly because the existing main warmer only seeds the root lockfile.
- Batch only the PowerShell quota-fixture reservations within each test, using
the original generated scripts in fresh local scopes. Commands under test
retain separate processes, real file identities, and existing race assertions.
A traced local run confirms 44 PowerShell starts become 24, with all 21 cases
passing. Alternating after/before/after elapsed times were 50.00/59.28/37.00
seconds on a shared macOS arm64 host; that variance does not justify a precise
percentage or hosted runner-time claim. Test budgets and worker counts are
unchanged.
The reproducible pnpm-store comparison is
`ORCA_BACKGROUND_LAUNCH=1 node config/scripts/ci-pnpm-store-benchmark.mjs --samples=3`.
On macOS arm64 with BSD tar, three alternating fresh-store pairs eliminated a
median 332,746,995-byte archive per miss. Median install time was 17.33 seconds
before and 16.94 after; the removed archive step alone took 53.51 seconds.
Those local disk/CPU measurements exclude uploads and are not a prediction of
Linux or Windows hosted savings. Restore cost is common to both policies.
## September 26 verification
[PR #23053](https://github.com/stablyai/orca/pull/23053) was merged before its
@@ -15,6 +15,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { getRemoteHostPlatform, type RemoteHostPlatform } from './ssh-remote-platform'
import { powerShellCommand, powerShellLiteral } from './ssh-remote-powershell'
import {
cleanupOwnedRelayUploadStageCommand,
parseReservedRelayUploadStage,
@@ -84,6 +85,22 @@ function createStage(
const reservedOwner = /^\.sftp-namespace-[0-9a-f]{32}$/u.test(stageOwner) ? stageOwner : owner
const reservation = runCommand(host, reserveRelayUploadStageCommand(host, pool, reservedOwner))
expect(reservation.status, reservation.stderr).toBe(0)
return populateReservedStage(
pool,
index,
stageOwner === reservedOwner ? undefined : stageOwner,
stale,
state
)
}
function populateReservedStage(
pool: string,
index: number,
replacementOwner?: string,
stale = false,
state: 'slot' | 'claim' | 'delete' = 'slot'
): string {
const slot = join(pool, `slot-${index}`)
const stage = join(pool, `${state}-${index}`)
if (stage !== slot) {
@@ -91,8 +108,8 @@ function createStage(
renameSync(slot, stage)
}
const marker = join(stage, '.orca-upload-owner')
if (stageOwner !== reservedOwner) {
writeFileSync(marker, stageOwner)
if (replacementOwner !== undefined) {
writeFileSync(marker, replacementOwner)
}
writeFileSync(join(stage, 'payload', 'relay.js'), `relay-${index}`)
if (stale) {
@@ -102,16 +119,48 @@ function createStage(
return stage
}
function createQuotaStages(host: RemoteHostPlatform, pool: string, count: number): void {
if (host.commandDialect !== 'powershell' || count < 2) {
for (let index = 0; index < count; index += 1) {
createStage(host, pool, index)
}
return
}
const command = reserveRelayUploadStageCommand(host, pool, owner)
// Only fixture setup shares a process; each unmodified script gets a fresh local scope.
const batch = powerShellCommand(
[
"$ErrorActionPreference = 'Stop'",
`$fixtureScript = ${powerShellLiteral(decodePowerShellCommand(command))}`,
'$reservation = [PowerShell]::Create()',
'try {',
`foreach ($fixtureIndex in 1..${count}) {`,
'$reservation.Commands.Clear()',
'$reservation.Streams.Error.Clear()',
'$null = $reservation.AddScript($fixtureScript, $true)',
'$reservation.Invoke()',
'if ($reservation.InvocationStateInfo.State -ne "Completed") { throw $reservation.InvocationStateInfo.Reason }',
'}',
'} finally { $reservation.Dispose() }'
].join('\n')
)
const result = runCommand(host, batch)
expect(result.status, result.stderr).toBe(0)
const reservations = result.stdout.trim().split(/\r?\n/u)
expect(reservations).toHaveLength(count)
for (const [index, output] of reservations.entries()) {
expect(parseReservedRelayUploadStage(host, pool, owner, output).slotName).toBe(`slot-${index}`)
populateReservedStage(pool, index)
}
}
afterEach(() => {
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
// Why: every case spawns a real interpreter per command, and on non-Windows hosts the PowerShell
// path also forks `/usr/bin/stat` per file-identity lookup — the 8-entry reservation alone measured
// ~50s idle, nearly all of it process-spawn sys time, and the full suite multiplies that under CPU
// contention. Sized for spawn count, not the assertions, which run in microseconds.
// Allow for real interpreter startup and file-identity probes under full-suite contention.
const SPAWNED_INTERPRETER_TIMEOUT_MS = 240_000
describe.each([
@@ -126,9 +175,7 @@ describe.each([
(_label, host) => {
it.each([0, 1, 7, 8, 9])('bounds reservation with %i occupied entries', (count) => {
const pool = createPool()
for (let index = 0; index < Math.min(count, RELAY_UPLOAD_STAGE_SLOT_COUNT); index += 1) {
createStage(host, pool, index)
}
createQuotaStages(host, pool, Math.min(count, RELAY_UPLOAD_STAGE_SLOT_COUNT))
if (count > RELAY_UPLOAD_STAGE_SLOT_COUNT) {
mkdirSync(join(pool, 'foreign-extra'))
}