Files
pikasTech-HWLAB/scripts/ci/restore-artifact-catalog.mjs
T

204 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
// Restore the node/lane artifact catalog from the GitOps branch before CI planning.
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import {
artifactCatalogAuthorityPath,
artifactCatalogServiceCoverage,
createBootstrapArtifactCatalogAuthority,
createHydratedArtifactCatalogAuthority
} from "../src/artifact-catalog-authority.mjs";
const startedAt = Date.now();
function env(name) {
const value = process.env[name];
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function csvEnv(name) {
return (env(name) ?? "").split(",").map((value) => value.trim()).filter(Boolean);
}
function emit(payload) {
process.stderr.write(JSON.stringify({
event: "artifact-catalog-restore",
source: "scripts/ci/restore-artifact-catalog.mjs",
at: new Date().toISOString(),
durationMs: Date.now() - startedAt,
catalogPath,
gitopsBranch,
...payload
}) + "\n");
}
function git(args, options = {}) {
return execFileSync("git", args, {
cwd: process.cwd(),
encoding: "utf8",
stdio: options.capture === false ? ["ignore", "ignore", "pipe"] : ["ignore", "pipe", "pipe"],
timeout: options.timeoutMs ?? 45_000
});
}
function gitProbe(args, options = {}) {
const result = spawnSync("git", args, {
cwd: process.cwd(),
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: options.timeoutMs ?? 45_000
});
if (result.error) throw result.error;
return {
status: result.status ?? 1,
stdout: result.stdout ?? "",
stderr: result.stderr ?? ""
};
}
const catalogPath = env("HWLAB_CATALOG_PATH");
const gitopsBranch = env("HWLAB_GITOPS_BRANCH");
const remote = env("HWLAB_GIT_READ_URL") ?? env("HWLAB_GIT_URL");
const selectedServices = csvEnv("HWLAB_SERVICES");
if (!catalogPath || !gitopsBranch || !remote || selectedServices.length === 0) {
emit({
status: "failed",
reason: "required-input-missing",
hasCatalogPath: Boolean(catalogPath),
hasGitopsBranch: Boolean(gitopsBranch),
hasRemote: Boolean(remote),
selectedServiceCount: selectedServices.length
});
process.exit(2);
}
try {
const selectedServiceCoverageInput = uniqueSorted(selectedServices);
if (selectedServiceCoverageInput.length !== selectedServices.length) {
throw new Error("selected services contain duplicate service ids");
}
const branchProbe = gitProbe(["ls-remote", "--exit-code", "--heads", remote, `refs/heads/${gitopsBranch}`], { timeoutMs: 45_000 });
if (branchProbe.status === 2) {
restoreBootstrapOrMissing({ reason: "gitops-branch-missing", selectedServices: selectedServiceCoverageInput });
process.exit(0);
}
if (branchProbe.status !== 0) {
throw new Error(`gitops branch probe failed: ${boundedError(branchProbe.stderr)}`);
}
git(["fetch", "--depth=1", remote, `refs/heads/${gitopsBranch}`], { capture: false, timeoutMs: 60_000 });
const gitopsCommitId = git(["rev-parse", "FETCH_HEAD"], { timeoutMs: 15_000 }).trim();
const catalogProbe = gitProbe(["ls-tree", "-z", "--full-tree", "--name-only", "FETCH_HEAD", "--", catalogPath], { timeoutMs: 15_000 });
if (catalogProbe.status !== 0) {
throw new Error(`gitops catalog tree probe failed: ${boundedError(catalogProbe.stderr)}`);
}
if (catalogProbe.stdout.length === 0) {
restoreBootstrapOrMissing({ reason: "gitops-catalog-missing", selectedServices: selectedServiceCoverageInput });
process.exit(0);
}
const content = git(["show", `FETCH_HEAD:${catalogPath}`], { timeoutMs: 30_000 });
const parsed = JSON.parse(content);
if (!Array.isArray(parsed?.services) || parsed.services.length === 0) {
throw new Error("gitops catalog has no services");
}
const coverage = artifactCatalogServiceCoverage(parsed, selectedServiceCoverageInput);
const authorityPath = artifactCatalogAuthorityPath(catalogPath);
const authority = createHydratedArtifactCatalogAuthority({
catalog: parsed,
catalogPath,
gitopsBranch,
gitopsCommitId,
selectedServices: selectedServiceCoverageInput
});
mkdirSync(dirname(catalogPath), { recursive: true });
writeFileSync(catalogPath, JSON.stringify(parsed, null, 2) + "\n");
writeFileSync(authorityPath, JSON.stringify(authority, null, 2) + "\n");
emit({
status: "hydrated",
reason: "gitops-catalog",
authority: authority.authority,
authorityPath,
gitopsCommitId,
catalogSourceCommitId: authority.catalogSourceCommitId,
catalogSha256: authority.catalogSha256,
serviceCount: parsed.services.length,
selectedServiceCount: selectedServices.length,
coverage,
bytes: Buffer.byteLength(content)
});
} catch (error) {
emit({
status: "failed",
reason: "gitops-catalog-unavailable",
error: error instanceof Error ? error.message.slice(0, 500) : String(error).slice(0, 500)
});
process.exit(1);
}
function restoreBootstrapOrMissing({ reason, selectedServices }) {
const authorityPath = artifactCatalogAuthorityPath(catalogPath);
if (!existsSync(catalogPath)) {
if (existsSync(authorityPath)) throw new Error(`artifact catalog authority exists without catalog ${catalogPath}`);
emit({
status: "missing",
reason,
authority: "none",
selectedServiceCount: selectedServices.length
});
return;
}
const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
const sourceCommitId = git(["rev-parse", "HEAD"], { timeoutMs: 15_000 }).trim();
validateBootstrapCatalog({ catalog, sourceCommitId, selectedServices });
const authority = createBootstrapArtifactCatalogAuthority({
catalog,
catalogPath,
sourceCommitId,
reason,
selectedServices
});
writeFileSync(authorityPath, JSON.stringify(authority, null, 2) + "\n");
emit({
status: "bootstrap",
reason,
authority: authority.authority,
authorityPath,
sourceCommitId,
serviceCount: authority.serviceCount,
catalogSha256: authority.catalogSha256,
coverage: authority.coverage
});
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
function validateBootstrapCatalog({ catalog, sourceCommitId, selectedServices }) {
if (catalog?.catalogVersion !== "v1" || catalog?.kind !== "hwlab-artifact-catalog" || catalog?.artifactState !== "contract-skeleton" || catalog?.environment !== "v02" || catalog?.profile !== "v02") {
throw new Error(`existing source catalog is not an allowed v02 contract skeleton for ${catalogPath}`);
}
if (catalog?.publish?.sourceCommitId !== sourceCommitId) {
throw new Error(`v02 contract skeleton source commit does not match HEAD for ${catalogPath}`);
}
const coverage = artifactCatalogServiceCoverage(catalog, selectedServices);
if (coverage.status !== "exact") {
throw new Error(`v02 contract skeleton service coverage is ${coverage.status} for ${catalogPath}`);
}
const unsafe = catalog.services.filter((service) => (
service?.sourceCommitId !== sourceCommitId
|| service?.buildBackend !== "contract-skeleton"
|| service?.digest !== null
|| (service?.environmentDigest !== undefined && service?.environmentDigest !== null)
));
if (unsafe.length > 0) throw new Error(`v02 contract skeleton contains reusable artifacts for ${catalogPath}`);
}
function boundedError(value) {
const result = String(value ?? "").replace(/\s+/gu, " ").trim();
return result ? result.slice(0, 300) : "exit-without-stderr";
}