fix: 阻止流水线扩大已审阅发布范围

This commit is contained in:
root
2026-07-21 15:50:28 +02:00
parent 54f9b0a65c
commit bcff84ff9f
10 changed files with 336 additions and 10 deletions
+71
View File
@@ -0,0 +1,71 @@
import { createHash } from "node:crypto";
export const CI_PLAN_IDENTITY_VERSION = "v1";
export function attachCiPlanIdentity(plan, context = {}) {
const canonical = canonicalCiPlanIdentity(plan, context);
const digest = createHash("sha256").update(stableStringify(canonical)).digest("hex");
return {
...plan,
planIdentity: {
version: CI_PLAN_IDENTITY_VERSION,
value: `sha256:${digest}`,
inputs: canonical.inputs,
scope: canonical.scope
}
};
}
export function canonicalCiPlanIdentity(plan, context = {}) {
const services = Array.isArray(plan?.services) ? plan.services : [];
return {
version: CI_PLAN_IDENTITY_VERSION,
inputs: {
target: text(context.releaseTarget),
consumer: text(context.releaseConsumer),
lane: text(plan?.compatibility?.lane),
sourceCommitId: text(plan?.sourceCommitId),
baseCommitId: text(plan?.baseCommitId),
catalog: {
authority: text(plan?.artifactCatalog?.authority),
sourceCommitId: text(plan?.artifactCatalog?.catalogSourceCommitId),
digest: text(plan?.artifactCatalog?.catalogSha256),
gitopsCommitId: text(plan?.artifactCatalog?.gitopsCommitId)
},
registryProbe: {
enabled: plan?.inputs?.verifyReuseRegistry === true,
services: services.map((service) => ({
serviceId: text(service?.serviceId),
status: text(service?.reuseRegistry?.status),
image: text(service?.reuseRegistry?.image),
digest: text(service?.reuseRegistry?.digest)
})).filter((service) => service.serviceId).sort(compareServiceId)
}
},
scope: {
buildServices: sortedStrings(plan?.buildServices),
rolloutServices: sortedStrings(plan?.rolloutServices),
affectedServices: sortedStrings(plan?.affectedServices)
}
};
}
export function stableStringify(value) {
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
if (value && typeof value === "object") {
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
}
return JSON.stringify(value);
}
function sortedStrings(value) {
return [...new Set(Array.isArray(value) ? value.map(text).filter(Boolean) : [])].sort();
}
function compareServiceId(left, right) {
return left.serviceId.localeCompare(right.serviceId);
}
function text(value) {
return typeof value === "string" ? value.trim() : "";
}
+2
View File
@@ -109,6 +109,7 @@ export async function createCiPlan(options = {}) {
const runtimeReuseConfig = await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null);
const runtimeReuseByService = runtimeReuseServices(runtimeReuseConfig);
const sourceCommitId = await gitValue(repoRoot, ["rev-parse", targetRef]);
const baseCommitId = await gitValue(repoRoot, ["rev-parse", baseRef]);
const shortCommitId = await gitValue(repoRoot, ["rev-parse", "--short=7", targetRef]);
const changedPaths = await changedPathsBetween(repoRoot, baseRef, targetRef);
const normalizedChangedPaths = changedPaths.map(normalizeRepoPath).filter(Boolean);
@@ -390,6 +391,7 @@ export async function createCiPlan(options = {}) {
return {
planVersion: CI_PLAN_VERSION,
sourceCommitId,
baseCommitId,
shortCommitId,
baseRef,
targetRef,
+15 -3
View File
@@ -313,7 +313,11 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
{ name: "revision" },
{ name: "catalog-path" },
{ name: "registry-prefix" },
{ name: "services" }
{ name: "services" },
{ name: "release-base-commit" },
{ name: "release-target" },
{ name: "release-consumer" },
{ name: "reviewed-plan" }
],
results: serviceIds.flatMap((serviceId) => [
{ name: affectedResultName(serviceId), description: `${serviceId} rollout affected according to ci-plan` },
@@ -340,7 +344,11 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) {
{ name: "revision", value: "$(params.revision)" },
{ name: "catalog-path", value: "$(params.catalog-path)" },
{ name: "registry-prefix", value: "$(params.registry-prefix)" },
{ name: "services", value: "$(params.services)" }
{ name: "services", value: "$(params.services)" },
{ name: "release-base-commit", value: "$(params.release-base-commit)" },
{ name: "release-target", value: "$(params.release-target)" },
{ name: "release-consumer", value: "$(params.release-consumer)" },
{ name: "reviewed-plan", value: "$(params.reviewed-plan)" }
]
};
}
@@ -580,7 +588,11 @@ function tektonPipeline(args = { lane: "node" }, deploy = null) {
{ name: "services", type: "string", default: servicesParamForLane(args.lane, deploy) },
{ name: "base-image", type: "string", default: defaultDevBaseImage },
{ name: "build-cache-mode", type: "string", default: "registry" },
{ name: "runtime-ready-timeout-ms", type: "string", default: "60000" }
{ name: "runtime-ready-timeout-ms", type: "string", default: "60000" },
{ name: "release-base-commit", type: "string", description: "Reviewed release plan base commit." },
{ name: "release-target", type: "string", description: "Reviewed release target." },
{ name: "release-consumer", type: "string", description: "Reviewed release consumer." },
{ name: "reviewed-plan", type: "string", description: "Reviewed release plan envelope supplied by the manual webhook." }
],
workspaces: [
{ name: "source" },
@@ -10,7 +10,8 @@ if [ -d "$ci_node_deps/yaml" ] && [ ! -e node_modules/yaml ]; then
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)" 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 scripts/ci-plan.mjs --lane "$(params.lane)" --base-ref "$(params.release-base-commit)" --target-ref HEAD --deploy-config deploy/deploy.yaml --artifact-catalog "$(params.catalog-path)" --registry-prefix "$(params.registry-prefix)" --services "$(params.services)" --release-target "$(params.release-target)" --release-consumer "$(params.release-consumer)" --verify-reuse-registry > /workspace/source/ci-plan.json
node scripts/ci/verify-reviewed-plan.mjs --actual-plan /workspace/source/ci-plan.json --reviewed-plan "$(params.reviewed-plan)"
node - <<'NODE'
const fs = require("node:fs");
const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8"));
@@ -43,6 +44,7 @@ for (const entry of entries) {
}
fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
sourceCommitId: plan.sourceCommitId,
planIdentity: plan.planIdentity || null,
affectedServices: plan.affectedServices || [],
rolloutServices: plan.rolloutServices || plan.affectedServices || [],
buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId),
@@ -58,5 +60,5 @@ fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({
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 || [], 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 || [] }));
console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, planIdentity: plan.planIdentity || null, 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