Files
playbook/antigravity-awesome-skills/tools/scripts/ci_artifact_preview.cjs
T
2026-07-29 07:59:17 +00:00

259 lines
9.8 KiB
JavaScript

#!/usr/bin/env node
const crypto = require("crypto");
const fs = require("fs");
const path = require("path");
const MODES = new Set(["source-preview", "canonical-exact-tree"]);
const SHA_RE = /^[0-9a-f]{40}$/;
const DIGEST_RE = /^[0-9a-f]{64}$/;
const REPOSITORY_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
function canonicalize(value) {
if (Array.isArray(value)) return value.map(canonicalize);
if (value && typeof value === "object") {
return Object.fromEntries(
Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]),
);
}
return value;
}
function canonicalJson(value) {
return JSON.stringify(canonicalize(value));
}
function sha256(value) {
return crypto.createHash("sha256").update(value).digest("hex");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function parsePositiveInteger(value, label) {
if (!/^[1-9]\d*$/.test(String(value))) throw new Error(`${label} must be a positive integer`);
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) throw new Error(`${label} is outside the safe integer range`);
return parsed;
}
function validateString(value, label) {
if (typeof value !== "string" || value.length === 0 || /[\0-\x1f\x7f]/.test(value)) {
throw new Error(`${label} must be a non-empty string without control characters`);
}
return value;
}
function validateRelativePath(value) {
validateString(value, "drift file");
if (value.includes("\\") || path.posix.isAbsolute(value) || path.posix.normalize(value) !== value) {
throw new Error(`Drift file must be a normalized repository-relative POSIX path: ${value}`);
}
if (value.split("/").some((part) => part === "" || part === "." || part === "..")) {
throw new Error(`Drift file contains an unsafe path segment: ${value}`);
}
return value;
}
function validateOrderedUniqueStrings(values, label, itemValidator = validateString) {
if (!Array.isArray(values)) throw new Error(`${label} must be an array`);
let previous = null;
return values.map((value, index) => {
const validated = itemValidator(value, `${label}[${index}]`);
if (previous !== null && previous >= validated) {
throw new Error(`${label} must be strictly sorted and contain no duplicates`);
}
previous = validated;
return validated;
});
}
function validateManifest(manifest) {
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
throw new Error("Manifest must be a JSON object");
}
const expectedKeys = [
"categories", "driftFiles", "headSha", "mode", "primaryCategory", "repository",
"runAttempt", "runId", "schemaVersion", "workflowSha",
].sort();
const actualKeys = Object.keys(manifest).sort();
if (JSON.stringify(actualKeys) !== JSON.stringify(expectedKeys)) {
throw new Error("Manifest contains missing or unsupported fields");
}
if (manifest.schemaVersion !== 1) throw new Error("Unsupported manifest schemaVersion");
if (!MODES.has(manifest.mode)) throw new Error(`Unsupported preview mode: ${manifest.mode}`);
if (!REPOSITORY_RE.test(validateString(manifest.repository, "repository"))) {
throw new Error("repository must use the owner/name form");
}
if (!/^\d+$/.test(String(manifest.runId))) throw new Error("runId must contain decimal digits");
parsePositiveInteger(manifest.runAttempt, "runAttempt");
for (const [label, value] of [["workflowSha", manifest.workflowSha], ["headSha", manifest.headSha]]) {
if (!SHA_RE.test(value)) throw new Error(`${label} must be a full lowercase SHA-1`);
}
validateString(manifest.primaryCategory, "primaryCategory");
validateOrderedUniqueStrings(manifest.categories, "categories");
validateOrderedUniqueStrings(manifest.driftFiles, "driftFiles", validateRelativePath);
if (manifest.mode === "canonical-exact-tree" && manifest.driftFiles.length !== 0) {
throw new Error("canonical-exact-tree manifests must not contain generated drift");
}
return manifest;
}
function parseOptions(argv) {
if (argv.length === 0) throw new Error("Expected create or verify-summary command");
const command = argv[0];
if (!new Set(["create", "verify-summary"]).has(command)) throw new Error(`Unknown command: ${command}`);
const repeatable = new Set(["drift-file"]);
const flags = new Set(["write-github-output", "write-step-summary"]);
const options = { command, driftFile: [] };
for (let index = 1; index < argv.length; index += 1) {
const token = argv[index];
if (!token.startsWith("--")) throw new Error(`Unexpected argument: ${token}`);
const name = token.slice(2);
if (flags.has(name)) {
const key = name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
if (options[key]) throw new Error(`Duplicate option: ${token}`);
options[key] = true;
continue;
}
const value = argv[index + 1];
if (!value || value.startsWith("--")) throw new Error(`${token} requires a value`);
const key = name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
if (!repeatable.has(name) && options[key] !== undefined) throw new Error(`Duplicate option: ${token}`);
if (repeatable.has(name)) options[key].push(value);
else options[key] = value;
index += 1;
}
return options;
}
function requireOptions(options, names) {
for (const name of names) {
if (options[name] === undefined) throw new Error(`--${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)} is required`);
}
}
function appendGithubOutput(digest) {
if (!process.env.GITHUB_OUTPUT) throw new Error("GITHUB_OUTPUT is required with --write-github-output");
fs.appendFileSync(process.env.GITHUB_OUTPUT, `manifest_digest=${digest}\n`, "utf8");
}
function createManifest(options) {
requireOptions(options, [
"output", "mode", "repository", "runId", "runAttempt", "workflowSha", "headSha",
"primaryCategory", "categoriesJson",
]);
let categories;
try {
categories = JSON.parse(options.categoriesJson);
} catch (error) {
throw new Error(`--categories-json must be valid JSON: ${error.message}`);
}
const manifest = validateManifest({
schemaVersion: 1,
mode: options.mode,
repository: options.repository,
runId: String(options.runId),
runAttempt: parsePositiveInteger(options.runAttempt, "runAttempt"),
workflowSha: options.workflowSha,
headSha: options.headSha,
primaryCategory: options.primaryCategory,
categories,
driftFiles: options.driftFile,
});
const serialized = canonicalJson(manifest);
const digest = sha256(serialized);
fs.mkdirSync(path.dirname(path.resolve(options.output)), { recursive: true });
fs.writeFileSync(options.output, `${serialized}\n`, { encoding: "utf8", mode: 0o600 });
if (options.writeGithubOutput) appendGithubOutput(digest);
process.stdout.write(`${digest}\n`);
return { digest, manifest };
}
function readCanonicalManifest(filePath) {
const raw = fs.readFileSync(filePath, "utf8");
let manifest;
try {
manifest = JSON.parse(raw);
} catch (error) {
throw new Error(`Manifest is not valid JSON: ${error.message}`);
}
validateManifest(manifest);
const serialized = canonicalJson(manifest);
if (raw !== `${serialized}\n`) throw new Error("Manifest is not encoded as canonical JSON with one trailing newline");
return { manifest, serialized };
}
function appendSummary(manifest, digest) {
if (!process.env.GITHUB_STEP_SUMMARY) throw new Error("GITHUB_STEP_SUMMARY is required with --write-step-summary");
const drift = manifest.driftFiles.length
? manifest.driftFiles.map((file) => `- <code>${escapeHtml(file)}</code>`).join("\n")
: "- none";
const lines = [
"## Artifact Preview", "",
`- Mode: <code>${escapeHtml(manifest.mode)}</code>`,
`- Primary change: <code>${escapeHtml(manifest.primaryCategory)}</code>`,
`- Categories: ${manifest.categories.length ? manifest.categories.map((item) => `<code>${escapeHtml(item)}</code>`).join(", ") : "none"}`,
`- Workflow SHA: <code>${escapeHtml(manifest.workflowSha)}</code>`,
`- Manifest SHA-256: <code>${escapeHtml(digest)}</code>`, "", "Generated drift:", drift, "",
];
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join("\n"), "utf8");
}
function verifySummary(options) {
requireOptions(options, [
"manifest", "expectedRepository", "expectedRunId", "expectedRunAttempt",
"expectedWorkflowSha", "expectedHeadSha", "expectedDigest",
]);
if (!DIGEST_RE.test(options.expectedDigest)) throw new Error("--expected-digest must be a lowercase SHA-256");
const { manifest, serialized } = readCanonicalManifest(options.manifest);
const bindings = [
["repository", options.expectedRepository],
["runId", String(options.expectedRunId)],
["runAttempt", parsePositiveInteger(options.expectedRunAttempt, "expectedRunAttempt")],
["workflowSha", options.expectedWorkflowSha],
["headSha", options.expectedHeadSha],
];
for (const [key, expected] of bindings) {
if (manifest[key] !== expected) throw new Error(`Manifest ${key} does not match the expected workflow identity`);
}
const digest = sha256(serialized);
if (digest !== options.expectedDigest) throw new Error("Manifest SHA-256 does not match --expected-digest");
if (options.writeStepSummary) appendSummary(manifest, digest);
process.stdout.write(`${digest}\n`);
return manifest;
}
function main(argv = process.argv.slice(2)) {
const options = parseOptions(argv);
return options.command === "create" ? createManifest(options) : verifySummary(options);
}
if (require.main === module) {
try {
main();
} catch (error) {
console.error(`[ci-artifact-preview] ${error.message}`);
process.exit(2);
}
}
module.exports = {
canonicalJson,
createManifest,
escapeHtml,
main,
parseOptions,
readCanonicalManifest,
sha256,
validateManifest,
validateRelativePath,
verifySummary,
};