import { readFile } from "node:fs/promises"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; const shaPattern = /^[0-9a-f]{40}$/; const digestPattern = /^sha256:[0-9a-f]{64}$/; const requiredKeys = ["git_sha", "api_digest", "web_digest"]; const optionalKeys = ["controller_sha256"]; const defaultRegistry = "ghcr.io/jesse-ux"; const acrRepository = "crpi-d1feco6itet73spp.cn-hongkong.personal.cr.aliyuncs.com/copse/jyotisha"; const registryPattern = /^(?:[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)(?::[1-9][0-9]{0,4})?(?:\/[a-z0-9]+(?:[._-][a-z0-9]+)*)*$/; export function parseStagingImageManifest(text, expectedSha, registry = defaultRegistry) { if (!registryPattern.test(registry)) { throw new Error("invalid staging image registry"); } if (!shaPattern.test(expectedSha)) { throw new Error("invalid expected staging revision"); } const lines = text.endsWith("\n") ? text.slice(0, -1).split("\n") : text.split("\n"); if (lines.length !== requiredKeys.length && lines.length !== requiredKeys.length + 1) { throw new Error("invalid staging image manifest"); } const values = new Map(); for (const line of lines) { const separator = line.indexOf("="); if (separator <= 0) throw new Error("invalid staging image manifest"); const key = line.slice(0, separator); const value = line.slice(separator + 1); if (![...requiredKeys, ...optionalKeys].includes(key) || values.has(key)) { throw new Error("invalid staging image manifest"); } values.set(key, value); } if (!requiredKeys.every((key) => values.has(key))) { throw new Error("invalid staging image manifest"); } if (values.get("git_sha") !== expectedSha) { throw new Error("staging image manifest revision mismatch"); } for (const key of ["api_digest", "web_digest"]) { if (!digestPattern.test(values.get(key) ?? "")) { throw new Error("invalid staging image digest"); } } const controllerSha256 = values.get("controller_sha256"); if (controllerSha256 !== undefined && !/^[0-9a-f]{64}$/.test(controllerSha256)) { throw new Error("invalid staging controller digest"); } const sharedRepository = registry === acrRepository; return { gitSha: expectedSha, apiDigest: values.get("api_digest"), webDigest: values.get("web_digest"), ...(controllerSha256 === undefined ? {} : { controllerSha256 }), apiImage: `${sharedRepository ? registry : `${registry}/jyotisha-api`}@${values.get("api_digest")}`, webImage: `${sharedRepository ? registry : `${registry}/jyotisha-web`}@${values.get("web_digest")}`, }; } const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined; if (invokedPath === import.meta.url) { try { const [manifestPath, expectedSha, registry] = process.argv.slice(2); if (!manifestPath || !expectedSha) { throw new Error("manifest path and expected revision are required"); } const manifest = parseStagingImageManifest( await readFile(manifestPath, "utf8"), expectedSha, registry, ); process.stdout.write( [ `git_sha=${manifest.gitSha}`, `api_image=${manifest.apiImage}`, `web_image=${manifest.webImage}`, ...(manifest.controllerSha256 === undefined ? [] : [`controller_sha256=${manifest.controllerSha256}`]), ].join("\n") + "\n", ); } catch { console.error("invalid staging image manifest"); process.exitCode = 1; } }