mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* 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>
148 lines
5.1 KiB
JavaScript
148 lines
5.1 KiB
JavaScript
import { createHash } from 'node:crypto'
|
|
import { readFileSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import { parse, parseAllDocuments } from 'yaml'
|
|
|
|
// Packages whose locked dependency closure is compiled into, or compiles, the cached addons.
|
|
export const NATIVE_MODULE_BUILD_PACKAGES = [
|
|
'node-pty',
|
|
'@vscode/windows-process-tree',
|
|
'@electron/rebuild',
|
|
'node-gyp'
|
|
]
|
|
// Only the release matters: it fixes the ABI and headers; its downloader closure does not.
|
|
export const NATIVE_MODULE_TARGET_PACKAGES = ['electron']
|
|
export const NATIVE_MODULE_WORKSPACES = ['native/windows-registry', 'native/windows-security']
|
|
// Resolution-only settings, plus patches, whose native effect already shows in snapshot ids.
|
|
const RESOLUTION_ONLY_WORKSPACE_SETTINGS = new Set([
|
|
'addMissingPeerTypes',
|
|
'catalog',
|
|
'catalogs',
|
|
'ignoredOptionalDependencies',
|
|
'minimumReleaseAge',
|
|
'minimumReleaseAgeExclude',
|
|
'overrides',
|
|
'packageExtensions',
|
|
'patchedDependencies'
|
|
])
|
|
|
|
/** `@scope/name@1.0.0(peer@2.0.0)` -> `@scope/name`. */
|
|
function snapshotName(key) {
|
|
const bare = key.replace(/\(.*$/, '')
|
|
return bare.slice(0, bare.lastIndexOf('@'))
|
|
}
|
|
|
|
function versionOf(entry) {
|
|
return typeof entry === 'string' ? entry : entry?.version
|
|
}
|
|
|
|
/** pnpm 12 splits the lockfile into a package-manager document and the project one; merge both. */
|
|
function readLockfile(contents) {
|
|
const importers = {}
|
|
const snapshots = {}
|
|
for (const document of parseAllDocuments(contents)) {
|
|
if (document.errors.length) {
|
|
throw document.errors[0]
|
|
}
|
|
const lockfile = document.toJS() ?? {}
|
|
for (const [path, importer] of Object.entries(lockfile.importers ?? {})) {
|
|
importers[path] = { ...importers[path], ...importer }
|
|
}
|
|
Object.assign(snapshots, lockfile.snapshots)
|
|
}
|
|
return { importers, snapshots }
|
|
}
|
|
|
|
/** Sorted snapshot ids (versions and patch hashes) a set of native packages build from. */
|
|
export function nativeDependencyClosure(
|
|
contents,
|
|
{ packages, versionOnly = [], workspaces = [] } = {}
|
|
) {
|
|
const { importers, snapshots } = readLockfile(contents)
|
|
const names = Object.keys(snapshots).map(snapshotName)
|
|
const unlocked = [...packages, ...versionOnly].filter((name) => !names.includes(name))
|
|
if (unlocked.length > 0) {
|
|
// Fail closed: a package that silently stopped matching would drop out of the key.
|
|
throw new Error(`pnpm-lock.yaml locks none of ${unlocked.join(', ')}`)
|
|
}
|
|
const pending = Object.keys(snapshots).filter((key) => packages.includes(snapshotName(key)))
|
|
for (const workspace of workspaces) {
|
|
const importer = importers[workspace]
|
|
if (!importer) {
|
|
throw new Error(`pnpm-lock.yaml has no importer for ${workspace}`)
|
|
}
|
|
for (const field of ['dependencies', 'optionalDependencies', 'devDependencies']) {
|
|
for (const [name, entry] of Object.entries(importer[field] ?? {})) {
|
|
const version = versionOf(entry)
|
|
if (!version.startsWith('link:')) {
|
|
pending.push(`${name}@${version}`)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const visited = new Set()
|
|
while (pending.length > 0) {
|
|
const key = pending.pop()
|
|
if (visited.has(key)) {
|
|
continue
|
|
}
|
|
const snapshot = snapshots[key]
|
|
if (!snapshot) {
|
|
throw new Error(`pnpm-lock.yaml has no snapshot for ${key}`)
|
|
}
|
|
visited.add(key)
|
|
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
for (const [name, version] of Object.entries(snapshot[field] ?? {})) {
|
|
pending.push(`${name}@${version}`)
|
|
}
|
|
}
|
|
}
|
|
for (const key of Object.keys(snapshots)) {
|
|
if (versionOnly.includes(snapshotName(key))) {
|
|
visited.add(key.replace(/\(.*$/, ''))
|
|
}
|
|
}
|
|
return [...visited].sort()
|
|
}
|
|
|
|
/** pnpm-workspace.yaml minus resolution-only settings, with build approvals for `closure` only. */
|
|
export function nativeWorkspaceSettings(contents, closure) {
|
|
const names = new Set(closure.map(snapshotName))
|
|
return Object.fromEntries(
|
|
Object.entries(parse(contents) ?? {})
|
|
.filter(([setting]) => !RESOLUTION_ONLY_WORKSPACE_SETTINGS.has(setting))
|
|
.map(([setting, value]) =>
|
|
setting === 'allowBuilds' && value && typeof value === 'object'
|
|
? [setting, Object.fromEntries(Object.entries(value).filter(([name]) => names.has(name)))]
|
|
: [setting, value]
|
|
)
|
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
)
|
|
}
|
|
|
|
export function nativeDependencyLockDigest({
|
|
repository = resolve(import.meta.dirname, '../..'),
|
|
...selection
|
|
}) {
|
|
const closure = nativeDependencyClosure(
|
|
readFileSync(join(repository, 'pnpm-lock.yaml'), 'utf8'),
|
|
selection
|
|
)
|
|
const settings = nativeWorkspaceSettings(
|
|
readFileSync(join(repository, 'pnpm-workspace.yaml'), 'utf8'),
|
|
closure
|
|
)
|
|
const digest = createHash('sha256').update(JSON.stringify({ closure, settings })).digest('hex')
|
|
return { digest, closure }
|
|
}
|
|
|
|
if (process.argv[1]?.endsWith('native-dependency-lock-digest.mjs')) {
|
|
const { digest, closure } = nativeDependencyLockDigest({
|
|
packages: NATIVE_MODULE_BUILD_PACKAGES,
|
|
versionOnly: NATIVE_MODULE_TARGET_PACKAGES,
|
|
workspaces: NATIVE_MODULE_WORKSPACES
|
|
})
|
|
console.error(`[native-cache] locked inputs: ${closure.join(' ')}`)
|
|
console.log(digest)
|
|
}
|