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() : ""; }