Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
165 lines
4.7 KiB
JavaScript
165 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
import { readFile } from "node:fs/promises";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
export const defaultTuttiAppRuntimeCatalogURL =
|
|
"https://d1x7gb6wqsqmnm.cloudfront.net/tutti-app-runtimes/catalog.json";
|
|
|
|
export function validateTuttiAppRuntimeRelease({ catalog, lock }) {
|
|
const expectedVersion = requiredString(
|
|
lock?.runtimeVersion,
|
|
"runtimeVersion"
|
|
);
|
|
const expectedVersionParts = parseRuntimeVersion(
|
|
expectedVersion,
|
|
"runtime lock runtimeVersion"
|
|
);
|
|
const platforms = Object.keys(lock?.platforms ?? {});
|
|
if (platforms.length === 0) {
|
|
throw new Error("runtime lock must declare at least one platform");
|
|
}
|
|
if (catalog?.schemaVersion !== "tutti.app.runtimes.v2") {
|
|
throw new Error(
|
|
`runtime catalog schemaVersion must be tutti.app.runtimes.v2, got ${JSON.stringify(catalog?.schemaVersion)}`
|
|
);
|
|
}
|
|
|
|
const mismatches = [];
|
|
for (const platform of platforms) {
|
|
const publishedVersion = catalog.runtimes?.[platform]?.version;
|
|
if (typeof publishedVersion !== "string") {
|
|
mismatches.push(
|
|
`${platform}: expected at least ${expectedVersion}, got missing`
|
|
);
|
|
continue;
|
|
}
|
|
let publishedVersionParts;
|
|
try {
|
|
publishedVersionParts = parseRuntimeVersion(
|
|
publishedVersion,
|
|
`${platform} published runtime version`
|
|
);
|
|
} catch (error) {
|
|
mismatches.push(`${platform}: ${error.message}`);
|
|
continue;
|
|
}
|
|
if (
|
|
compareRuntimeVersions(publishedVersionParts, expectedVersionParts) < 0
|
|
) {
|
|
mismatches.push(
|
|
`${platform}: expected at least ${expectedVersion}, got ${publishedVersion}`
|
|
);
|
|
}
|
|
}
|
|
if (mismatches.length > 0) {
|
|
throw new Error(
|
|
[
|
|
`managed app runtime ${expectedVersion} is not published for every desktop platform:`,
|
|
...mismatches.map((mismatch) => `- ${mismatch}`),
|
|
"Run Publish Tutti App Runtime for this release target and verify the production catalog before promoting the desktop release."
|
|
].join("\n")
|
|
);
|
|
}
|
|
|
|
return { platforms, runtimeVersion: expectedVersion };
|
|
}
|
|
|
|
export async function verifyTuttiAppRuntimeRelease({
|
|
catalogURL = defaultTuttiAppRuntimeCatalogURL,
|
|
fetchImpl = globalThis.fetch,
|
|
lockFile = "config/tutti.app-runtime.lock.json"
|
|
} = {}) {
|
|
const lock = JSON.parse(await readFile(lockFile, "utf8"));
|
|
const response = await fetchImpl(catalogURL, {
|
|
headers: {
|
|
accept: "application/json",
|
|
"cache-control": "no-cache"
|
|
},
|
|
redirect: "follow",
|
|
signal: AbortSignal.timeout(15_000)
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`failed to fetch managed app runtime catalog ${catalogURL}: HTTP ${response.status}`
|
|
);
|
|
}
|
|
const result = validateTuttiAppRuntimeRelease({
|
|
catalog: await response.json(),
|
|
lock
|
|
});
|
|
return { ...result, catalogURL };
|
|
}
|
|
|
|
export async function main(argv = process.argv.slice(2)) {
|
|
const options = parseArgs(argv);
|
|
const result = await verifyTuttiAppRuntimeRelease(options);
|
|
console.log(
|
|
`Verified managed app runtime ${result.runtimeVersion} for ${result.platforms.join(", ")} at ${result.catalogURL}`
|
|
);
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const options = {};
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
switch (arg) {
|
|
case "--catalog-url":
|
|
options.catalogURL = requiredArgument(argv[++index], arg);
|
|
break;
|
|
case "--lock-file":
|
|
options.lockFile = requiredArgument(argv[++index], arg);
|
|
break;
|
|
default:
|
|
throw new Error(`unknown argument ${arg}`);
|
|
}
|
|
}
|
|
return options;
|
|
}
|
|
|
|
function requiredArgument(value, flag) {
|
|
if (!value || value.startsWith("--")) {
|
|
throw new Error(`${flag} requires a value`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requiredString(value, name) {
|
|
if (typeof value !== "string" || value.trim() === "") {
|
|
throw new Error(`runtime lock ${name} is required`);
|
|
}
|
|
return value.trim();
|
|
}
|
|
|
|
function parseRuntimeVersion(value, name) {
|
|
const match = /^(\d{4})\.(\d{2})\.(0|[1-9]\d*)$/u.exec(value);
|
|
if (!match) {
|
|
throw new Error(
|
|
`${name} must use YYYY.MM.PATCH, got ${JSON.stringify(value)}`
|
|
);
|
|
}
|
|
const parts = match.slice(1).map(Number);
|
|
if (parts[1] < 1 || parts[1] > 12) {
|
|
throw new Error(`${name} has an invalid month in ${JSON.stringify(value)}`);
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
function compareRuntimeVersions(left, right) {
|
|
for (let index = 0; index < left.length; index += 1) {
|
|
if (left[index] !== right[index]) {
|
|
return left[index] - right[index];
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (
|
|
process.argv[1] &&
|
|
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
) {
|
|
main().catch((error) => {
|
|
console.error(error.message);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|