Files
pikasTech-HWLAB/scripts/src/artifact-catalog-authority.mjs
T

231 lines
9.8 KiB
JavaScript

import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";
export function artifactCatalogAuthorityPath(catalogPath) {
return `${catalogPath}.authority.json`;
}
export function artifactCatalogSha256(catalog) {
return createHash("sha256").update(JSON.stringify(catalog)).digest("hex");
}
export function artifactCatalogSourceCommitId(catalog) {
const direct = text(catalog?.publish?.sourceCommitId) || text(catalog?.sourceCommitId) || text(catalog?.commitId);
if (direct) return direct;
const serviceCommits = unique((catalog?.services ?? []).map((service) => text(service?.sourceCommitId)).filter(Boolean));
return serviceCommits.length === 1 ? serviceCommits[0] : null;
}
export function artifactCatalogServiceCoverage(catalog, selectedServices) {
if (!Array.isArray(catalog?.services)) throw new Error("artifact catalog services must be an array");
const selectedServiceIds = normalizedServiceIds(selectedServices, "selected services");
const catalogServiceIds = normalizedServiceIds(catalog.services.map((service) => service?.serviceId), "artifact catalog services");
const missingSelectedServices = selectedServiceIds.filter((serviceId) => !catalogServiceIds.includes(serviceId));
const extraCatalogServices = catalogServiceIds.filter((serviceId) => !selectedServiceIds.includes(serviceId));
return {
status: missingSelectedServices.length > 0
? (extraCatalogServices.length > 0 ? "divergent" : "partial")
: (extraCatalogServices.length > 0 ? "superset" : "exact"),
selectedServiceCount: selectedServiceIds.length,
selectedServicePresentCount: selectedServiceIds.length - missingSelectedServices.length,
catalogServiceCount: catalogServiceIds.length,
selectedServiceIds,
catalogServiceIds,
missingSelectedServices,
extraCatalogServices
};
}
export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId, selectedServices }) {
return {
schemaVersion: "v1",
status: "hydrated",
authority: "gitops-branch",
catalogPath: normalizeCatalogPath(catalogPath),
gitopsBranch: text(gitopsBranch),
gitopsCommitId: text(gitopsCommitId),
catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
catalogSha256: artifactCatalogSha256(catalog),
coverage: artifactCatalogServiceCoverage(catalog, selectedServices)
};
}
export function createBootstrapArtifactCatalogAuthority({ catalog, catalogPath, sourceCommitId, reason, selectedServices }) {
return {
schemaVersion: "v1",
status: "bootstrap",
authority: "source-bootstrap",
catalogPath: normalizeCatalogPath(catalogPath),
sourceCommitId: text(sourceCommitId),
bootstrapReason: text(reason),
catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
catalogSha256: artifactCatalogSha256(catalog),
coverage: artifactCatalogServiceCoverage(catalog, selectedServices)
};
}
export async function readArtifactCatalogSummary({ repoRoot, catalogPath, authorityPath = artifactCatalogAuthorityPath(catalogPath), catalog = undefined }) {
const loadedCatalog = catalog === undefined ? await readJsonIfPresent(repoRoot, catalogPath) : catalog;
const authority = await readJsonIfPresent(repoRoot, authorityPath);
const normalizedCatalogPath = normalizeCatalogPath(catalogPath);
if (!loadedCatalog) {
if (authority) throw new Error(`artifact catalog authority exists without catalog ${normalizedCatalogPath}`);
return {
status: "missing",
authority: "none",
catalogPath: normalizedCatalogPath,
authorityPath: null,
gitopsBranch: null,
gitopsCommitId: null,
catalogSourceCommitId: null,
serviceCount: 0,
catalogSha256: null,
coverage: null,
bootstrapReason: null
};
}
const serviceCount = Array.isArray(loadedCatalog.services) ? loadedCatalog.services.length : 0;
const catalogSha256 = artifactCatalogSha256(loadedCatalog);
const catalogSourceCommitId = artifactCatalogSourceCommitId(loadedCatalog);
if (!authority) {
return {
status: "loaded",
authority: "workspace-file",
catalogPath: normalizedCatalogPath,
authorityPath: null,
gitopsBranch: null,
gitopsCommitId: null,
catalogSourceCommitId,
serviceCount,
catalogSha256,
coverage: null,
bootstrapReason: null
};
}
const coverage = validateCatalogAuthority({ authority, catalog: loadedCatalog, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 });
if (authority.authority === "gitops-branch") {
return {
status: "hydrated",
authority: "gitops-branch",
catalogPath: normalizedCatalogPath,
authorityPath: normalizeCatalogPath(authorityPath),
gitopsBranch: text(authority.gitopsBranch),
gitopsCommitId: text(authority.gitopsCommitId),
catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
serviceCount,
catalogSha256,
coverage,
bootstrapReason: null
};
}
return {
status: "bootstrap",
authority: "source-bootstrap",
catalogPath: normalizedCatalogPath,
authorityPath: normalizeCatalogPath(authorityPath),
gitopsBranch: null,
gitopsCommitId: null,
sourceCommitId: text(authority.sourceCommitId),
catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
serviceCount,
catalogSha256,
coverage,
bootstrapReason: text(authority.bootstrapReason)
};
}
function validateCatalogAuthority({ authority, catalog, catalogPath, serviceCount, catalogSha256 }) {
const gitopsAuthority = authority.schemaVersion === "v1" && authority.status === "hydrated" && authority.authority === "gitops-branch";
const bootstrapAuthority = authority.schemaVersion === "v1" && authority.status === "bootstrap" && authority.authority === "source-bootstrap";
if (!gitopsAuthority && !bootstrapAuthority) {
throw new Error(`artifact catalog authority for ${catalogPath} is not a supported authority record`);
}
if (normalizeCatalogPath(authority.catalogPath) !== catalogPath) {
throw new Error(`artifact catalog authority path mismatch: expected ${catalogPath}, got ${authority.catalogPath ?? "missing"}`);
}
if (authority.serviceCount !== serviceCount) {
throw new Error(`artifact catalog authority service count mismatch for ${catalogPath}: expected ${serviceCount}, got ${authority.serviceCount ?? "missing"}`);
}
if (authority.catalogSha256 !== catalogSha256) {
throw new Error(`artifact catalog authority fingerprint mismatch for ${catalogPath}`);
}
const coverage = artifactCatalogServiceCoverage(catalog, authority.coverage?.selectedServiceIds);
validateCoverageRecord(authority.coverage, coverage, catalogPath);
if (gitopsAuthority && (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? ""))) {
throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`);
}
if (bootstrapAuthority) {
const sourceCommitId = text(authority.sourceCommitId);
if (!/^[a-f0-9]{40}$/u.test(sourceCommitId ?? "") || !text(authority.bootstrapReason)) {
throw new Error(`artifact catalog bootstrap authority is incomplete for ${catalogPath}`);
}
if (sourceCommitId !== artifactCatalogSourceCommitId(catalog)) {
throw new Error(`artifact catalog bootstrap source commit mismatch for ${catalogPath}`);
}
}
return coverage;
}
async function readJsonIfPresent(repoRoot, filePath) {
const resolved = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
try {
return JSON.parse(await readFile(resolved, "utf8"));
} catch (error) {
if (error?.code === "ENOENT") return null;
throw error;
}
}
function normalizeCatalogPath(value) {
return String(value ?? "").replaceAll("\\", "/").replace(/^\.\//u, "").trim();
}
function text(value) {
const result = typeof value === "string" ? value.trim() : "";
return result || null;
}
function unique(values) {
return [...new Set(values)];
}
function normalizedServiceIds(values, label) {
if (!Array.isArray(values) || values.length === 0) throw new Error(`${label} must be a non-empty array`);
const normalized = values.map(text);
if (normalized.some((value) => value === null)) throw new Error(`${label} contains an empty service id`);
const deduplicated = unique(normalized);
if (deduplicated.length !== normalized.length) throw new Error(`${label} contains duplicate service ids`);
return deduplicated.sort();
}
function validateCoverageRecord(actual, expected, catalogPath) {
const scalarFields = ["status", "selectedServiceCount", "selectedServicePresentCount", "catalogServiceCount"];
const arrayFields = ["selectedServiceIds", "catalogServiceIds", "missingSelectedServices", "extraCatalogServices"];
const expectedFields = [...scalarFields, ...arrayFields].sort();
const actualFields = actual && typeof actual === "object" && !Array.isArray(actual)
? Object.keys(actual).sort()
: [];
if (!sameArray(actualFields, expectedFields)) {
throw new Error(`artifact catalog authority coverage fields mismatch for ${catalogPath}`);
}
for (const field of scalarFields) {
if (actual[field] !== expected[field]) {
throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`);
}
}
for (const field of arrayFields) {
if (!sameArray(actual[field], expected[field])) {
throw new Error(`artifact catalog authority coverage ${field} mismatch for ${catalogPath}`);
}
}
}
function sameArray(left, right) {
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
}