Files
orca/config/scripts/daemon-protocol-facts.mjs
T
OrcaWinandm4air 2a83c9536f ci(daemon): gate PRs on daemon protocol crossing from the newest release (#24089)
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.

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
2026-09-30 23:23:28 -07:00

100 lines
4.0 KiB
JavaScript

// Reads a build's daemon protocol version and the older versions it can still attach to.
// Parsed from source text so a release tag and a candidate tree are read the same way.
// Usage: node daemon-protocol-facts.mjs <daemon-protocol-version.ts> [output.json]
import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
export const DAEMON_PROTOCOL_SOURCE_PATH = 'src/main/daemon/daemon-protocol-version.ts'
const CURRENT_DECLARATION = /^export const PROTOCOL_VERSION\b[^=\n]*=\s*([^\n;]*)/gmu
const PREVIOUS_DECLARATION =
/^export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS\b[^=\n]*=\s*\[([^\]]*)\]/gmu
function singleMatch(source, pattern, name, label) {
const matches = [...source.matchAll(pattern)]
if (matches.length !== 1) {
throw new Error(
`${label}: expected exactly one \`export const ${name}\` declaration, found ${matches.length}`
)
}
return matches[0][1]
}
function parseProtocolInteger(text, name, label) {
const trimmed = text.trim()
// Why: a reference like `= NEXT_VERSION` must fail, not coerce to NaN or 0.
if (!/^\d+$/u.test(trimmed)) {
throw new Error(`${label}: ${name} must be an integer literal, got \`${trimmed}\``)
}
const value = Number(trimmed)
if (!Number.isSafeInteger(value) || value < 1) {
throw new Error(`${label}: ${name} must be a positive integer, got \`${trimmed}\``)
}
return value
}
/**
* @param {string} source
* @param {string} [label] names the source in errors
* @returns {{ protocolVersion: number, previousProtocolVersions: number[] }}
*/
export function parseDaemonProtocolFacts(source, label = DAEMON_PROTOCOL_SOURCE_PATH) {
const protocolVersion = parseProtocolInteger(
singleMatch(source, CURRENT_DECLARATION, 'PROTOCOL_VERSION', label),
'PROTOCOL_VERSION',
label
)
const previousProtocolVersions = singleMatch(
source,
PREVIOUS_DECLARATION,
'PREVIOUS_DAEMON_PROTOCOL_VERSIONS',
label
)
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => parseProtocolInteger(entry, 'PREVIOUS_DAEMON_PROTOCOL_VERSIONS entry', label))
if (protocolVersion > 1 && previousProtocolVersions.length === 0) {
throw new Error(`${label}: PREVIOUS_DAEMON_PROTOCOL_VERSIONS parsed as empty`)
}
const outOfRange = previousProtocolVersions.filter((version) => version >= protocolVersion)
if (outOfRange.length > 0) {
throw new Error(
`${label}: PREVIOUS_DAEMON_PROTOCOL_VERSIONS lists ${outOfRange.join(', ')}, not below PROTOCOL_VERSION ${protocolVersion}`
)
}
return { protocolVersion, previousProtocolVersions }
}
/** Whether `reader` can route sessions owned by a daemon speaking `owner`'s protocol. */
export function canAttach(reader, owner) {
return (
reader.protocolVersion === owner.protocolVersion ||
reader.previousProtocolVersions.includes(owner.protocolVersion)
)
}
/** What any candidate must declare for sessions to cross in each direction with `release`. */
export function crossingRequirements(release) {
const accepted = [...release.previousProtocolVersions, release.protocolVersion]
return {
upgrade: `candidate speaks ${release.protocolVersion} or lists ${release.protocolVersion} as previous`,
rollback: `candidate speaks one of ${Math.min(...accepted)}..${Math.max(...accepted)} (the release's own or previous list)`,
// Only an owner the release already speaks can survive a rollback, so a newer-protocol candidate cannot.
bothDirections: `candidate speaks ${release.protocolVersion}, or speaks an older release-listed version and lists ${release.protocolVersion} as previous`
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const [input, output] = process.argv.slice(2)
if (!input) {
throw new Error('usage: daemon-protocol-facts.mjs <daemon-protocol-version.ts> [output.json]')
}
const facts = parseDaemonProtocolFacts(readFileSync(input, 'utf8'), input)
if (output) {
writeFileSync(output, `${JSON.stringify(facts)}\n`)
}
console.log(JSON.stringify(facts))
}