Files
orca/config/scripts/orcad-windows-prebuild-cache.test.mjs
T
OrcaWinandOrcaWin 2cd3e6d6a3 ci: narrow churny cache keys and move PR-local cache saves to main (#34600)
* ci: narrow native, launcher and packaging cache keys and stop PR-local saves

* ci: hash Rust crate sources, not build outputs, in launcher and hook keys

* ci: fail closed on unlocked native packages and cover transitive key inputs

* ci: resolve the Windows Rust cache scope with a script, not a nested local action

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-27 18:14:29 -07:00

311 lines
11 KiB
JavaScript

import {
cpSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync
} from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
import { sha256Of } from './orcad-prebuild-slot-contents.mjs'
import {
validateWindowsPrebuildCache,
windowsPrebuildCacheIdentity,
WINDOWS_PREBUILD_CACHE_INPUTS
} from './orcad-windows-prebuild-cache.mjs'
import { peImage } from './windows-pe-image-fixture.mjs'
const require = createRequire(import.meta.url)
const { CYGWIN_BREAKAWAY_MARKER, NATIVE_HOST_JOB_MARKER } = require('./node-pty-job-ownership.cjs')
const temporary = []
afterEach(() => {
for (const dir of temporary.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function fixture(arch = 'x64') {
const dir = mkdtempSync(join(tmpdir(), 'orca-windows-prebuild-cache-'))
temporary.push(dir)
const sourceDir = join(dir, 'source')
const prebuildsDir = join(dir, 'prebuilds')
const slot = `win32-${arch}`
const slotDir = join(prebuildsDir, slot)
const write = (path, bytes) => {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, bytes)
}
write(join(sourceDir, 'package.json'), '{"version":"1.1.0"}')
write(join(sourceDir, 'binding.gyp'), '--no-as-needed,-l:libutil.so.1')
write(join(sourceDir, 'src/unix/pty.cc'), '.symver openpty,openpty@')
write(join(sourceDir, 'src/win/conpty.cc'), 'L"msys-2.0.dll"; "assignCurrentProcessToStrictJob"')
mkdirSync(join(sourceDir, 'third_party/conpty/1', `win10-${arch}`), { recursive: true })
const files = {}
for (const [file, bytes] of [
[
'conpty.node',
Buffer.concat([peImage({ arch }), CYGWIN_BREAKAWAY_MARKER, NATIVE_HOST_JOB_MARKER])
],
['conpty_console_list.node', peImage({ arch })],
['conpty/conpty.dll', peImage({ arch })],
['conpty/OpenConsole.exe', peImage({ arch })],
['pty.node', peImage({ arch })],
['winpty.dll', peImage({ arch })],
['winpty-agent.exe', peImage({ arch })]
]) {
write(join(slotDir, file), bytes)
files[file] = sha256Of(join(slotDir, file))
if (file !== 'conpty.node' && !file.startsWith('conpty/')) {
write(join(sourceDir, 'prebuilds', slot, file), bytes)
}
if (file.startsWith('conpty/')) {
write(
join(sourceDir, 'third_party/conpty/1', `win10-${arch}`, file.slice('conpty/'.length)),
bytes
)
}
}
const entry = { platform: 'win32', arch, libc: 'none', glibc: null, napi: 8, files }
const manifest = {
schemaVersion: 2,
module: 'node-pty',
version: '1.1.0',
napi: 8,
nodeHeaders: NODE_RUNTIME_PIN.version,
slots: { [slot]: entry }
}
const save = () => writeFileSync(join(prebuildsDir, 'manifest.json'), JSON.stringify(manifest))
save()
return { sourceDir, prebuildsDir, slot, slotDir, entry, manifest, save, platform: 'win32', arch }
}
describe('Windows server prebuild cache validation', () => {
it.each(['x64', 'arm64'])('accepts a complete current %s payload', (arch) => {
const f = fixture(arch)
expect(validateWindowsPrebuildCache(f)).toBe(f.slot)
expect(() => validateWindowsPrebuildCache({ ...f, platform: 'linux' })).toThrow(
/requires win32/
)
})
it.each([
['module', 'upstream'],
['version', '0.0.0'],
['napi', 10],
['nodeHeaders', '24.0.0'],
['schemaVersion', 1]
])('refuses a changed manifest %s', (field, value) => {
const f = fixture()
f.manifest[field] = value
f.save()
expect(() => validateWindowsPrebuildCache(f)).toThrow(/manifest does not match/)
})
it.each([
['arch', 'arm64'],
['platform', 'linux'],
['libc', 'glibc'],
['glibc', '2.28'],
['napi', 10]
])('refuses a changed slot %s', (field, value) => {
const f = fixture()
f.entry[field] = value
f.save()
expect(() => validateWindowsPrebuildCache(f)).toThrow(/metadata does not match/)
})
it('refuses missing, extra and unlisted payloads before trusting their hashes', () => {
const f = fixture()
delete f.entry.files['conpty/conpty.dll']
f.save()
expect(() => validateWindowsPrebuildCache(f)).toThrow(/exactly the current ConPTY payload/)
const extra = fixture()
writeFileSync(join(extra.slotDir, 'conpty.pdb'), 'upstream symbols')
expect(() => validateWindowsPrebuildCache(extra)).toThrow(/exactly the current ConPTY payload/)
const missing = fixture()
rmSync(join(missing.slotDir, 'conpty/OpenConsole.exe'))
expect(() => validateWindowsPrebuildCache(missing)).toThrow(
/exactly the current ConPTY payload/
)
})
it('refuses modified bytes even when the file inventory is complete', () => {
const f = fixture()
writeFileSync(join(f.slotDir, 'conpty/conpty.dll'), 'tampered')
expect(() => validateWindowsPrebuildCache(f)).toThrow(/sha256 does not match/)
})
it('refuses unexpected empty directories and payload symlinks', () => {
const f = fixture()
mkdirSync(join(f.slotDir, 'upstream'))
expect(() => validateWindowsPrebuildCache(f)).toThrow(/exactly the current ConPTY payload/)
rmSync(join(f.slotDir, 'upstream'), { recursive: true })
const path = join(f.slotDir, 'conpty.node')
const target = join(f.prebuildsDir, 'outside.node')
cpSync(path, target)
rmSync(path)
symlinkSync(target, path)
expect(() => validateWindowsPrebuildCache(f)).toThrow(/cannot contain symlinks/)
})
it('compares vendored payload bytes with the current installed dependency', () => {
const f = fixture()
writeFileSync(
join(f.sourceDir, 'third_party/conpty/1/win10-x64/conpty.dll'),
'different vendor payload'
)
expect(() => validateWindowsPrebuildCache(f)).toThrow(/differs from the current vendored/)
})
it.each([
[
'wrong PE architecture',
Buffer.concat([peImage({ arch: 'arm64' }), CYGWIN_BREAKAWAY_MARKER]),
/targets win32-x64/
],
['old breakaway policy', peImage({ arch: 'x64' }), /predates the Cygwin/],
[
'old native-host ownership',
Buffer.concat([peImage({ arch: 'x64' }), CYGWIN_BREAKAWAY_MARKER]),
/lacks strict native-host process ownership/
],
[
'post-baseline N-API import',
Buffer.concat([
peImage({ arch: 'x64' }),
CYGWIN_BREAKAWAY_MARKER,
Buffer.from('node_api_symbol_for')
]),
/above N-API 8/
]
])('refuses %s even with a matching cached digest', (_name, bytes, error) => {
const f = fixture()
writeFileSync(join(f.slotDir, 'conpty.node'), bytes)
f.entry.files['conpty.node'] = sha256Of(join(f.slotDir, 'conpty.node'))
f.save()
expect(() => validateWindowsPrebuildCache(f)).toThrow(error)
})
it('checks both current source guards independently of cached payload bytes', () => {
const f = fixture()
writeFileSync(join(f.sourceDir, 'src/win/conpty.cc'), 'upstream')
expect(() => validateWindowsPrebuildCache(f)).toThrow(/source.*does not carry/)
writeFileSync(
join(f.sourceDir, 'src/win/conpty.cc'),
'L"msys-2.0.dll"; "assignCurrentProcessToStrictJob"'
)
writeFileSync(join(f.sourceDir, 'binding.gyp'), 'upstream')
expect(() => validateWindowsPrebuildCache(f)).toThrow(/patch is not applied/)
})
})
describe('Windows server prebuild cache inputs', () => {
const options = { platform: 'win32', arch: 'x64', imageOS: 'win22' }
it('separates host architecture and image without fallback keys', () => {
const identity = windowsPrebuildCacheIdentity(options)
expect(identity.key).toMatch(/^orcad-windows-prebuild-v2-win32-x64-[a-f0-9]{64}$/)
expect(windowsPrebuildCacheIdentity(options)).toEqual(identity)
for (const override of [{ arch: 'arm64' }, { imageOS: 'win25' }]) {
expect(windowsPrebuildCacheIdentity({ ...options, ...override }).key).not.toBe(identity.key)
}
expect(() => windowsPrebuildCacheIdentity({ ...options, imageOS: '' })).toThrow(/ImageOS/)
})
const compilerInputs = [
...[
'run-process',
'spawn-resolution',
'process-tree-termination',
'process-tree-kill-gate',
'spawn-observer',
'bounded-output-sink',
'growing-byte-buffer',
'child-termination-reporter',
'process-spec',
'windows-command-line',
'windows-cmd-shim-resolution',
'windows-system-binary',
'windows-tree-kill'
].map((name) => `src/packages/process-host/src/${name}.ts`),
'src/packages/process-host/.gitignore',
'src/packages/process-host/package.json',
'src/packages/process-host/scripts/build-dist.mjs',
'src/packages/process-host/tsconfig.json'
]
const cacheInputs = [...new Set([...WINDOWS_PREBUILD_CACHE_INPUTS, ...compilerInputs])]
function repositoryCopy() {
const repository = mkdtempSync(join(tmpdir(), 'orca-prebuild-key-'))
temporary.push(repository)
const source = resolve(import.meta.dirname, '../..')
for (const relative of [
...cacheInputs,
'package.json',
'pnpm-lock.yaml',
'pnpm-workspace.yaml'
]) {
mkdirSync(dirname(join(repository, relative)), { recursive: true })
cpSync(join(source, relative), join(repository, relative))
}
return repository
}
it('ignores app versions and unrelated manifest and lockfile edits but not the build scripts', () => {
const repository = repositoryCopy()
const key = () => windowsPrebuildCacheIdentity({ ...options, repository }).key
const before = key()
const manifestPath = join(repository, 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
writeFileSync(
manifestPath,
JSON.stringify({
...manifest,
version: '999.0.0',
dependencies: { ...manifest.dependencies, unrelated: '1.0.0' }
})
)
const lockPath = join(repository, 'pnpm-lock.yaml')
writeFileSync(lockPath, `${readFileSync(lockPath, 'utf8')}\n unrelated-package@1.0.0: {}\n`)
expect(key()).toBe(before)
writeFileSync(
manifestPath,
JSON.stringify({
...manifest,
scripts: { ...manifest.scripts, 'build:orcad-prebuilds': 'changed' }
})
)
expect(key()).not.toBe(before)
})
it('invalidates a changed node-pty or node-gyp lock entry', () => {
const repository = repositoryCopy()
const before = windowsPrebuildCacheIdentity({ ...options, repository }).key
const lockPath = join(repository, 'pnpm-lock.yaml')
const lock = readFileSync(lockPath, 'utf8')
for (const [from, to] of [
[/(\n {2}node-pty@[^\n]+:\n {4}dependencies:\n {6}node-addon-api: )[^\n]+/, '$17.1.0'],
[/(\n {2}node-gyp@[^\n]+:\n {4}dependencies:\n)/, '$1 zwitch: 2.0.4\n']
]) {
expect(lock).toMatch(from)
writeFileSync(lockPath, lock.replace(from, to))
expect(windowsPrebuildCacheIdentity({ ...options, repository }).key).not.toBe(before)
}
})
it.each(cacheInputs)('invalidates a changed %s input', (file) => {
const repository = repositoryCopy()
const before = windowsPrebuildCacheIdentity({ ...options, repository }).key
writeFileSync(
join(repository, file),
`${readFileSync(join(repository, file), 'utf8')}\nchanged input`
)
expect(windowsPrebuildCacheIdentity({ ...options, repository }).key).not.toBe(before)
})
})