mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* build: share one pnpm virtual store across worktrees, off under CI
Each worktree carried a full node_modules; the global virtual store links
one slot per package version instead. pnpm keeps an explicit setting on
under CI, so a pnpmfile hook turns it off there: CI packages from,
caches and verifies a project-local node_modules.
* fix(build): keep shared electron and windows-process-tree dirs untouched under GVS
Every worktree runs Electron from the one shared electron package, so the
per-repo dist cache must not swap that dist and a foreign-arch install must
not land in it. The relay addon builder now repairs and compiles a private
copy instead of the shared windows-process-tree dir.
* fix(build): accept CLI runtime imports reached through an in-artifact store link
Under the global virtual store node_modules/<pkg> realpaths into a store
outside the repo. The check still rejects anything not reached through a
node_modules link inside the artifact, such as a stray ~/node_modules.
* docs: explain the global virtual store layout and the ~/node_modules hazard
* test: compare CLI closure paths with POSIX separators so the suite passes on Windows
* build: keep Windows on the normal pnpm layout unless ORCA_PNPM_GLOBAL_VIRTUAL_STORE=1
Store links are true symlinks on Windows, and installs without admin or
Developer Mode are unverified; CI still forces the normal layout first.
* fix(build): close the remaining shared-slot gaps under the global virtual store
- verify-skills-cli-runtime resolves each import from the importer's realpath,
as Node does, so an artifact-local copy can't mask a ~/node_modules load.
- A concurrent sibling install of the shared Electron dist is kept, not swapped out.
- rebuild:node goes through ensure-native-runtime, which builds a shared
node-pty beside the slot instead of node-gyp cleaning its build/ dir.
* fix(build): keep pnpm rebuild:node a forced rebuild
It replaced pnpm rebuild node-pty, which always rebuilt; without --force it
only rebuilt a node-pty that failed to load.
* docs: note that the store switches must stay set, since pnpm run relinks
* build: move the pnpmfile under config/ to keep the repo root allowlisted
pnpm-workspace.yaml points pnpm at it; the lockfile checksum is unchanged
because the content is.
* fix(build): enable the global virtual store from the pnpmfile, not the workspace file
pnpm exec derives NODE_PATH from the file setting, so even with the store
forced off under CI every child process got the root node_modules on its
NODE_PATH; a Windows rebuild fixture then resolved the real
@orca/windows-registry it deliberately omits. The hook now sets the store
both ways, and ORCA_PNPM_GLOBAL_VIRTUAL_STORE=0/1 is the one switch.
* test(pty): keep F3's concurrent spawns on the node-pty mock
Two concurrent first import('node-pty') calls race vitest's mock registry
and the loser loads the real addon under the mocked linux platform. It
passed only where a platform-agnostic build/Release happened to exist,
so it failed on every shared-store worktree.
* ci: fail PRs whose packages need a dependency they never declared
A new dependency_isolation job installs with the global virtual store
forced on (ORCA_PNPM_FORCE_GLOBAL_VIRTUAL_STORE_IN_CI), HOME and the store
in an empty temp dir, then runs pnpm tc and the undeclared-dependency test.
Postinstall now warns when a node_modules above the shared store, such as
~/node_modules, would mask the same class of bug locally.
* ci: set the isolation job's HOME and store through GITHUB_ENV
Job-level env cannot read the runner context, which made pr.yml invalid.
172 lines
5.5 KiB
JavaScript
172 lines
5.5 KiB
JavaScript
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 { dirname, 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 '@orca/process-host'
|
||
import { workspacePackageManifests } from './workspace-source-exports.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',
|
||
// The lockfile pins its checksum, so a frozen install fails without it.
|
||
'config/pnpmfile.cjs',
|
||
...workspacePackageManifests(repository)
|
||
]
|
||
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 resetModules() {
|
||
const packageDirs = new Set(
|
||
manifests.filter((file) => file.endsWith('package.json')).map(dirname)
|
||
)
|
||
for (const directory of packageDirs) {
|
||
rmSync(join(checkout, directory, 'node_modules'), { recursive: true, force: true })
|
||
}
|
||
}
|
||
|
||
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, '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) {
|
||
resetModules()
|
||
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.
|
||
resetModules()
|
||
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 })
|
||
}
|