mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 08:02:12 +00:00
feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check (#24087)
* feat(runtime): pin the Node 24.21.0 server runtime with an offline CI check Add src/shared/node-runtime-pin.ts (NODE_RUNTIME_PIN, SERVER_TARGETS, NODE_RUNTIME_ASSETS for all 8 server targets plus the headers tarball), generated by config/scripts/update-node-runtime-pin.mjs from the nodejs.org and unofficial-builds SHASUMS. check-node-runtime-pin.mjs verifies, with no network, that the pin tracks the locked Electron, matches engines.node's major, and covers exactly SERVER_TARGETS; it runs in the static analysis job. ORCAD_BUN_TARGETS consumers now read SERVER_TARGETS so there is one target list; orcad's Bun runtime and build output are unchanged. * fix(runtime): reject a pinned archive that belongs to another target --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
This commit is contained in:
@@ -267,6 +267,9 @@ jobs:
|
||||
- name: Enforce runtime Electron-import ratchet
|
||||
run: pnpm run check:runtime-electron-ratchet
|
||||
|
||||
- name: Check Node runtime pin
|
||||
run: pnpm run check:node-runtime-pin
|
||||
|
||||
# Why: extraction writes sorted evidence to an isolated temporary path,
|
||||
# so feature PRs need one normalized AST pass rather than a three-OS matrix.
|
||||
- name: Verify localization extraction
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
orcadTemplateCommonFilenames
|
||||
} from '../../src/shared/orcad-artifacts.ts'
|
||||
import { orcadAgentBrowserNativeName } from '../../src/shared/orcad-agent-browser-name.ts'
|
||||
import { ORCAD_TEMPLATE_TARGETS } from '../../src/shared/orcad-bun-runtime.ts'
|
||||
import { ORCAD_TEMPLATE_TARGETS } from '../../src/shared/node-runtime-pin.ts'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { materializeWatcherPackage } from './orcad-watcher-package.mjs'
|
||||
import { verifyPackagedOrcadTemplate } from './verify-packaged-orcad-template.cjs'
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env node
|
||||
// Static, offline consistency gate for src/shared/node-runtime-pin.ts; update-node-runtime-pin.mjs owns the network.
|
||||
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseAllDocuments } from 'yaml'
|
||||
import {
|
||||
NODE_RUNTIME_ASSETS,
|
||||
NODE_RUNTIME_PIN,
|
||||
SERVER_TARGETS
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import { nodeDistArchiveName } from './update-node-runtime-pin.mjs'
|
||||
|
||||
const SHA256 = /^[0-9a-f]{64}$/
|
||||
const ASSET_SOURCES = new Set(['official', 'unofficial'])
|
||||
|
||||
function majorOf(range) {
|
||||
const match = /(\d+)/.exec(String(range ?? ''))
|
||||
return match ? Number(match[1]) : null
|
||||
}
|
||||
|
||||
/** Strips pnpm's peer suffix: `43.7.5(supports-color@7.2.0)` -> `43.7.5`. */
|
||||
function lockedVersion(entry) {
|
||||
const version = typeof entry === 'string' ? entry : entry?.version
|
||||
return typeof version === 'string' ? version.replace(/\(.*$/, '') : null
|
||||
}
|
||||
|
||||
/** pnpm 12 splits the lockfile into a package-manager document and the project one; merge both. */
|
||||
export function lockfileRootImporter(contents) {
|
||||
const importer = {}
|
||||
for (const document of parseAllDocuments(contents)) {
|
||||
if (document.errors.length) {
|
||||
throw document.errors[0]
|
||||
}
|
||||
Object.assign(importer, document.toJS()?.importers?.['.'])
|
||||
}
|
||||
return importer
|
||||
}
|
||||
|
||||
export function findNodeRuntimePinProblems({ pin, assets, targets, packageJson, rootImporter }) {
|
||||
const problems = []
|
||||
const declaredElectron =
|
||||
packageJson.devDependencies?.electron ?? packageJson.dependencies?.electron
|
||||
if (declaredElectron !== pin.electron) {
|
||||
problems.push(
|
||||
`package.json electron is ${declaredElectron}, but NODE_RUNTIME_PIN.electron is ${pin.electron}`
|
||||
)
|
||||
}
|
||||
const lockedElectron = lockedVersion(
|
||||
rootImporter.devDependencies?.electron ?? rootImporter.dependencies?.electron
|
||||
)
|
||||
if (lockedElectron !== pin.electron) {
|
||||
problems.push(
|
||||
`pnpm-lock.yaml resolves electron ${lockedElectron}, but NODE_RUNTIME_PIN.electron is ${pin.electron}`
|
||||
)
|
||||
}
|
||||
// Only the major is gated here; whether the pin may differ from Electron's Node is design D1
|
||||
// (docs/reference/node-runtime-design.html).
|
||||
const engineMajor = majorOf(packageJson.engines?.node)
|
||||
if (majorOf(pin.version) !== engineMajor) {
|
||||
problems.push(
|
||||
`NODE_RUNTIME_PIN.version ${pin.version} is not package.json engines.node major ${engineMajor}`
|
||||
)
|
||||
}
|
||||
if (!Number.isInteger(pin.napi) || pin.napi < 1) {
|
||||
problems.push(`NODE_RUNTIME_PIN.napi must be a positive integer, got ${pin.napi}`)
|
||||
}
|
||||
if (!SHA256.test(pin.headers?.sha256 ?? '')) {
|
||||
problems.push('NODE_RUNTIME_PIN.headers.sha256 is not a 64-character hex SHA-256')
|
||||
}
|
||||
if (pin.headers?.file !== `node-v${pin.version}-headers.tar.gz`) {
|
||||
problems.push(`NODE_RUNTIME_PIN.headers.file ${pin.headers?.file} is not for ${pin.version}`)
|
||||
}
|
||||
|
||||
const expected = new Set(targets)
|
||||
for (const target of targets) {
|
||||
if (!Object.hasOwn(assets, target)) {
|
||||
problems.push(`NODE_RUNTIME_ASSETS has no entry for ${target}`)
|
||||
}
|
||||
}
|
||||
for (const [target, asset] of Object.entries(assets)) {
|
||||
if (!expected.has(target)) {
|
||||
problems.push(`NODE_RUNTIME_ASSETS has ${target}, which is not in SERVER_TARGETS`)
|
||||
continue
|
||||
}
|
||||
if (!ASSET_SOURCES.has(asset.source)) {
|
||||
problems.push(`${target}: source must be official or unofficial, got ${asset.source}`)
|
||||
}
|
||||
const expectedArchive = nodeDistArchiveName(pin.version, target)
|
||||
if (asset.archive !== expectedArchive) {
|
||||
problems.push(`${target}: archive ${asset.archive} is not ${expectedArchive}`)
|
||||
}
|
||||
if (!SHA256.test(asset.archiveSha256 ?? '')) {
|
||||
problems.push(`${target}: archiveSha256 is not a 64-character hex SHA-256`)
|
||||
}
|
||||
if (!SHA256.test(asset.executableSha256 ?? '')) {
|
||||
problems.push(`${target}: executableSha256 is not a 64-character hex SHA-256`)
|
||||
}
|
||||
if (!Number.isInteger(asset.executableSize) || asset.executableSize <= 0) {
|
||||
problems.push(`${target}: executableSize must be a positive integer`)
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
export function main(root = resolve(import.meta.dirname, '../..')) {
|
||||
const problems = findNodeRuntimePinProblems({
|
||||
pin: NODE_RUNTIME_PIN,
|
||||
assets: NODE_RUNTIME_ASSETS,
|
||||
targets: SERVER_TARGETS,
|
||||
packageJson: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')),
|
||||
rootImporter: lockfileRootImporter(readFileSync(join(root, 'pnpm-lock.yaml'), 'utf8'))
|
||||
})
|
||||
if (problems.length > 0) {
|
||||
console.error('Node runtime pin check failed:')
|
||||
for (const problem of problems) {
|
||||
console.error(`- ${problem}`)
|
||||
}
|
||||
console.error(
|
||||
'Regenerate with: node config/scripts/update-node-runtime-pin.mjs --version <x.y.z>'
|
||||
)
|
||||
return 1
|
||||
}
|
||||
console.log(
|
||||
`Node runtime pin check passed: Node ${NODE_RUNTIME_PIN.version} for Electron ${NODE_RUNTIME_PIN.electron}.`
|
||||
)
|
||||
return 0
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
process.exit(main())
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import {
|
||||
NODE_RUNTIME_ASSETS,
|
||||
NODE_RUNTIME_PIN,
|
||||
SERVER_TARGETS
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import {
|
||||
findNodeRuntimePinProblems,
|
||||
lockfileRootImporter,
|
||||
main
|
||||
} from './check-node-runtime-pin.mjs'
|
||||
|
||||
const projectDir = path.resolve(import.meta.dirname, '../..')
|
||||
const HASH = 'a'.repeat(64)
|
||||
|
||||
function validInput() {
|
||||
const pin = {
|
||||
version: '24.21.0',
|
||||
electron: '43.7.5',
|
||||
napi: 10,
|
||||
headers: { file: 'node-v24.21.0-headers.tar.gz', sha256: HASH }
|
||||
}
|
||||
const targets = ['linux-x64-glibc', 'win32-x64']
|
||||
const assets = {
|
||||
'linux-x64-glibc': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-linux-x64.tar.gz',
|
||||
archiveSha256: HASH,
|
||||
executableSha256: HASH,
|
||||
executableSize: 1
|
||||
},
|
||||
'win32-x64': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-win-x64.zip',
|
||||
archiveSha256: HASH,
|
||||
executableSha256: HASH,
|
||||
executableSize: 1
|
||||
}
|
||||
}
|
||||
return {
|
||||
pin,
|
||||
assets,
|
||||
targets,
|
||||
packageJson: { devDependencies: { electron: '43.7.5' }, engines: { node: '24' } },
|
||||
rootImporter: {
|
||||
devDependencies: {
|
||||
electron: { specifier: '43.7.5', version: '43.7.5(supports-color@7.2.0)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('findNodeRuntimePinProblems', () => {
|
||||
it('accepts a consistent pin', () => {
|
||||
expect(findNodeRuntimePinProblems(validInput())).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects an Electron bump that the pin did not follow', () => {
|
||||
const input = validInput()
|
||||
input.packageJson.devDependencies.electron = '43.8.0'
|
||||
input.rootImporter.devDependencies.electron.version = '43.8.0'
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'package.json electron is 43.8.0, but NODE_RUNTIME_PIN.electron is 43.7.5',
|
||||
'pnpm-lock.yaml resolves electron 43.8.0, but NODE_RUNTIME_PIN.electron is 43.7.5'
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects a lockfile that resolves a different Electron than package.json', () => {
|
||||
const input = validInput()
|
||||
input.rootImporter.devDependencies.electron.version = '43.7.6'
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'pnpm-lock.yaml resolves electron 43.7.6, but NODE_RUNTIME_PIN.electron is 43.7.5'
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects a pin major that differs from engines.node', () => {
|
||||
const input = validInput()
|
||||
input.packageJson.engines.node = '>=26'
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'NODE_RUNTIME_PIN.version 24.21.0 is not package.json engines.node major 26'
|
||||
])
|
||||
})
|
||||
|
||||
it('requires exactly one asset per server target', () => {
|
||||
const input = validInput()
|
||||
delete input.assets['win32-x64']
|
||||
input.assets['freebsd-x64'] = input.assets['linux-x64-glibc']
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'NODE_RUNTIME_ASSETS has no entry for win32-x64',
|
||||
'NODE_RUNTIME_ASSETS has freebsd-x64, which is not in SERVER_TARGETS'
|
||||
])
|
||||
})
|
||||
|
||||
it('rejects malformed hashes, sizes, sources and stale archive names', () => {
|
||||
const input = validInput()
|
||||
input.pin.headers.sha256 = 'ABC'
|
||||
input.assets['linux-x64-glibc'] = {
|
||||
source: 'mirror',
|
||||
archive: 'node-v24.20.0-linux-x64.tar.gz',
|
||||
archiveSha256: HASH.toUpperCase(),
|
||||
executableSha256: `${HASH}0`,
|
||||
executableSize: 0
|
||||
}
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'NODE_RUNTIME_PIN.headers.sha256 is not a 64-character hex SHA-256',
|
||||
'linux-x64-glibc: source must be official or unofficial, got mirror',
|
||||
'linux-x64-glibc: archive node-v24.20.0-linux-x64.tar.gz is not node-v24.21.0-linux-x64.tar.gz',
|
||||
'linux-x64-glibc: archiveSha256 is not a 64-character hex SHA-256',
|
||||
'linux-x64-glibc: executableSha256 is not a 64-character hex SHA-256',
|
||||
'linux-x64-glibc: executableSize must be a positive integer'
|
||||
])
|
||||
})
|
||||
it("rejects another target's archive", () => {
|
||||
const input = validInput()
|
||||
input.assets['win32-x64'].archive = 'node-v24.21.0-win-arm64.zip'
|
||||
expect(findNodeRuntimePinProblems(input)).toEqual([
|
||||
'win32-x64: archive node-v24.21.0-win-arm64.zip is not node-v24.21.0-win-x64.zip'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('lockfileRootImporter', () => {
|
||||
it('merges the root importer across pnpm 12 lockfile documents', () => {
|
||||
const contents = [
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' packageManagerDependencies: {}',
|
||||
'---',
|
||||
"lockfileVersion: '9.0'",
|
||||
'importers:',
|
||||
' .:',
|
||||
' devDependencies:',
|
||||
' electron:',
|
||||
' specifier: 43.7.5',
|
||||
' version: 43.7.5(supports-color@7.2.0)',
|
||||
''
|
||||
].join('\n')
|
||||
expect(lockfileRootImporter(contents).devDependencies.electron.version).toBe(
|
||||
'43.7.5(supports-color@7.2.0)'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('committed pin', () => {
|
||||
it('passes the repository check', () => {
|
||||
expect(main(projectDir)).toBe(0)
|
||||
})
|
||||
|
||||
it('covers every server target', () => {
|
||||
expect(Object.keys(NODE_RUNTIME_ASSETS).sort()).toEqual([...SERVER_TARGETS].sort())
|
||||
expect(NODE_RUNTIME_PIN.headers.file).toBe(`node-v${NODE_RUNTIME_PIN.version}-headers.tar.gz`)
|
||||
})
|
||||
|
||||
it('runs in the static analysis job', () => {
|
||||
const workflow = parse(readFileSync(path.join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
|
||||
const commands = workflow.jobs.static_analysis.steps.map((step) => step.run ?? '')
|
||||
expect(commands).toContain('pnpm run check:node-runtime-pin')
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ORCAD_RIPGREP_ARTIFACTS,
|
||||
orcadArtifactFilenames
|
||||
} from '../../src/shared/orcad-artifacts.ts'
|
||||
import { ORCAD_BUN_TARGETS } from '../../src/shared/orcad-bun-runtime.ts'
|
||||
import { SERVER_TARGETS } from '../../src/shared/node-runtime-pin.ts'
|
||||
import { orcadAgentBrowserNativeName } from '../../src/shared/orcad-agent-browser-name.ts'
|
||||
import { readOrcadArtifactIdentity } from '../../src/main/orcad/orcad-artifact-identity.ts'
|
||||
import { computeOrcadFullVersion } from './orcad-artifact-version.mjs'
|
||||
@@ -42,7 +42,7 @@ describe('standalone runtime version', () => {
|
||||
expect(() => computeOrcadFullVersion(dir)).toThrow(ORCAD_RIPGREP_ARTIFACTS[0])
|
||||
})
|
||||
|
||||
it.each(ORCAD_BUN_TARGETS)(
|
||||
it.each(SERVER_TARGETS)(
|
||||
'matches the installed %s identity with and without its optional browser',
|
||||
async (target) => {
|
||||
const dir = createArtifactDirectory(target)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ORCAD_TEMPLATE_TARGETS_DIR,
|
||||
orcadTemplateCommonFilenames
|
||||
} from '../../src/shared/orcad-artifacts.ts'
|
||||
import { ORCAD_TEMPLATE_TARGETS } from '../../src/shared/orcad-bun-runtime.ts'
|
||||
import { ORCAD_TEMPLATE_TARGETS } from '../../src/shared/node-runtime-pin.ts'
|
||||
|
||||
async function write(path, contents) {
|
||||
await mkdir(dirname(path), { recursive: true })
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createRequire } from 'node:module'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { x as extractTar } from 'tar'
|
||||
import { parseAllDocuments } from 'yaml'
|
||||
import { ORCAD_BUN_TARGETS } from '../../src/shared/orcad-bun-runtime.ts'
|
||||
import { SERVER_TARGETS } from '../../src/shared/node-runtime-pin.ts'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
@@ -22,7 +22,7 @@ export function parseWatcherLockfile(contents) {
|
||||
}
|
||||
|
||||
export function watcherPackageIdentity(target, version, lockfile) {
|
||||
if (!ORCAD_BUN_TARGETS.includes(target)) {
|
||||
if (!SERVER_TARGETS.includes(target)) {
|
||||
throw new Error(`Unsupported watcher target: ${target}`)
|
||||
}
|
||||
const name = `@parcel/watcher-${target}`
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
#!/usr/bin/env node
|
||||
// Regenerates the pinned asset table in src/shared/node-runtime-pin.ts. Needs network; CI never runs it.
|
||||
// Usage: node config/scripts/update-node-runtime-pin.mjs --version 24.21.0 [--work-dir DIR] [--keyring FILE]
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import {
|
||||
createReadStream,
|
||||
createWriteStream,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { Readable } from 'node:stream'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import {
|
||||
SERVER_TARGETS,
|
||||
nodeRuntimeExecutablePath,
|
||||
nodeRuntimeReleaseUrl
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import { currentTarget } from './build-orcad-bun.mjs'
|
||||
import { runProcessSync } from './script-child-process.mjs'
|
||||
import { getZipExtractorCommand } from './zip-extractor-command.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const PIN_FILE = join(root, 'src/shared/node-runtime-pin.ts')
|
||||
const GENERATED_BEGIN = '// @generated-begin by config/scripts/update-node-runtime-pin.mjs'
|
||||
const GENERATED_END = '// @generated-end'
|
||||
const RELEASE_KEYRING_URL =
|
||||
'https://raw.githubusercontent.com/nodejs/release-keys/HEAD/gpg/pubring.kbx'
|
||||
|
||||
/** Node's platform suffix for each server target; nodejs.org names Windows `win`, not `win32`. */
|
||||
export const NODE_DIST_PLATFORMS = {
|
||||
'darwin-arm64': 'darwin-arm64',
|
||||
'darwin-x64': 'darwin-x64',
|
||||
'linux-arm64-glibc': 'linux-arm64',
|
||||
'linux-x64-glibc': 'linux-x64',
|
||||
'linux-arm64-musl': 'linux-arm64-musl',
|
||||
'linux-x64-musl': 'linux-x64-musl',
|
||||
'win32-arm64': 'win-arm64',
|
||||
'win32-x64': 'win-x64'
|
||||
}
|
||||
|
||||
export function nodeDistArchiveName(version, target) {
|
||||
// Why .tar.gz over .tar.xz: every POSIX host can extract gzip; xz is not guaranteed.
|
||||
const extension = target.startsWith('win32-') ? 'zip' : 'tar.gz'
|
||||
return `node-v${version}-${NODE_DIST_PLATFORMS[target]}.${extension}`
|
||||
}
|
||||
|
||||
export function parseShasums(text) {
|
||||
const hashes = new Map()
|
||||
for (const line of text.split('\n')) {
|
||||
const match = /^([0-9a-f]{64}) {2}(\S+)$/.exec(line.trim())
|
||||
if (match) {
|
||||
hashes.set(match[2], match[1])
|
||||
}
|
||||
}
|
||||
return hashes
|
||||
}
|
||||
|
||||
/** Official builds win over unofficial ones when both publish the same archive. */
|
||||
export function selectAssetSource(archive, officialHashes, unofficialHashes) {
|
||||
if (officialHashes.has(archive)) {
|
||||
return { source: 'official', archiveSha256: officialHashes.get(archive) }
|
||||
}
|
||||
if (unofficialHashes.has(archive)) {
|
||||
return { source: 'unofficial', archiveSha256: unofficialHashes.get(archive) }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function parseNodeApiVersion(nodeVersionHeader) {
|
||||
const match = /#define NODE_API_SUPPORTED_VERSION_MAX (\d+)/.exec(nodeVersionHeader)
|
||||
if (!match) {
|
||||
throw new Error('node_version.h has no NODE_API_SUPPORTED_VERSION_MAX')
|
||||
}
|
||||
return Number(match[1])
|
||||
}
|
||||
|
||||
export function renderGeneratedBlock(pin, assets) {
|
||||
const lines = [
|
||||
GENERATED_BEGIN,
|
||||
'export const NODE_RUNTIME_PIN: NodeRuntimePin = {',
|
||||
` version: '${pin.version}',`,
|
||||
` electron: '${pin.electron}',`,
|
||||
` napi: ${pin.napi},`,
|
||||
' headers: {',
|
||||
` file: '${pin.headers.file}',`,
|
||||
` sha256: '${pin.headers.sha256}'`,
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
'export const NODE_RUNTIME_ASSETS: Record<ServerTarget, NodeRuntimeAsset> = {'
|
||||
]
|
||||
SERVER_TARGETS.forEach((target, index) => {
|
||||
const asset = assets[target]
|
||||
lines.push(
|
||||
` '${target}': {`,
|
||||
` source: '${asset.source}',`,
|
||||
` archive: '${asset.archive}',`,
|
||||
` archiveSha256: '${asset.archiveSha256}',`,
|
||||
` executableSha256: '${asset.executableSha256}',`,
|
||||
` executableSize: ${asset.executableSize}`,
|
||||
index === SERVER_TARGETS.length - 1 ? ' }' : ' },'
|
||||
)
|
||||
})
|
||||
lines.push('}', GENERATED_END)
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
export function replaceGeneratedBlock(source, block) {
|
||||
const begin = source.indexOf(GENERATED_BEGIN)
|
||||
const end = source.indexOf(GENERATED_END)
|
||||
if (begin === -1 || end === -1 || end < begin) {
|
||||
throw new Error('node-runtime-pin.ts is missing its @generated markers')
|
||||
}
|
||||
return source.slice(0, begin) + block + source.slice(end + GENERATED_END.length)
|
||||
}
|
||||
|
||||
function argument(name) {
|
||||
const index = process.argv.indexOf(name)
|
||||
return index === -1 ? null : process.argv[index + 1]
|
||||
}
|
||||
|
||||
async function fetchText(url) {
|
||||
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(60_000) })
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel()
|
||||
throw new Error(`GET ${url} failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
return response.text()
|
||||
}
|
||||
|
||||
async function download(url, destination) {
|
||||
const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(600_000) })
|
||||
if (!response.ok || !response.body) {
|
||||
await response.body?.cancel()
|
||||
throw new Error(`GET ${url} failed: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
await pipeline(Readable.fromWeb(response.body), createWriteStream(destination))
|
||||
}
|
||||
|
||||
async function sha256File(path) {
|
||||
const hash = createHash('sha256')
|
||||
await pipeline(createReadStream(path), hash)
|
||||
return hash.digest('hex')
|
||||
}
|
||||
|
||||
function run(program, args) {
|
||||
const result = runProcessSync({ program, args, timeoutMs: 300_000 })
|
||||
if (result.code !== 0) {
|
||||
throw new Error(
|
||||
`${program} ${args.join(' ')} exited ${result.code}: ${result.stderr || result.stdout}`
|
||||
)
|
||||
}
|
||||
return result.stdout
|
||||
}
|
||||
|
||||
function tarProgram() {
|
||||
return process.platform === 'win32'
|
||||
? join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'tar.exe')
|
||||
: 'tar'
|
||||
}
|
||||
|
||||
function extract(archivePath, destination, member) {
|
||||
mkdirSync(destination, { recursive: true })
|
||||
if (archivePath.endsWith('.zip')) {
|
||||
const command = getZipExtractorCommand(archivePath, destination)
|
||||
run(command.file, command.args)
|
||||
return
|
||||
}
|
||||
run(tarProgram(), ['-xzf', archivePath, '-C', destination, member])
|
||||
}
|
||||
|
||||
function gpgAvailable() {
|
||||
try {
|
||||
return runProcessSync({ program: 'gpg', args: ['--version'], timeoutMs: 10_000 }).code === 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyOfficialShasums(version, workDir, shasumsPath) {
|
||||
if (!gpgAvailable()) {
|
||||
console.warn(
|
||||
'\n!!! WARNING: gpg is not installed, so SHASUMS256.txt was NOT signature-verified.\n' +
|
||||
'!!! Its hashes are trusted over TLS only. Install gpg and rerun before committing a pin.\n'
|
||||
)
|
||||
return false
|
||||
}
|
||||
const signaturePath = join(workDir, 'SHASUMS256.txt.sig')
|
||||
await download(nodeRuntimeReleaseUrl('official', 'SHASUMS256.txt.sig', version), signaturePath)
|
||||
let keyring = argument('--keyring')
|
||||
if (!keyring) {
|
||||
keyring = join(workDir, 'nodejs-release-keys.kbx')
|
||||
try {
|
||||
await download(RELEASE_KEYRING_URL, keyring)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`\n!!! WARNING: could not fetch Node release keys (${error.message}); ` +
|
||||
'SHASUMS256.txt was NOT signature-verified.\n'
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const gnupgHome = join(workDir, 'gnupg')
|
||||
mkdirSync(gnupgHome, { recursive: true, mode: 0o700 })
|
||||
const result = runProcessSync({
|
||||
program: 'gpg',
|
||||
args: [
|
||||
'--homedir',
|
||||
gnupgHome,
|
||||
'--no-default-keyring',
|
||||
'--keyring',
|
||||
resolve(keyring),
|
||||
'--verify',
|
||||
signaturePath,
|
||||
shasumsPath
|
||||
],
|
||||
timeoutMs: 60_000
|
||||
})
|
||||
if (result.code !== 0) {
|
||||
throw new Error(`SHASUMS256.txt signature verification failed:\n${result.stderr}`)
|
||||
}
|
||||
console.log('Verified SHASUMS256.txt signature against the Node.js release keys.')
|
||||
return true
|
||||
}
|
||||
|
||||
async function pinTarget({ version, napi, target, workDir, officialHashes, unofficialHashes }) {
|
||||
const archive = nodeDistArchiveName(version, target)
|
||||
const selected = selectAssetSource(archive, officialHashes, unofficialHashes)
|
||||
if (!selected) {
|
||||
// Why fail: a bump must not ship with a target that has no runtime (design D1 risks).
|
||||
throw new Error(`No published ${archive} for ${target}; the pin cannot move to ${version}`)
|
||||
}
|
||||
const archivePath = join(workDir, archive)
|
||||
await download(nodeRuntimeReleaseUrl(selected.source, archive, version), archivePath)
|
||||
const actual = await sha256File(archivePath)
|
||||
if (actual !== selected.archiveSha256) {
|
||||
throw new Error(`${archive} hash ${actual} does not match SHASUMS ${selected.archiveSha256}`)
|
||||
}
|
||||
const member = nodeRuntimeExecutablePath(target, archive)
|
||||
const extracted = join(workDir, `extract-${target}`)
|
||||
extract(archivePath, extracted, member)
|
||||
const executablePath = join(extracted, member)
|
||||
const asset = {
|
||||
source: selected.source,
|
||||
archive,
|
||||
archiveSha256: selected.archiveSha256,
|
||||
executableSha256: await sha256File(executablePath),
|
||||
executableSize: statSync(executablePath).size
|
||||
}
|
||||
if (target === currentTarget()) {
|
||||
const reported = run(executablePath, [
|
||||
'-p',
|
||||
'`${process.version} ${process.versions.napi}`'
|
||||
]).trim()
|
||||
if (reported !== `v${version} ${napi}`) {
|
||||
throw new Error(`${member} reports ${reported}, expected v${version} ${napi}`)
|
||||
}
|
||||
}
|
||||
rmSync(extracted, { recursive: true, force: true })
|
||||
rmSync(archivePath, { force: true })
|
||||
console.log(`${target}: ${asset.source} ${archive} (${asset.executableSize} bytes)`)
|
||||
return asset
|
||||
}
|
||||
|
||||
async function pinHeaders(version, workDir, officialHashes) {
|
||||
const file = `node-v${version}-headers.tar.gz`
|
||||
const sha256 = officialHashes.get(file)
|
||||
if (!sha256) {
|
||||
throw new Error(`SHASUMS256.txt lists no ${file}`)
|
||||
}
|
||||
const archivePath = join(workDir, file)
|
||||
await download(nodeRuntimeReleaseUrl('official', file, version), archivePath)
|
||||
const actual = await sha256File(archivePath)
|
||||
if (actual !== sha256) {
|
||||
throw new Error(`${file} hash ${actual} does not match SHASUMS ${sha256}`)
|
||||
}
|
||||
const member = `node-v${version}/include/node/node_version.h`
|
||||
const extracted = join(workDir, 'extract-headers')
|
||||
extract(archivePath, extracted, member)
|
||||
const napi = parseNodeApiVersion(readFileSync(join(extracted, member), 'utf8'))
|
||||
return { headers: { file, sha256 }, napi }
|
||||
}
|
||||
|
||||
function pinnedElectronVersion() {
|
||||
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
||||
const declared = pkg.devDependencies?.electron ?? pkg.dependencies?.electron
|
||||
if (!/^\d+\.\d+\.\d+$/.test(declared ?? '')) {
|
||||
throw new Error(`package.json must pin an exact electron version, found ${declared}`)
|
||||
}
|
||||
return declared
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const version = argument('--version')
|
||||
if (!/^\d+\.\d+\.\d+$/.test(version ?? '')) {
|
||||
throw new Error('Usage: update-node-runtime-pin.mjs --version <major.minor.patch>')
|
||||
}
|
||||
const workParent = argument('--work-dir') ?? tmpdir()
|
||||
mkdirSync(workParent, { recursive: true })
|
||||
const workDir = mkdtempSync(join(workParent, 'orca-node-runtime-pin-'))
|
||||
try {
|
||||
const shasumsPath = join(workDir, 'SHASUMS256.txt')
|
||||
await download(nodeRuntimeReleaseUrl('official', 'SHASUMS256.txt', version), shasumsPath)
|
||||
await verifyOfficialShasums(version, workDir, shasumsPath)
|
||||
const officialHashes = parseShasums(readFileSync(shasumsPath, 'utf8'))
|
||||
const unofficialHashes = parseShasums(
|
||||
await fetchText(nodeRuntimeReleaseUrl('unofficial', 'SHASUMS256.txt', version))
|
||||
)
|
||||
const { headers, napi } = await pinHeaders(version, workDir, officialHashes)
|
||||
const assets = {}
|
||||
for (const target of SERVER_TARGETS) {
|
||||
assets[target] = await pinTarget({
|
||||
version,
|
||||
napi,
|
||||
target,
|
||||
workDir,
|
||||
officialHashes,
|
||||
unofficialHashes
|
||||
})
|
||||
}
|
||||
const pin = { version, electron: pinnedElectronVersion(), napi, headers }
|
||||
const source = readFileSync(PIN_FILE, 'utf8')
|
||||
writeFileSync(PIN_FILE, replaceGeneratedBlock(source, renderGeneratedBlock(pin, assets)))
|
||||
console.log(`Wrote ${PIN_FILE}. Run check-node-runtime-pin.mjs before committing.`)
|
||||
} finally {
|
||||
rmSync(workDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main()
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
NODE_RUNTIME_ASSETS,
|
||||
NODE_RUNTIME_PIN,
|
||||
SERVER_TARGETS,
|
||||
nodeRuntimeExecutablePath
|
||||
} from '../../src/shared/node-runtime-pin.ts'
|
||||
import {
|
||||
nodeDistArchiveName,
|
||||
parseNodeApiVersion,
|
||||
parseShasums,
|
||||
renderGeneratedBlock,
|
||||
replaceGeneratedBlock,
|
||||
selectAssetSource
|
||||
} from './update-node-runtime-pin.mjs'
|
||||
|
||||
const pinFile = path.resolve(import.meta.dirname, '../../src/shared/node-runtime-pin.ts')
|
||||
const HASH_A = 'a'.repeat(64)
|
||||
const HASH_B = 'b'.repeat(64)
|
||||
|
||||
describe('nodeDistArchiveName', () => {
|
||||
it('uses nodejs.org platform names and zip only on Windows', () => {
|
||||
expect(nodeDistArchiveName('24.21.0', 'linux-x64-glibc')).toBe('node-v24.21.0-linux-x64.tar.gz')
|
||||
expect(nodeDistArchiveName('24.21.0', 'linux-arm64-musl')).toBe(
|
||||
'node-v24.21.0-linux-arm64-musl.tar.gz'
|
||||
)
|
||||
expect(nodeDistArchiveName('24.21.0', 'win32-arm64')).toBe('node-v24.21.0-win-arm64.zip')
|
||||
})
|
||||
|
||||
it('covers every server target', () => {
|
||||
for (const target of SERVER_TARGETS) {
|
||||
expect(nodeDistArchiveName('24.21.0', target)).not.toContain('undefined')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('nodeRuntimeExecutablePath', () => {
|
||||
it('points at bin/node on POSIX and node.exe on Windows', () => {
|
||||
expect(nodeRuntimeExecutablePath('darwin-arm64', 'node-v24.21.0-darwin-arm64.tar.gz')).toBe(
|
||||
'node-v24.21.0-darwin-arm64/bin/node'
|
||||
)
|
||||
expect(nodeRuntimeExecutablePath('win32-x64', 'node-v24.21.0-win-x64.zip')).toBe(
|
||||
'node-v24.21.0-win-x64/node.exe'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseShasums and selectAssetSource', () => {
|
||||
const official = parseShasums(
|
||||
`${HASH_A} node-v24.21.0-linux-x64-musl.tar.gz\nnot a hash line\n${HASH_A} node-v24.21.0-linux-x64.tar.gz\n`
|
||||
)
|
||||
const unofficial = parseShasums(
|
||||
`${HASH_B} node-v24.21.0-linux-x64-musl.tar.gz\n${HASH_B} node-v24.21.0-linux-arm64-musl.tar.gz\n`
|
||||
)
|
||||
|
||||
it('prefers the official build when both publish an archive', () => {
|
||||
expect(selectAssetSource('node-v24.21.0-linux-x64-musl.tar.gz', official, unofficial)).toEqual({
|
||||
source: 'official',
|
||||
archiveSha256: HASH_A
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to unofficial builds and reports a missing archive as null', () => {
|
||||
expect(
|
||||
selectAssetSource('node-v24.21.0-linux-arm64-musl.tar.gz', official, unofficial)
|
||||
).toEqual({ source: 'unofficial', archiveSha256: HASH_B })
|
||||
expect(selectAssetSource('node-v24.21.0-aix-ppc64.tar.gz', official, unofficial)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseNodeApiVersion', () => {
|
||||
it('reads the highest supported N-API version from node_version.h', () => {
|
||||
expect(
|
||||
parseNodeApiVersion(
|
||||
'#define NODE_API_SUPPORTED_VERSION_MAX 10\n#define NODE_API_SUPPORTED_VERSION_MIN 1\n'
|
||||
)
|
||||
).toBe(10)
|
||||
expect(() => parseNodeApiVersion('#define NODE_MAJOR_VERSION 24')).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('generated block', () => {
|
||||
it('reproduces the committed table byte for byte', () => {
|
||||
const source = readFileSync(pinFile, 'utf8')
|
||||
const regenerated = replaceGeneratedBlock(
|
||||
source,
|
||||
renderGeneratedBlock(NODE_RUNTIME_PIN, NODE_RUNTIME_ASSETS)
|
||||
)
|
||||
expect(regenerated).toBe(source)
|
||||
})
|
||||
|
||||
it('refuses a file without markers', () => {
|
||||
expect(() => replaceGeneratedBlock('export {}\n', 'x')).toThrow(/markers/)
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,7 @@ const {
|
||||
ORCAD_TEMPLATE_TARGETS_DIR,
|
||||
orcadTemplateCommonFilenames
|
||||
} = require('../../src/shared/orcad-artifacts.ts')
|
||||
const { ORCAD_TEMPLATE_TARGETS } = require('../../src/shared/orcad-bun-runtime.ts')
|
||||
const { ORCAD_TEMPLATE_TARGETS } = require('../../src/shared/node-runtime-pin.ts')
|
||||
|
||||
const SHA256_PATTERN = /^[a-f0-9]{64}$/
|
||||
const BROWSER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@
|
||||
"audit:perf": "oxlint --config config/oxlint-performance-audit.json --format json src",
|
||||
"test:perf:contracts": "vitest run --config config/vitest.performance.config.ts",
|
||||
"format": "oxfmt --write .",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage",
|
||||
"lint": "oxlint && pnpm run audit:anti-slop && pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run check:reliability-gates && pnpm run check:dead-classes && pnpm run check:max-lines-ratchet && pnpm run check:ts-nocheck-ratchet && pnpm run check:runtime-electron-ratchet && pnpm run check:readme-local-links && pnpm run check:node-runtime-pin && pnpm run verify:rpc-params-catalog && pnpm run verify:bundled-skill-guides && pnpm run verify:skill-bundle-manifest && pnpm run verify:localization-catalogs && pnpm run verify:localization-extraction && pnpm run verify:localization-coverage",
|
||||
"audit:code-quality": "pnpm run audit:code-quality:native && pnpm run audit:code-quality:type-aware && pnpm run audit:react-doctor",
|
||||
"audit:code-quality:native": "oxlint --config config/oxlint-code-quality-native-plugins.json src config tests mobile --deny-warnings",
|
||||
"audit:code-quality:type-aware": "oxlint --type-aware --config config/oxlint-code-quality-type-aware.json src config tests --deny-warnings",
|
||||
@@ -39,6 +39,7 @@
|
||||
"check:ts-nocheck-ratchet": "node config/scripts/check-ts-nocheck-ratchet.mjs",
|
||||
"check:runtime-electron-ratchet": "node config/scripts/check-runtime-electron-ratchet.mjs",
|
||||
"check:readme-local-links": "node config/scripts/check-readme-local-links.mjs",
|
||||
"check:node-runtime-pin": "node config/scripts/check-node-runtime-pin.mjs",
|
||||
"build:orcad": "node config/scripts/build-orcad-bun.mjs",
|
||||
"build:orcad-template": "node config/scripts/build-orcad-template.mjs",
|
||||
"build:orcad-prebuilds": "node config/scripts/build-orcad-prebuilds.mjs",
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
orcadArtifactFilenames,
|
||||
orcadArtifactHashPrefix
|
||||
} from '../../shared/orcad-artifacts'
|
||||
import { ORCAD_BUN_TARGETS } from '../../shared/orcad-bun-runtime'
|
||||
import { SERVER_TARGETS } from '../../shared/node-runtime-pin'
|
||||
import { orcadAgentBrowserNativeName } from '../../shared/orcad-agent-browser-name'
|
||||
|
||||
/** Hash installed bytes in the build's order; a version marker is not proof of delivery. */
|
||||
export async function readOrcadArtifactIdentity(directory: string): Promise<string> {
|
||||
const target = z
|
||||
.enum(ORCAD_BUN_TARGETS)
|
||||
.enum(SERVER_TARGETS)
|
||||
.parse((await readFile(join(directory, ORCAD_BUILD_TARGET_FILENAME), 'utf8')).trim())
|
||||
const platform = target.startsWith('win32-')
|
||||
? 'win32'
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* The one Node runtime Orca runs outside Electron (docs/reference/node-runtime-design.html, D1).
|
||||
*
|
||||
* Keep this file erasable-only TypeScript — build scripts import it directly under Node's
|
||||
* type stripping, which rejects enums, namespaces and parameter properties.
|
||||
*/
|
||||
|
||||
export const SERVER_TARGETS = [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64-glibc',
|
||||
'linux-x64-glibc',
|
||||
'linux-arm64-musl',
|
||||
'linux-x64-musl',
|
||||
'win32-arm64',
|
||||
'win32-x64'
|
||||
] as const
|
||||
|
||||
export type ServerTarget = (typeof SERVER_TARGETS)[number]
|
||||
|
||||
// Managed SSH deployment supports POSIX hosts; Windows uses standalone builds.
|
||||
export const ORCAD_TEMPLATE_TARGETS = SERVER_TARGETS.filter(
|
||||
(target) => !target.startsWith('win32-')
|
||||
)
|
||||
|
||||
export type NodeRuntimePin = {
|
||||
version: string
|
||||
/** Electron whose embedded Node this pin tracks; the older/newer policy is open (design D1). */
|
||||
electron: string
|
||||
/** Highest N-API version the runtime supports (NODE_API_SUPPORTED_VERSION_MAX). */
|
||||
napi: number
|
||||
headers: { file: string; sha256: string }
|
||||
}
|
||||
|
||||
/** unofficial-builds.nodejs.org publishes no SHASUMS signature, so its hash is trusted at pin time. */
|
||||
export type NodeRuntimeAssetSource = 'official' | 'unofficial'
|
||||
|
||||
export type NodeRuntimeAsset = {
|
||||
source: NodeRuntimeAssetSource
|
||||
archive: string
|
||||
archiveSha256: string
|
||||
executableSha256: string
|
||||
executableSize: number
|
||||
}
|
||||
|
||||
// @generated-begin by config/scripts/update-node-runtime-pin.mjs
|
||||
export const NODE_RUNTIME_PIN: NodeRuntimePin = {
|
||||
version: '24.21.0',
|
||||
electron: '43.7.5',
|
||||
napi: 10,
|
||||
headers: {
|
||||
file: 'node-v24.21.0-headers.tar.gz',
|
||||
sha256: '57c6bee2e30bbbee5bd51d6cc343eb992e174b56a2a1d0eab7a7510771c20ea2'
|
||||
}
|
||||
}
|
||||
|
||||
export const NODE_RUNTIME_ASSETS: Record<ServerTarget, NodeRuntimeAsset> = {
|
||||
'darwin-arm64': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-darwin-arm64.tar.gz',
|
||||
archiveSha256: 'bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057',
|
||||
executableSha256: 'e4b5a3af0e05c75de2eae013904145f40fe7fc2a6e6f17510128bf45cca4e79b',
|
||||
executableSize: 122129232
|
||||
},
|
||||
'darwin-x64': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-darwin-x64.tar.gz',
|
||||
archiveSha256: '1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097',
|
||||
executableSha256: '7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49',
|
||||
executableSize: 125270960
|
||||
},
|
||||
'linux-arm64-glibc': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-linux-arm64.tar.gz',
|
||||
archiveSha256: '724282c3b43aec998aa9527380465b45d229e021b58035f5f4f63095eabfe5d5',
|
||||
executableSha256: '0f8949d1028f6d61506b2d5bc57e7e6fe893d7b1997509b7847294fc9c616584',
|
||||
executableSize: 122893672
|
||||
},
|
||||
'linux-x64-glibc': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-linux-x64.tar.gz',
|
||||
archiveSha256: '6e1db87ef58b8819e5d5402eff1536491b18edd8eb7bee5ef7897876e88dc5ff',
|
||||
executableSha256: '7fde7b8afa198da66257f42ee2001d874c7355631e6d1579a5fb5ef1f246df4c',
|
||||
executableSize: 126595440
|
||||
},
|
||||
'linux-arm64-musl': {
|
||||
source: 'unofficial',
|
||||
archive: 'node-v24.21.0-linux-arm64-musl.tar.gz',
|
||||
archiveSha256: '3048b0811e158ca0d8672b59c839861763e144492980d8d29b369dfee45747e4',
|
||||
executableSha256: 'fa2789559dbc3603794a229877c244d1c0d06625c124631611ca4e13eac765be',
|
||||
executableSize: 128653768
|
||||
},
|
||||
'linux-x64-musl': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-linux-x64-musl.tar.gz',
|
||||
archiveSha256: '3d63405fc65a0d2d2976c1f0bc2fd27bb0bd07212469e705aac3f03ae5ab4c9c',
|
||||
executableSha256: '2cd83acecc7693ce96bcb4e292ff4c80461b7490028a002abe5a28ac9892bc29',
|
||||
executableSize: 132204408
|
||||
},
|
||||
'win32-arm64': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-win-arm64.zip',
|
||||
archiveSha256: '8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921',
|
||||
executableSha256: 'dff59da18b6ffe1bf1ca99e1d2af4906080c481740619f5b5098c0fca28bd9b7',
|
||||
executableSize: 81881416
|
||||
},
|
||||
'win32-x64': {
|
||||
source: 'official',
|
||||
archive: 'node-v24.21.0-win-x64.zip',
|
||||
archiveSha256: '158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541',
|
||||
executableSha256: 'ba4e6d110e8c1592a1ecd390f6b05f3da124b13871a5be62b341a07a853c6c32',
|
||||
executableSize: 93580104
|
||||
}
|
||||
}
|
||||
// @generated-end
|
||||
|
||||
const NODE_RUNTIME_BASE_URLS: Record<NodeRuntimeAssetSource, string> = {
|
||||
official: 'https://nodejs.org/dist',
|
||||
unofficial: 'https://unofficial-builds.nodejs.org/download/release'
|
||||
}
|
||||
|
||||
export function nodeRuntimeReleaseUrl(
|
||||
source: NodeRuntimeAssetSource,
|
||||
file: string,
|
||||
version: string = NODE_RUNTIME_PIN.version
|
||||
): string {
|
||||
return `${NODE_RUNTIME_BASE_URLS[source]}/v${version}/${file}`
|
||||
}
|
||||
|
||||
/** Archive-relative path of the executable, e.g. node-v24.21.0-linux-x64/bin/node. */
|
||||
export function nodeRuntimeExecutablePath(target: ServerTarget, archive: string): string {
|
||||
const topLevel = archive.replace(/\.(?:tar\.gz|tar\.xz|zip)$/, '')
|
||||
return target.startsWith('win32-') ? `${topLevel}/node.exe` : `${topLevel}/bin/node`
|
||||
}
|
||||
@@ -1,22 +1,9 @@
|
||||
// Targets come from SERVER_TARGETS. Type-only: a value import needs a .ts suffix tsc rejects.
|
||||
import type { ServerTarget } from './node-runtime-pin.ts'
|
||||
|
||||
export const ORCAD_BUN_VERSION = '1.4.2'
|
||||
|
||||
export const ORCAD_BUN_TARGETS = [
|
||||
'darwin-arm64',
|
||||
'darwin-x64',
|
||||
'linux-arm64-glibc',
|
||||
'linux-x64-glibc',
|
||||
'linux-arm64-musl',
|
||||
'linux-x64-musl',
|
||||
'win32-arm64',
|
||||
'win32-x64'
|
||||
] as const
|
||||
|
||||
export type OrcadBunTarget = (typeof ORCAD_BUN_TARGETS)[number]
|
||||
|
||||
// Managed SSH deployment supports POSIX hosts; Windows uses standalone builds.
|
||||
export const ORCAD_TEMPLATE_TARGETS = ORCAD_BUN_TARGETS.filter(
|
||||
(target) => !target.startsWith('win32-')
|
||||
)
|
||||
export type OrcadBunTarget = ServerTarget
|
||||
|
||||
export type OrcadBunReleaseAsset = {
|
||||
filename: string
|
||||
|
||||
Reference in New Issue
Block a user