diff --git a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts index 4b45b815..ad69db86 100644 --- a/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts @@ -426,6 +426,9 @@ test("L2/L3 release exposes plan first and trigger only sends the PaC webhook", expect(planAction).toContain("imageBuildCount"); expect(planAction).toContain("envReuse"); expect(planAction).toContain("requestedScope"); + expect(planAction).toContain("domain?.artifactCatalog?.status === 'missing'"); + expect(planAction).toContain("mode: firstMaterialization ? 'first-materialization' : 'changed-paths'"); + expect(planAction).toContain("[...new Set([...buildServices, ...rolloutServices, ...affectedServices])].sort()"); expect(planAction).toContain("service.envChangedPaths"); expect(planAction).toContain("service.codeChangedPaths"); expect(planAction).toContain("(service.reason || []).includes('gitops-render-input-changed')"); @@ -628,6 +631,41 @@ test("PaC compact release plan keeps bounded registry probe results", () => { expect(JSON.stringify(compact)).not.toContain("sha256:aaaaaaaa"); }); +test("PaC compact release plan summarizes env reuse without duplicate service lists", () => { + const compact = compactManualReleasePlan({ + envReuse: { + status: "planned", + groups: [{ + id: "shared-runtime", + buildService: "api", + buildRequired: true, + services: ["api", "worker", "web"], + buildServices: ["api"], + consumerServices: ["worker", "web"], + imageRepository: "registry.invalid/private/shared-runtime", + }], + reusedServices: ["unchanged-service"], + reusedServiceCount: 1, + buildSkippedServices: ["worker", "web", "unchanged-service"], + }, + }); + expect(compact.envReuse).toEqual({ + status: "planned", + groups: [{ + id: "shared-runtime", + buildService: "api", + buildRequired: true, + serviceCount: 3, + buildServiceCount: 1, + consumerServiceCount: 2, + }], + reusedServiceCount: 1, + buildSkippedServiceCount: 3, + }); + expect(JSON.stringify(compact)).not.toContain("registry.invalid"); + expect(JSON.stringify(compact)).not.toContain("unchanged-service"); +}); + test("PaC release manifest renders YAML-owned watcher startup backlog probes", () => { const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true }); const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-leading-comment-only.yaml"), "utf8"); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 04baf613..5ea37d05 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -611,7 +611,7 @@ const configuredImages = JSON.parse(process.env.UNIDESK_PAC_MANUAL_IMAGE_REPOSIT const affectedServices = domain?.affectedServices || (effectiveChangedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []); const rolloutServices = domain?.rolloutServices || affectedServices; const buildServices = domain?.buildServices || (effectiveChangedPaths.length > 0 ? configuredImages : []); -const requestedServices = domain +const changedRequestedServices = domain ? (domain.services || []) .filter((service) => (service.changedPaths || []).length > 0 || (service.envChangedPaths || []).length > 0 @@ -621,6 +621,10 @@ const requestedServices = domain || (service.reason || []).includes('gitops-render-input-changed')) .map((service) => service.serviceId || service.id) : (effectiveChangedPaths.length > 0 ? [process.env.UNIDESK_PAC_MANUAL_RUNTIME_SERVICE].filter(Boolean) : []); +const firstMaterialization = domain?.artifactCatalog?.status === 'missing'; +const requestedServices = firstMaterialization + ? [...new Set([...buildServices, ...rolloutServices, ...affectedServices])].sort() + : changedRequestedServices; const expandedBuildServices = domain ? buildServices.filter((serviceId) => !requestedServices.includes(serviceId)) : []; @@ -685,6 +689,7 @@ const plan = { : null, }, requestedScope: { + mode: firstMaterialization ? 'first-materialization' : 'changed-paths', affectedServiceCount: requestedServices.length, affectedServices: requestedServices, }, diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index c27f7dc8..f8e7d591 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -728,7 +728,7 @@ export function compactManualReleasePlan(plan: Record): Record): Record | null { + if (value === null || typeof value !== "object") return null; + const envReuse = value as Record; + const groups = Array.isArray(envReuse.groups) + ? envReuse.groups.map((item) => { + const group = item !== null && typeof item === "object" ? item as Record : {}; + const services = Array.isArray(group.services) ? group.services : []; + const buildServices = Array.isArray(group.buildServices) ? group.buildServices : []; + const consumerServices = Array.isArray(group.consumerServices) ? group.consumerServices : []; + return { + id: typeof group.id === "string" ? group.id : null, + buildService: typeof group.buildService === "string" ? group.buildService : null, + buildRequired: group.buildRequired === true, + serviceCount: services.length, + buildServiceCount: buildServices.length, + consumerServiceCount: consumerServices.length, + }; + }).filter((item) => item.id !== null) + : []; + const reusedServices = Array.isArray(envReuse.reusedServices) ? envReuse.reusedServices : []; + const buildSkippedServices = Array.isArray(envReuse.buildSkippedServices) ? envReuse.buildSkippedServices : []; + return { + status: typeof envReuse.status === "string" ? envReuse.status : null, + groups, + reusedServiceCount: typeof envReuse.reusedServiceCount === "number" + ? envReuse.reusedServiceCount + : reusedServices.length, + buildSkippedServiceCount: buildSkippedServices.length, + }; +} + function compactRegistryProbe(value: unknown): Record | null { if (value === null || typeof value !== "object") return null; const probe = value as Record;