mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 00:02:29 +00:00
* ci: avoid unrelated headless server qualification * ci: skip headless detection for ineligible draft PRs * ci: preserve cross-host qualification and skip supplied prerequisites * ci: include Windows server cache validation in change detection
233 lines
9.1 KiB
JavaScript
233 lines
9.1 KiB
JavaScript
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { runInNewContext } from 'node:vm'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
import { runProcessSync } from './script-child-process.mjs'
|
|
|
|
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
|
const steps = workflow.jobs.persistence.steps
|
|
const identity = steps.find((step) => step.id === 'orcad-prebuild-cache-identity')
|
|
const restore = steps.find((step) => step.id === 'orcad-prebuild-cache-restore')
|
|
const build = steps.find((step) => step.name?.startsWith('Build and smoke this runner'))
|
|
const save = steps.find((step) => step.uses === 'actions/cache/save@v5')
|
|
|
|
function evaluate(expression, context) {
|
|
return runInNewContext(
|
|
expression.replaceAll(
|
|
'steps.orcad-prebuild-cache-identity',
|
|
'steps["orcad-prebuild-cache-identity"]'
|
|
),
|
|
context
|
|
)
|
|
}
|
|
|
|
function context(os, arch, event, ref, template = false) {
|
|
return {
|
|
runner: { os, arch },
|
|
github: { event_name: event, ref },
|
|
inputs: { build_template: template, ref: '' },
|
|
steps: {
|
|
'orcad-prebuild-cache-identity': { outcome: 'success', outputs: { key: 'exact-key' } }
|
|
},
|
|
success: () => true,
|
|
fromJSON: JSON.parse,
|
|
contains: (values, value) => values.includes(value)
|
|
}
|
|
}
|
|
|
|
describe('Windows server prebuild cache workflow', () => {
|
|
it.each([
|
|
['pull_request', 'refs/pull/1/merge', false, true, false],
|
|
['push', 'refs/heads/main', false, true, true],
|
|
['schedule', 'refs/heads/main', false, false, true],
|
|
['workflow_dispatch', 'refs/heads/main', false, false, true],
|
|
['workflow_call', 'refs/heads/main', false, false, false],
|
|
['push', 'refs/tags/v1', false, false, false],
|
|
['workflow_dispatch', 'refs/heads/feature', false, false, false],
|
|
['pull_request', 'refs/pull/1/merge', true, false, false],
|
|
['workflow_dispatch', 'refs/heads/main', true, false, false]
|
|
])('%s %s template=%s restores=%s saves=%s', (event, ref, template, reads, writes) => {
|
|
for (const [os, arch] of [
|
|
['Windows', 'X64'],
|
|
['Windows', 'ARM64'],
|
|
['Windows', 'X86'],
|
|
['Linux', 'X64'],
|
|
['macOS', 'ARM64']
|
|
]) {
|
|
const ctx = context(os, arch, event, ref, template)
|
|
const windows = os === 'Windows' && ['X64', 'ARM64'].includes(arch)
|
|
const resolves = evaluate(identity.if, ctx)
|
|
expect(resolves).toBe(windows && (reads || writes))
|
|
ctx.steps['orcad-prebuild-cache-identity'].outcome = resolves ? 'success' : 'skipped'
|
|
expect(evaluate(restore.if, ctx)).toBe(windows && reads)
|
|
expect(evaluate(save.if, ctx)).toBe(windows && writes)
|
|
ctx.success = () => false
|
|
expect(evaluate(save.if, ctx)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('treats missing identities and explicit release refs as fresh builds', () => {
|
|
for (const event of ['pull_request', 'push']) {
|
|
const ctx = context('Windows', 'X64', event, 'refs/heads/main')
|
|
for (const outcome of ['failure', 'skipped']) {
|
|
ctx.steps['orcad-prebuild-cache-identity'].outcome = outcome
|
|
expect(evaluate(restore.if, ctx)).toBe(false)
|
|
expect(evaluate(save.if, ctx)).toBe(false)
|
|
}
|
|
ctx.steps['orcad-prebuild-cache-identity'] = { outcome: 'success', outputs: { key: '' } }
|
|
expect(evaluate(restore.if, ctx)).toBe(false)
|
|
expect(evaluate(save.if, ctx)).toBe(false)
|
|
ctx.inputs.ref = 'refs/tags/v1'
|
|
expect(evaluate(identity.if, ctx)).toBe(false)
|
|
expect(evaluate(save.if, ctx)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it.each(['push', 'schedule', 'workflow_dispatch', 'pull_request'])(
|
|
'keeps reusable release/template calls fresh under the inherited %s event',
|
|
(event) => {
|
|
for (const inputs of [
|
|
{ build_template: true, ref: '' },
|
|
{ build_template: false, ref: 'refs/tags/v1' },
|
|
{ build_template: true, ref: 'refs/tags/v1' }
|
|
]) {
|
|
const ctx = context('Windows', 'X64', event, 'refs/heads/main')
|
|
ctx.inputs = inputs
|
|
expect(evaluate(identity.if, ctx)).toBe(false)
|
|
expect(evaluate(save.if, ctx)).toBe(false)
|
|
ctx.steps['orcad-prebuild-cache-identity'].outcome = 'skipped'
|
|
expect(evaluate(restore.if, ctx)).toBe(false)
|
|
}
|
|
}
|
|
)
|
|
|
|
it('restores only the exact key and saves after every qualification gate', () => {
|
|
expect(identity['continue-on-error']).toBe(true)
|
|
expect(identity.run).toBe('node config/scripts/orcad-windows-prebuild-cache.mjs --fingerprint')
|
|
expect(restore.uses).toBe('actions/cache/restore@v5')
|
|
expect(restore['continue-on-error']).toBe(true)
|
|
expect(restore.with['restore-keys']).toBeUndefined()
|
|
expect(restore.with).toEqual(save.with)
|
|
expect(restore.with.key).toBe('${{ steps.orcad-prebuild-cache-identity.outputs.key }}')
|
|
expect(restore.with.path).toBe('${{ steps.orcad-prebuild-cache-identity.outputs.path }}')
|
|
expect(steps.indexOf(restore)).toBeLessThan(steps.indexOf(build))
|
|
expect(save['continue-on-error']).toBe(true)
|
|
expect(build.if).toBeUndefined()
|
|
expect(build['continue-on-error']).toBeUndefined()
|
|
for (const gate of steps.filter((step) =>
|
|
/require-slots|--smoke|pnpm build:orcad$|test:node-server|--orcad-smoke-load-check/.test(
|
|
step.run ?? ''
|
|
)
|
|
)) {
|
|
expect(steps.indexOf(save)).toBeGreaterThan(steps.indexOf(gate))
|
|
expect(gate['continue-on-error']).toBeUndefined()
|
|
}
|
|
for (const lane of [
|
|
'linux_glibc_floor',
|
|
'linux_glibc217_compat',
|
|
'linux_musl',
|
|
'desktop_template'
|
|
]) {
|
|
expect(
|
|
workflow.jobs[lane].steps.some((step) => step.id?.startsWith('orcad-prebuild-cache'))
|
|
).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
const commandMocks = `
|
|
node() {
|
|
echo "node $*" >> "$COMMAND_LOG"
|
|
case "$*" in
|
|
*--validate)
|
|
IFS= read -r verdict < out/orcad-prebuilds/manifest.json
|
|
[ "$verdict" = valid ] ;;
|
|
*--print-slot) echo win32-x64 ;;
|
|
*) return 41 ;;
|
|
esac
|
|
}
|
|
pnpm() {
|
|
echo "pnpm $*" >> "$COMMAND_LOG"
|
|
if [ "$*" = build:orcad-prebuilds ]; then
|
|
if [ "$RUNNER_OS" = Windows ]; then
|
|
[ ! -e out/orcad-prebuilds/old-payload ] || return 42
|
|
fi
|
|
[ "$COMPILE_STATUS" = 0 ] || return "$COMPILE_STATUS"
|
|
mkdir -p out/orcad-prebuilds
|
|
echo valid > out/orcad-prebuilds/manifest.json
|
|
fi
|
|
}
|
|
`
|
|
|
|
function runBuild({ os = 'Windows', hit = '', outcome = '', manifest = 'valid', compile = '0' }) {
|
|
const directory = mkdtempSync(join(tmpdir(), 'orca-windows-prebuild-workflow-'))
|
|
const log = join(directory, 'commands')
|
|
try {
|
|
mkdirSync(join(directory, 'out/orcad-prebuilds'), { recursive: true })
|
|
writeFileSync(join(directory, 'out/orcad-prebuilds/manifest.json'), `${manifest}\n`)
|
|
writeFileSync(join(directory, 'out/orcad-prebuilds/old-payload'), 'old payload')
|
|
writeFileSync(join(directory, 'out/preserve-other-output'), 'keep')
|
|
writeFileSync(log, '')
|
|
const result = runProcessSync({
|
|
program: 'bash',
|
|
args: ['-e', '-o', 'pipefail', '-c', `${commandMocks}\n${build.run}`],
|
|
cwd: directory,
|
|
env: {
|
|
...process.env,
|
|
ORCA_BACKGROUND_LAUNCH: '1',
|
|
RUNNER_OS: os,
|
|
WINDOWS_PREBUILD_CACHE_HIT: hit,
|
|
WINDOWS_PREBUILD_CACHE_RESTORE_OUTCOME: outcome,
|
|
COMMAND_LOG: log,
|
|
COMPILE_STATUS: compile
|
|
},
|
|
timeoutMs: 10_000
|
|
})
|
|
expect(readFileSync(join(directory, 'out/preserve-other-output'), 'utf8')).toBe('keep')
|
|
return { result, commands: readFileSync(log, 'utf8').trim().split('\n') }
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
describe.skipIf(process.platform === 'win32')('Windows prebuild Bash fallback', () => {
|
|
it.each([
|
|
['warm valid', 'true', 'success', 'valid', false, true],
|
|
['invalid manifest', 'true', 'success', '{broken', true, true],
|
|
['restore error', 'true', 'failure', 'valid', true, false],
|
|
['cache miss', 'false', 'success', 'valid', true, false],
|
|
['missing identity', '', '', 'valid', true, false]
|
|
])('%s retains fresh gates', (_name, hit, outcome, manifest, compiles, validates) => {
|
|
const { result, commands } = runBuild({ hit, outcome, manifest })
|
|
expect(result.code).toBe(0)
|
|
expect(commands.includes('pnpm build:orcad-prebuilds')).toBe(compiles)
|
|
expect(
|
|
commands.includes('node config/scripts/orcad-windows-prebuild-cache.mjs --validate')
|
|
).toBe(validates)
|
|
expect(commands.slice(-3)).toEqual([
|
|
'node config/scripts/build-orcad-prebuilds.mjs --print-slot',
|
|
'pnpm build:orcad-prebuilds --require-slots win32-x64',
|
|
'pnpm build:orcad-prebuilds --smoke'
|
|
])
|
|
})
|
|
|
|
it.each(['Linux', 'macOS'])('%s keeps its fresh compilation and other output', (os) => {
|
|
const { result, commands } = runBuild({ os, hit: 'true', outcome: 'success' })
|
|
expect(result.code).toBe(0)
|
|
expect(commands[0]).toBe('pnpm build:orcad-prebuilds')
|
|
})
|
|
|
|
it('stops qualification when fallback compilation fails', () => {
|
|
const { result, commands } = runBuild({
|
|
hit: 'true',
|
|
outcome: 'success',
|
|
manifest: '{broken',
|
|
compile: '9'
|
|
})
|
|
expect(result.code).toBe(9)
|
|
expect(commands.at(-1)).toBe('pnpm build:orcad-prebuilds')
|
|
})
|
|
})
|