Files
orca/config/scripts/ci-pnpm-store-benchmark.mjs
T
Neil 4fc1855bda build: share one pnpm virtual store across worktrees (off in CI and on Windows) (#30254)
* 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.
2026-10-18 20:36:24 -07:00

172 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 })
}