feat(agent): pin managed runtimes and add update awareness (#2906)

This commit is contained in:
Carlos Florêncio
2026-08-22 22:08:45 +00:00
committed by GitHub
parent dd1b3d3d3b
commit 358a41a8a7
92 changed files with 5334 additions and 584 deletions
+162
View File
@@ -0,0 +1,162 @@
import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
export const DEFAULT_CATALOGUE_PATH = path.resolve(
SCRIPT_DIR,
"../apps/backend/internal/agent/agents/managed_npm_runtime_versions.json",
);
const STABLE_SEMVER = /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
export function parseStableVersion(value) {
if (typeof value !== "string" || !STABLE_SEMVER.test(value)) {
throw new Error(`value is not a stable SemVer: ${String(value)}`);
}
return value;
}
function assertCatalogue(catalogue) {
if (!catalogue || typeof catalogue !== "object" || Array.isArray(catalogue)) {
throw new Error("runtime pin catalogue must be a JSON object");
}
const packages = Object.keys(catalogue);
if (packages.length === 0) {
throw new Error("runtime pin catalogue is empty");
}
for (const packageName of packages) {
if (!packageName.trim()) {
throw new Error("runtime pin catalogue contains an empty package name");
}
parseStableVersion(catalogue[packageName]);
}
}
export function updatePins(catalogue, latestByPackage) {
assertCatalogue(catalogue);
if (!latestByPackage || typeof latestByPackage !== "object") {
throw new Error("latest version lookup did not return an object");
}
const next = {};
let changed = false;
for (const packageName of Object.keys(catalogue)) {
const latest = latestByPackage[packageName];
if (latest === undefined || latest === null || latest === "") {
throw new Error(`latest version is missing for ${packageName}`);
}
parseStableVersion(latest);
next[packageName] = latest;
changed ||= latest !== catalogue[packageName];
}
return { catalogue: next, changed };
}
export function formatCatalogue(catalogue) {
assertCatalogue(catalogue);
return `${JSON.stringify(catalogue, Object.keys(catalogue).sort(), 2)}\n`;
}
async function fetchLatest(packageName, fetchImpl) {
const response = await fetchImpl(
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
{ headers: { accept: "application/json" } },
);
if (!response.ok) {
throw new Error(`npm lookup for ${packageName} returned HTTP ${response.status}`);
}
const metadata = await response.json();
const latest = metadata?.["dist-tags"]?.latest;
parseStableVersion(latest);
return latest;
}
export async function fetchLatestVersions(packages, fetchImpl = globalThis.fetch) {
if (typeof fetchImpl !== "function") {
throw new Error("fetch is unavailable");
}
const pairs = await Promise.all(
packages.map(async (packageName) => [packageName, await fetchLatest(packageName, fetchImpl)]),
);
return Object.fromEntries(pairs);
}
export async function readCatalogue(cataloguePath = DEFAULT_CATALOGUE_PATH) {
const raw = await fs.readFile(cataloguePath, "utf8");
const catalogue = JSON.parse(raw);
assertCatalogue(catalogue);
return catalogue;
}
export async function writeCatalogueAtomically(cataloguePath, catalogue) {
const directory = path.dirname(cataloguePath);
const temporaryPath = path.join(
directory,
`.${path.basename(cataloguePath)}.${process.pid}.tmp`,
);
await fs.writeFile(temporaryPath, formatCatalogue(catalogue), "utf8");
await fs.rename(temporaryPath, cataloguePath);
}
export async function updateCatalogue({
cataloguePath = DEFAULT_CATALOGUE_PATH,
fetchImpl = globalThis.fetch,
} = {}) {
const catalogue = await readCatalogue(cataloguePath);
const latestByPackage = await fetchLatestVersions(Object.keys(catalogue), fetchImpl);
const result = updatePins(catalogue, latestByPackage);
if (result.changed) {
await writeCatalogueAtomically(cataloguePath, result.catalogue);
}
return result;
}
function outputChanged(outputPath, changed) {
if (!outputPath) {
return;
}
return fs.appendFile(outputPath, `changed=${changed ? "true" : "false"}\n`);
}
export function parseArguments(argv) {
let cataloguePath = DEFAULT_CATALOGUE_PATH;
let outputPath = "";
let positionalPathSeen = false;
for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--github-output") {
outputPath = argv[index + 1] ?? "";
if (!outputPath || outputPath.startsWith("--")) {
throw new Error("--github-output requires a path");
}
index += 1;
continue;
}
if (argument.startsWith("--")) {
throw new Error(`unknown option: ${argument}`);
}
if (positionalPathSeen) {
throw new Error("only one catalogue path is supported");
}
cataloguePath = argument;
positionalPathSeen = true;
}
return { cataloguePath, outputPath };
}
async function main(argv = process.argv.slice(2)) {
const { cataloguePath, outputPath } = parseArguments(argv);
const result = await updateCatalogue({ cataloguePath });
await outputChanged(outputPath, result.changed);
process.stdout.write(result.changed ? "Managed runtime pins changed.\n" : "Managed runtime pins are current.\n");
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
main().catch((error) => {
console.error(`Managed runtime pin update failed: ${error.message}`);
process.exitCode = 1;
});
}
@@ -0,0 +1,74 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
DEFAULT_CATALOGUE_PATH,
parseStableVersion,
parseArguments,
updatePins,
formatCatalogue,
} from "./update-agent-runtime-pins.mjs";
const catalogue = {
"@example/claude": "1.0.0",
"opencode-ai": "2.0.0",
};
test("updates only existing trusted catalogue entries", () => {
const result = updatePins(catalogue, {
"@example/claude": "1.1.0",
"opencode-ai": "2.0.0",
"@example/untrusted": "9.9.9",
});
assert.equal(result.changed, true);
assert.deepEqual(result.catalogue, {
"@example/claude": "1.1.0",
"opencode-ai": "2.0.0",
});
});
test("does not report a change when every latest value matches", () => {
const result = updatePins(catalogue, {
"@example/claude": "1.0.0",
"opencode-ai": "2.0.0",
});
assert.equal(result.changed, false);
assert.deepEqual(result.catalogue, catalogue);
});
test("rejects prerelease, malformed, and missing latest values before rewriting", () => {
for (const latest of ["1.1.0-beta.1", "latest", "", undefined]) {
assert.throws(
() => updatePins(catalogue, { "@example/claude": latest, "opencode-ai": "2.0.0" }),
/stable SemVer|latest version/i,
);
}
});
test("rejects an invalid current catalogue pin", () => {
assert.throws(
() => updatePins({ "@example/claude": "1.0.0-beta.1" }, { "@example/claude": "1.1.0" }),
/stable SemVer/i,
);
});
test("accepts build metadata but not prerelease syntax", () => {
assert.equal(parseStableVersion("1.2.3+build.7"), "1.2.3+build.7");
assert.throws(() => parseStableVersion("1.2.3-rc.1"), /stable SemVer/i);
});
test("parses the workflow output flag before the default catalogue path", () => {
assert.deepEqual(parseArguments(["--github-output", "/tmp/github-output"]), {
cataloguePath: DEFAULT_CATALOGUE_PATH,
outputPath: "/tmp/github-output",
});
});
test("formats the catalogue as one complete atomic-write payload", () => {
assert.equal(
formatCatalogue({ "@example/claude": "1.1.0", "opencode-ai": "2.0.0" }),
'{\n "@example/claude": "1.1.0",\n "opencode-ai": "2.0.0"\n}\n',
);
});