fix: 保留 catalog bootstrap 并同步 PaC pipeline

This commit is contained in:
root
2026-07-10 23:02:42 +02:00
parent beacbdd37f
commit 6f2a7c39bc
6 changed files with 491 additions and 46 deletions
+121 -14
View File
@@ -17,7 +17,27 @@ export function artifactCatalogSourceCommitId(catalog) {
return serviceCommits.length === 1 ? serviceCommits[0] : null;
}
export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId }) {
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",
@@ -27,7 +47,23 @@ export function createHydratedArtifactCatalogAuthority({ catalog, catalogPath, g
gitopsCommitId: text(gitopsCommitId),
catalogSourceCommitId: artifactCatalogSourceCommitId(catalog),
serviceCount: Array.isArray(catalog?.services) ? catalog.services.length : 0,
catalogSha256: artifactCatalogSha256(catalog)
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)
};
}
@@ -46,7 +82,9 @@ export async function readArtifactCatalogSummary({ repoRoot, catalogPath, author
gitopsCommitId: null,
catalogSourceCommitId: null,
serviceCount: 0,
catalogSha256: null
catalogSha256: null,
coverage: null,
bootstrapReason: null
};
}
@@ -63,27 +101,49 @@ export async function readArtifactCatalogSummary({ repoRoot, catalogPath, author
gitopsCommitId: null,
catalogSourceCommitId,
serviceCount,
catalogSha256
catalogSha256,
coverage: null,
bootstrapReason: null
};
}
validateHydratedAuthority({ authority, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 });
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: "hydrated",
authority: "gitops-branch",
status: "bootstrap",
authority: "source-bootstrap",
catalogPath: normalizedCatalogPath,
authorityPath: normalizeCatalogPath(authorityPath),
gitopsBranch: text(authority.gitopsBranch),
gitopsCommitId: text(authority.gitopsCommitId),
gitopsBranch: null,
gitopsCommitId: null,
sourceCommitId: text(authority.sourceCommitId),
catalogSourceCommitId: text(authority.catalogSourceCommitId) || catalogSourceCommitId,
serviceCount,
catalogSha256
catalogSha256,
coverage,
bootstrapReason: text(authority.bootstrapReason)
};
}
function validateHydratedAuthority({ authority, catalogPath, serviceCount, catalogSha256 }) {
if (authority.schemaVersion !== "v1" || authority.status !== "hydrated" || authority.authority !== "gitops-branch") {
throw new Error(`artifact catalog authority for ${catalogPath} is not a hydrated gitops-branch record`);
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"}`);
@@ -94,9 +154,21 @@ function validateHydratedAuthority({ authority, catalogPath, serviceCount, catal
if (authority.catalogSha256 !== catalogSha256) {
throw new Error(`artifact catalog authority fingerprint mismatch for ${catalogPath}`);
}
if (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? "")) {
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) {
@@ -121,3 +193,38 @@ function text(value) {
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]);
}