📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
#!/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("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user