mirror of
https://github.com/stablyai/orca.git
synced 2026-10-04 08:02:09 +00:00
* ci(daemon): gate PRs on daemon protocol crossing from the newest release Lands daemon-protocol-facts.mjs from the Windows update diagnostic branch with a stricter parser, and adds check-daemon-protocol-crossing.mjs (rule R1): the working tree must attach the newest release tag's daemon. Rollback crossing is reported only. Runs in the cross-version-wire job, which already has full tags; tag selection moves to config/scripts/stable-release-tags.mjs so both use one rule. * feat(persistence): run profile backups in the worker whenever its entry is bundled * refactor(orcad): make profile and native preflight runtime-neutral The profile preflight parser now takes the expected runtime identity from the caller (shipped callers pass the pinned Bun identity), and the native preflight is renamed to orcad-runtime-native-preflight with neutral wording. * 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. * test(persistence): skip plain-Node backup selection tests in the Bun profile suite * fix(runtime): reject a pinned archive that belongs to another target * ci(daemon): fail PRs that swap a runtime launcher and bump the daemon protocol D7.1 R3: hosting orcad or the daemon on another runtime is not a protocol change, so one PR must not do both. The launcher file list lives in the check script; the allow-runtime-launcher-protocol-bump label overrides it. * feat(orcad): select pinned-Node slots by a .runtime-node marker D7.1 R5: a Node slot names its shared runtimes/node-<sha256>/node through .runtime-node instead of .build-target, so Bun-era clients read it as a legacy slot rather than exiting 78 on a missing bun-runtime. Nothing builds the marker yet. * fix(runtime): load the Node pin without the typeless-module warning check-node-runtime-pin.mjs now requires the pin and takes nodeDistArchiveName from its own module, so it no longer loads the update script's build graph. * fix(orcad): resolve Node slots to the design's runtimes/node-<sha>/bin/node layout --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
182 lines
5.9 KiB
JavaScript
182 lines
5.9 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
RUNTIME_LAUNCHER_PATHS,
|
|
RUNTIME_PROTOCOL_OVERRIDE_ENV,
|
|
assessRuntimeLauncherProtocolRatchet,
|
|
checkRuntimeLauncherProtocolRatchet,
|
|
isOverrideEnabled
|
|
} from './check-runtime-launcher-protocol-ratchet.mjs'
|
|
import { DAEMON_PROTOCOL_SOURCE_PATH } from './daemon-protocol-facts.mjs'
|
|
import { classifyPrJobs } from './pr-code-change-scope.mjs'
|
|
|
|
const repoRoot = resolve(import.meta.dirname, '..', '..')
|
|
const repos = []
|
|
const LAUNCHER = 'src/main/ssh/orcad-remote-runtime.ts'
|
|
|
|
function git(repo, args) {
|
|
return execFileSync('git', args, {
|
|
cwd: repo,
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
}
|
|
|
|
function write(repo, path, contents) {
|
|
const file = join(repo, path)
|
|
mkdirSync(dirname(file), { recursive: true })
|
|
writeFileSync(file, contents)
|
|
}
|
|
|
|
function writeProtocol(repo, current) {
|
|
const previous = Array.from({ length: current - 1 }, (_, index) => index + 1)
|
|
write(
|
|
repo,
|
|
DAEMON_PROTOCOL_SOURCE_PATH,
|
|
`export const PROTOCOL_VERSION = ${current}\nexport const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [${previous.join(', ')}] as const\n`
|
|
)
|
|
}
|
|
|
|
function commit(repo, message) {
|
|
git(repo, ['add', '.'])
|
|
git(repo, [
|
|
'-c',
|
|
'user.name=test',
|
|
'-c',
|
|
'user.email=test@example.com',
|
|
'commit',
|
|
'-q',
|
|
'--no-gpg-sign',
|
|
'-m',
|
|
message
|
|
])
|
|
return git(repo, ['rev-parse', 'HEAD']).trim()
|
|
}
|
|
|
|
/** A base commit at protocol 3, then a candidate commit shaped by `change`. */
|
|
function repoWithCandidate(change) {
|
|
const repo = mkdtempSync(join(tmpdir(), 'runtime-launcher-ratchet-'))
|
|
repos.push(repo)
|
|
git(repo, ['init', '-q'])
|
|
writeProtocol(repo, 3)
|
|
write(repo, LAUNCHER, 'export const launcher = 1\n')
|
|
write(repo, 'src/unrelated.ts', 'export const unrelated = 1\n')
|
|
const base = commit(repo, 'base')
|
|
change(repo)
|
|
commit(repo, 'candidate')
|
|
return { repo, base }
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const repo of repos.splice(0)) {
|
|
rmSync(repo, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
describe('RUNTIME_LAUNCHER_PATHS', () => {
|
|
it('names only files that exist, so a rename cannot silently drop one from the gate', () => {
|
|
expect(RUNTIME_LAUNCHER_PATHS.filter((path) => !existsSync(join(repoRoot, path)))).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('PR routing', () => {
|
|
it('runs the ratchet job when its checker or the protocol changes', () => {
|
|
for (const file of [
|
|
'config/scripts/check-runtime-launcher-protocol-ratchet.mjs',
|
|
DAEMON_PROTOCOL_SOURCE_PATH
|
|
]) {
|
|
expect(classifyPrJobs([file])['cross-version-wire']).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('assessRuntimeLauncherProtocolRatchet', () => {
|
|
const launcherAndBump = {
|
|
changedFiles: [LAUNCHER, DAEMON_PROTOCOL_SOURCE_PATH],
|
|
baseProtocolVersion: 3,
|
|
candidateProtocolVersion: 4
|
|
}
|
|
|
|
it('fails a launcher change that also bumps the protocol', () => {
|
|
expect(
|
|
assessRuntimeLauncherProtocolRatchet({ ...launcherAndBump, overridden: false })
|
|
).toMatchObject({
|
|
ok: false,
|
|
violated: true
|
|
})
|
|
})
|
|
|
|
it('lets an explicit override through while still reporting the violation', () => {
|
|
const result = assessRuntimeLauncherProtocolRatchet({ ...launcherAndBump, overridden: true })
|
|
expect(result).toMatchObject({ ok: true, violated: true })
|
|
expect(result.lines.at(-1)).toContain('OVERRIDDEN')
|
|
})
|
|
|
|
it('passes a protocol bump without a launcher change, and a launcher change without a bump', () => {
|
|
expect(
|
|
assessRuntimeLauncherProtocolRatchet({
|
|
...launcherAndBump,
|
|
changedFiles: [DAEMON_PROTOCOL_SOURCE_PATH],
|
|
overridden: false
|
|
}).ok
|
|
).toBe(true)
|
|
expect(
|
|
assessRuntimeLauncherProtocolRatchet({
|
|
...launcherAndBump,
|
|
candidateProtocolVersion: 3,
|
|
overridden: false
|
|
}).ok
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('isOverrideEnabled', () => {
|
|
it('accepts only an explicit true', () => {
|
|
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: 'true' })).toBe(true)
|
|
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: '1' })).toBe(true)
|
|
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: 'false' })).toBe(false)
|
|
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: '' })).toBe(false)
|
|
expect(isOverrideEnabled({})).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('checkRuntimeLauncherProtocolRatchet against git history', () => {
|
|
it('fails when the diff from the base both edits a launcher and bumps PROTOCOL_VERSION', () => {
|
|
const { repo, base } = repoWithCandidate((candidate) => {
|
|
writeProtocol(candidate, 4)
|
|
write(candidate, LAUNCHER, 'export const launcher = 2\n')
|
|
})
|
|
expect(checkRuntimeLauncherProtocolRatchet({ repoRoot: repo, base, env: {} }).ok).toBe(false)
|
|
expect(
|
|
checkRuntimeLauncherProtocolRatchet({
|
|
repoRoot: repo,
|
|
base,
|
|
env: { [RUNTIME_PROTOCOL_OVERRIDE_ENV]: 'true' }
|
|
}).ok
|
|
).toBe(true)
|
|
})
|
|
|
|
it('passes a bump next to unrelated edits', () => {
|
|
const { repo, base } = repoWithCandidate((candidate) => {
|
|
writeProtocol(candidate, 4)
|
|
write(candidate, 'src/unrelated.ts', 'export const unrelated = 2\n')
|
|
})
|
|
expect(checkRuntimeLauncherProtocolRatchet({ repoRoot: repo, base, env: {} }).ok).toBe(true)
|
|
})
|
|
|
|
it('ignores edits to the protocol file that keep PROTOCOL_VERSION', () => {
|
|
const { repo, base } = repoWithCandidate((candidate) => {
|
|
write(
|
|
candidate,
|
|
DAEMON_PROTOCOL_SOURCE_PATH,
|
|
'// comment\nexport const PROTOCOL_VERSION = 3\nexport const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [1, 2] as const\n'
|
|
)
|
|
write(candidate, LAUNCHER, 'export const launcher = 2\n')
|
|
})
|
|
expect(checkRuntimeLauncherProtocolRatchet({ repoRoot: repo, base, env: {} }).ok).toBe(true)
|
|
})
|
|
})
|