fix: 修复 PaC 环境复用计划语义
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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 createHydratedArtifactCatalogAuthority({ catalog, catalogPath, gitopsBranch, gitopsCommitId }) {
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
validateHydratedAuthority({ authority, catalogPath: normalizedCatalogPath, serviceCount, catalogSha256 });
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
if (!text(authority.gitopsBranch) || !/^[a-f0-9]{40}$/u.test(text(authority.gitopsCommitId) ?? "")) {
|
||||
throw new Error(`artifact catalog authority gitops provenance is incomplete for ${catalogPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
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)];
|
||||
}
|
||||
@@ -6,6 +6,10 @@ import * as https from "node:https";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
artifactCatalogAuthorityPath,
|
||||
readArtifactCatalogSummary
|
||||
} from "./artifact-catalog-authority.mjs";
|
||||
import { parseStructuredConfig, readStructuredFileIfPresent } from "./structured-config.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
@@ -86,6 +90,13 @@ export async function createCiPlan(options = {}) {
|
||||
assertLaneEnvReuseConfig(laneConfig, lane);
|
||||
const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane);
|
||||
const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null);
|
||||
const catalogAuthorityPath = options.artifactCatalogAuthorityPath ?? artifactCatalogAuthorityPath(artifactCatalogPath);
|
||||
const artifactCatalogSummary = await readArtifactCatalogSummary({
|
||||
repoRoot,
|
||||
catalogPath: artifactCatalogPath,
|
||||
authorityPath: catalogAuthorityPath,
|
||||
catalog: artifactCatalog
|
||||
});
|
||||
const runtimeReuseConfigPath = options.runtimeReuseConfigPath ?? "gitops/reuse.ymal";
|
||||
const runtimeReuseConfig = await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null);
|
||||
const runtimeReuseByService = runtimeReuseServices(runtimeReuseConfig);
|
||||
@@ -339,7 +350,9 @@ export async function createCiPlan(options = {}) {
|
||||
|
||||
const affectedServices = services.filter((service) => service.affected).map((service) => service.serviceId);
|
||||
const buildServices = services.filter((service) => service.buildRequired).map((service) => service.serviceId);
|
||||
const reusedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
|
||||
const rolloutWithoutImageBuildServices = services.filter((service) => service.affected && !service.buildRequired).map((service) => service.serviceId);
|
||||
const reusedServices = services.filter((service) => !service.affected && !service.buildRequired).map((service) => service.serviceId);
|
||||
const imageBuildSkippedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
|
||||
const gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths);
|
||||
return {
|
||||
planVersion: CI_PLAN_VERSION,
|
||||
@@ -349,7 +362,8 @@ export async function createCiPlan(options = {}) {
|
||||
targetRef,
|
||||
compatibility: {
|
||||
deployConfig: deployConfigPath,
|
||||
artifactCatalog: artifactCatalog ? artifactCatalogPath : null,
|
||||
artifactCatalog: artifactCatalogSummary.status === "missing" ? null : artifactCatalogPath,
|
||||
artifactCatalogAuthority: artifactCatalogSummary.authority,
|
||||
lane,
|
||||
ciContractSource: "scripts/gitops-render.mjs",
|
||||
mode: "advisory-read-only",
|
||||
@@ -373,17 +387,22 @@ export async function createCiPlan(options = {}) {
|
||||
},
|
||||
changedPaths: normalizedChangedPaths,
|
||||
changedPathSummary: globalChange,
|
||||
artifactCatalog: artifactCatalogSummary,
|
||||
imageBuildRequired: buildServices.length > 0,
|
||||
affectedServices,
|
||||
rolloutServices: affectedServices,
|
||||
buildServices,
|
||||
rolloutWithoutImageBuildServices,
|
||||
reusedServices,
|
||||
buildSkippedCount: services.length - buildServices.length,
|
||||
serviceReusedCount: reusedServices.length,
|
||||
imageBuildSkippedServices,
|
||||
buildSkippedCount: imageBuildSkippedServices.length,
|
||||
envArtifactGroups: envArtifactGroupPlans,
|
||||
services,
|
||||
ciCdPlan: {
|
||||
willBuild: buildServices,
|
||||
willRollout: affectedServices,
|
||||
willRolloutWithoutImageBuild: rolloutWithoutImageBuildServices,
|
||||
willReuse: reusedServices,
|
||||
envArtifactGroups: envArtifactGroupPlans,
|
||||
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged,
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ -d "$ci_node_deps/yaml" ] && [ ! -e node_modules/yaml ]; then
|
||||
ln -s "$ci_node_deps/yaml" node_modules/yaml
|
||||
fi
|
||||
test "$(git rev-parse HEAD)" = "$(params.revision)"
|
||||
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.git-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" node scripts/ci/restore-artifact-catalog.mjs
|
||||
HWLAB_CATALOG_PATH="$(params.catalog-path)" HWLAB_GIT_URL="$(params.git-url)" HWLAB_GIT_READ_URL="$(params.git-read-url)" HWLAB_GITOPS_BRANCH="$(params.gitops-branch)" HWLAB_SERVICES="$(params.services)" node scripts/ci/restore-artifact-catalog.mjs
|
||||
node scripts/ci-plan.mjs --lane "$(params.lane)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" --verify-reuse-registry > /workspace/source/ci-plan.json
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
@@ -46,13 +46,17 @@ fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
|
||||
affectedServices: plan.affectedServices || [],
|
||||
rolloutServices: plan.rolloutServices || plan.affectedServices || [],
|
||||
buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),
|
||||
rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [],
|
||||
reusedServices: plan.reusedServices || [],
|
||||
serviceReusedCount: plan.serviceReusedCount || 0,
|
||||
imageBuildSkippedServices: plan.imageBuildSkippedServices || [],
|
||||
buildSkippedCount: plan.buildSkippedCount || 0,
|
||||
artifactCatalog: plan.artifactCatalog || null,
|
||||
envArtifactGroups: plan.envArtifactGroups || [],
|
||||
changedPathSummary: plan.changedPathSummary || null,
|
||||
ciCdPlan: plan.ciCdPlan || null,
|
||||
services: plan.services || [],
|
||||
entries
|
||||
}, null, 2) + String.fromCharCode(10));
|
||||
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0, envArtifactGroups: plan.envArtifactGroups || [] }));
|
||||
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], rolloutWithoutImageBuildServices: plan.rolloutWithoutImageBuildServices || [], reusedServices: plan.reusedServices || [], serviceReusedCount: plan.serviceReusedCount || 0, imageBuildSkippedServices: plan.imageBuildSkippedServices || [], buildSkippedCount: plan.buildSkippedCount || 0, artifactCatalog: plan.artifactCatalog || null, noImageBuildReason: plan.ciCdPlan?.noImageBuildReason || null, envArtifactGroups: plan.envArtifactGroups || [] }));
|
||||
NODE
|
||||
|
||||
Reference in New Issue
Block a user