Files
orca/config/scripts/release/agent-state-rules/agent-state-rules-bundle.mjs
T
Neil 6bb2e13c35 refactor(tooling): organize remaining scripts by domain owners (#38407)
* refactor(tooling): group remaining scripts by concrete domain owners

* fix(tooling): preserve relocated launcher resource roots
2026-11-05 00:29:16 -08:00

163 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.
// Assemble rules accepted by the app loader and enforce channel publishing constraints.
// Usage: build <out> [--bundled-only] | publish-next [--bundled-only] | promote-stable.
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { agentStateRulesTag } from '../versioning/release-tag-patterns.mjs'
const RULES_DIR = join(import.meta.dirname, '../../..', '..', 'src/main/runtime/agent-state-rules')
export const AGENT_STATE_RULES_ASSET = 'agent-state-rules.json'
function readJson(path) {
return JSON.parse(readFileSync(path, 'utf8'))
}
/** The live-updatable agents' files, in release order, under the release's version. */
export function buildAgentStateRulesBundle({ bundledOnly = false } = {}) {
const release = readJson(join(RULES_DIR, 'agent-state-rules-release.json'))
const files = release.liveUpdatable.map((id) => readJson(join(RULES_DIR, `${id}.json`)))
const bundle = {
version: release.version,
engineVersion: files[0].engineVersion,
...(bundledOnly ? { bundledOnly: true } : {}),
files
}
return `${JSON.stringify(bundle, null, 2)}\n`
}
function runGh(args) {
const result = spawnSync('gh', args, { encoding: 'utf8' })
return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
}
function ghOrThrow(gh, args) {
const result = gh(args)
if (result.status !== 0) {
throw new Error(`gh ${args.join(' ')} failed: ${result.stderr.trim()}`)
}
return result.stdout
}
/** The file on `tag`'s release, or null when there is no release yet. */
function downloadPublished(gh, repo, tag) {
const args = [
'release',
'download',
tag,
'--repo',
repo,
'-p',
AGENT_STATE_RULES_ASSET,
'-O',
'-'
]
const result = gh(args)
if (result.status === 0) {
return result.stdout
}
// Why throw on anything else, a missing asset included: that means an earlier upload broke.
if (/release not found/i.test(result.stderr)) {
return null
}
throw new Error(`gh ${args.join(' ')} failed: ${result.stderr.trim()}`)
}
/**
* Puts `text` on the channel's release for its engine, as a prerelease that never becomes Latest
* (the app updater follows Latest). `gh` is injectable so the sequence is testable.
*/
export function publishAgentStateRules({ repo, channel, text, target, gh = runGh }) {
const candidate = JSON.parse(text)
const tag = agentStateRulesTag(candidate.engineVersion, channel)
const publishedText = downloadPublished(gh, repo, tag)
// Apps refuse existing/lower versions, so only strictly increasing publication reaches upgraded clients.
const published = publishedText === null ? null : JSON.parse(publishedText).version
if (published !== null && candidate.version <= published) {
throw new Error(
`version ${candidate.version} is not higher than the published ${published}; bump agent-state-rules-release.json`
)
}
const file = join(mkdtempSync(join(tmpdir(), 'agent-state-rules-')), AGENT_STATE_RULES_ASSET)
writeFileSync(file, text)
if (published === null) {
ghOrThrow(gh, [
'release',
'create',
tag,
file,
'--repo',
repo,
'--target',
target,
'--prerelease',
'--latest=false',
'--title',
`Agent state rules (${channel})`,
'--notes',
'Agent state rules for Orca. Running apps download this file; it is not an app release.'
])
} else {
// Keep fixed channel URL with --clobber; mid-upload 404 retains the app’s last good copy.
ghOrThrow(gh, ['release', 'upload', tag, file, '--repo', repo, '--clobber'])
}
return { tag, version: candidate.version }
}
// Promote exact gated next bytes without rebuilding; bundledOnly next also permits rollback to shipped rules.
export function promoteAgentStateRules({ repo, engineVersion, target, gh = runGh }) {
const nextTag = agentStateRulesTag(engineVersion, 'next')
const text = downloadPublished(gh, repo, nextTag)
if (text === null) {
throw new Error(`${nextTag} has not been published`)
}
return publishAgentStateRules({ repo, channel: 'stable', text, target, gh })
}
function requireEnv(name) {
const value = process.env[name]
if (!value) {
throw new Error(`${name} is not set`)
}
return value
}
function main([command, ...args]) {
const bundledOnly = args.includes('--bundled-only')
switch (command) {
case 'build': {
const out = args.find((arg) => !arg.startsWith('--'))
if (!out) {
throw new Error('usage: build <out> [--bundled-only]')
}
writeFileSync(out, buildAgentStateRulesBundle({ bundledOnly }))
return
}
case 'publish-next':
case 'promote-stable': {
const repo = requireEnv('GITHUB_REPOSITORY')
const target = requireEnv('GITHUB_SHA')
const text = buildAgentStateRulesBundle({ bundledOnly })
const { tag, version } =
command === 'publish-next'
? publishAgentStateRules({ repo, channel: 'next', text, target })
: promoteAgentStateRules({ repo, engineVersion: JSON.parse(text).engineVersion, target })
console.log(`Published version ${version} to ${tag}.`)
return
}
default:
throw new Error(`unknown command ${command ?? '(none)'}`)
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
try {
main(process.argv.slice(2))
} catch (error) {
console.error(`::error::${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
}