From b70c9a1a54ac977345b7d0dd2a60d16946dff777 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 30 May 2026 01:16:07 +0800 Subject: [PATCH] fix: enable v02 env reuse code-only rollout --- scripts/artifact-publish.mjs | 23 ++++++++++++++------- scripts/g14-ci-plan.test.mjs | 10 +++++++++- scripts/g14-gitops-render.mjs | 33 +++++++++++++++++-------------- scripts/g14-gitops-render.test.ts | 19 ++++++++++++------ scripts/src/g14-ci-plan-lib.mjs | 21 ++++++++++++++------ 5 files changed, 71 insertions(+), 35 deletions(-) diff --git a/scripts/artifact-publish.mjs b/scripts/artifact-publish.mjs index 65288bb1..e1bd34c2 100644 --- a/scripts/artifact-publish.mjs +++ b/scripts/artifact-publish.mjs @@ -1994,6 +1994,7 @@ function artifactRecordForCatalogReuse({ service, catalogService, commitId }) { imageTag, buildCreatedAt: catalogService?.buildCreatedAt ?? null, buildSource: catalogService?.buildSource ?? null, + buildBackend: "reused-catalog", digest, repositoryDigest: repositoryDigestFor(image, digest), reusedFrom: null, @@ -2040,6 +2041,7 @@ function artifactRecordForEnvReuseCatalogReuse({ service, catalogService, commit bootSh: service.bootSh ?? catalogService?.bootSh ?? `deploy/runtime/boot/${service.serviceId}.sh`, buildCreatedAt: catalogService?.buildCreatedAt ?? null, buildSource: catalogService?.buildSource ?? null, + buildBackend: "reused-env-catalog", repositoryDigest: catalogService?.repositoryDigest ?? repositoryDigestFor(environmentImage, environmentDigest), bootEnvEvidence: bootEnvEvidence({ service, commitId, environmentImage, environmentDigest }) }; @@ -2092,11 +2094,15 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) { const artifacts = new Map(); const blockers = []; for (const service of services) { + const buildRequired = service.buildRequired === true || service.ciBuildRequired === true; const reportPath = externalServiceReportPath(dir, service.serviceId); let report; try { report = JSON.parse(await readFile(reportPath, "utf8")); } catch (error) { + if (!buildRequired) { + continue; + } blockers.push(blocker({ type: "environment_blocker", scope: service.serviceId, @@ -2119,11 +2125,11 @@ async function readExternalServiceArtifacts({ dir, services, commitId }) { const artifact = artifactRecordFromExternalReport({ service, record, reportPath }); artifacts.set(service.serviceId, artifact); - if (artifact.status !== "published" || !shaDigestPattern.test(artifact.digest ?? "")) { + if (!publishReadyStatuses.has(artifact.status) || !shaDigestPattern.test(artifact.digest ?? "")) { blockers.push(blocker({ type: "environment_blocker", scope: service.serviceId, - summary: `per-service artifact report ${reportPath} is not a verified published image for ${service.serviceId}`, + summary: `per-service artifact report ${reportPath} is not a verified published/reused image for ${service.serviceId}`, next: "Inspect the corresponding service TaskRun logs and rerun the failed service build." })); } @@ -2280,6 +2286,7 @@ function plannerFieldsFor(servicePlan) { bootRepo: servicePlan.bootRepo ?? null, bootCommit: servicePlan.bootCommit ?? null, bootSh: servicePlan.bootSh ?? null, + buildRequired: servicePlan.buildRequired ?? servicePlan.affected ?? false, environmentImage: servicePlan.environmentImage ?? null, environmentDigest: servicePlan.environmentDigest ?? null, dockerfileHash: servicePlan.dockerfileHash ?? null, @@ -2312,6 +2319,8 @@ function ciPlanReportSummary(ciPlan) { changedPathSummary: ciPlan.changedPathSummary, imageBuildRequired: ciPlan.imageBuildRequired, affectedServices: ciPlan.affectedServices, + rolloutServices: ciPlan.rolloutServices ?? ciPlan.affectedServices, + buildServices: ciPlan.buildServices ?? ciPlan.affectedServices, reusedServices: ciPlan.reusedServices, buildSkippedCount: ciPlan.buildSkippedCount, ciCdPlan: ciPlan.ciCdPlan @@ -2569,6 +2578,7 @@ async function main() { const producer = g14ArtifactProducer(process.env); const ciPlan = await createG14CiPlan({ repoRoot, + lane: args.lane, targetRef: "HEAD", deployJsonPath: args.deployPath, artifactCatalogPath: effectiveCatalogPath, @@ -2579,11 +2589,10 @@ async function main() { const services = enrichServicesWithCiPlan(await resolveServices(args.services, catalog), ciPlan); const catalogByServiceId = new Map((catalog?.services ?? []).map((service) => [service.serviceId, service])); const affectedServiceIds = new Set(ciPlan?.affectedServices ?? []); - const buildServiceIds = new Set( - services - .filter((service) => service.artifactRequired && (service.envReuse ? service.envChanged === true : affectedServiceIds.has(service.serviceId))) - .map((service) => service.serviceId) - ); + const plannedBuildServices = Array.isArray(ciPlan?.buildServices) ? ciPlan.buildServices : null; + const buildServiceIds = new Set(plannedBuildServices ?? services + .filter((service) => service.artifactRequired && (service.envReuse ? service.envChanged === true : affectedServiceIds.has(service.serviceId))) + .map((service) => service.serviceId)); const preflightBuildServiceIds = args.externalServiceReportDir || args.externalServiceResultsEnv ? new Set() : buildServiceIds; let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities, buildServiceIds: preflightBuildServiceIds }); diff --git a/scripts/g14-ci-plan.test.mjs b/scripts/g14-ci-plan.test.mjs index 8020839b..d9b25bae 100644 --- a/scripts/g14-ci-plan.test.mjs +++ b/scripts/g14-ci-plan.test.mjs @@ -90,11 +90,12 @@ test("global change classifier distinguishes gitops-only and test-only", () => { test("v02 planner marks device-pod code-only change as env reuse rollout", async () => { const repo = await createFixtureRepo({ includeDevicePod: true }); + const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim(); const initialPlan = await createG14CiPlan({ repoRoot: repo, lane: "v02", baseRef: "HEAD", - targetRef: "HEAD", + targetRef: initialSha, artifactCatalogPath: "deploy/artifact-catalog.v02.json", services: ["hwlab-device-pod"] }); @@ -123,8 +124,15 @@ test("v02 planner marks device-pod code-only change as env reuse rollout", async assert.equal(devicePod.runtimeMode, "env-reuse-git-mirror-checkout"); assert.equal(devicePod.envChanged, false); assert.equal(devicePod.codeChanged, true); + assert.equal(devicePod.buildRequired, false); assert.equal(devicePod.bootSh, "deploy/runtime/boot/hwlab-device-pod.sh"); assert.deepEqual(plan.affectedServices, ["hwlab-device-pod"]); + assert.deepEqual(plan.rolloutServices, ["hwlab-device-pod"]); + assert.deepEqual(plan.buildServices, []); + assert.equal(plan.imageBuildRequired, false); + assert.deepEqual(plan.ciCdPlan.willBuild, []); + assert.deepEqual(plan.ciCdPlan.willRollout, ["hwlab-device-pod"]); + assert.equal(plan.ciCdPlan.noImageBuildReason, "env-reuse-code-only-rollout"); }); async function createFixtureRepo(options = {}) { diff --git a/scripts/g14-gitops-render.mjs b/scripts/g14-gitops-render.mjs index 6dabb5d7..93619be6 100644 --- a/scripts/g14-gitops-render.mjs +++ b/scripts/g14-gitops-render.mjs @@ -235,7 +235,6 @@ function parseArgs(argv) { if (args.lane === "v02") { assert.equal(args.sourceBranch, "v0.2", "v02 source branch must be v0.2"); assert.equal(args.gitopsBranch, "v0.2-gitops", "v02 GitOps branch must be v0.2-gitops"); - assert.equal(args.catalogPath, "deploy/artifact-catalog.v02.json", "v02 catalog path must be deploy/artifact-catalog.v02.json"); } return args; } @@ -761,7 +760,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix if (!podTemplate?.spec?.containers) continue; const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]); const templateArtifact = runtimeArtifactForService({ catalog, serviceId: templateServiceId, source, registryPrefix, useDeployImages, digestPin }); - const templateSourceCommit = source.full; + const templateSourceCommit = profile === "v02" ? (templateArtifact.commit ?? source.full) : source.full; const templateArtifactCommit = templateArtifact.commit; label(podTemplate.metadata ??= {}, { ...stableRuntimeLabels, @@ -807,7 +806,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix } upsertEnv(container.env, "HWLAB_GITOPS_TARGET", gitopsTarget); upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel); - upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", source.full); + upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", profile === "v02" ? runtimeCommit : source.full); if (bootMetadata) { upsertEnv(container.env, "HWLAB_RUNTIME_MODE", bootMetadata.runtimeMode); upsertEnv(container.env, "HWLAB_BOOT_REPO", bootMetadata.bootRepo); @@ -1239,6 +1238,7 @@ const plan = JSON.parse(fs.readFileSync("/workspace/source/ci-plan.json", "utf8" const selected = String(process.env.HWLAB_SELECTED_SERVICES || "").split(",").map((item) => item.trim()).filter(Boolean); const allServices = selected.length > 0 ? selected : ${JSON.stringify(defaultServiceIds)}; const affected = new Set(plan.affectedServices || []); +const plannedBuildServices = Array.isArray(plan.buildServices) ? new Set(plan.buildServices) : null; const selectedSet = new Set(selected); const byService = new Map((plan.services || []).map((service) => [service.serviceId, service])); const entries = allServices.map((serviceId) => { @@ -1246,11 +1246,12 @@ const entries = allServices.map((serviceId) => { const serviceSelected = selectedSet.has(serviceId); const rolloutAffected = serviceSelected && affected.has(serviceId); const envReuse = service.runtimeMode === "env-reuse-git-mirror-checkout" || service.envReuse === true; - const buildRequired = envReuse ? service.envChanged === true : rolloutAffected; + const buildRequired = serviceSelected && (plannedBuildServices ? plannedBuildServices.has(serviceId) : (envReuse ? service.envChanged === true : rolloutAffected)); return { serviceId, selected: serviceSelected, affected: buildRequired, + buildRequired, rolloutAffected, runtimeMode: service.runtimeMode || "service-image", envChanged: service.envChanged ?? null, @@ -1263,12 +1264,14 @@ for (const entry of entries) { fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({ sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], + rolloutServices: plan.rolloutServices || plan.affectedServices || [], + buildServices: plan.buildServices || entries.filter((entry) => entry.buildRequired).map((entry) => entry.serviceId), reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0, services: plan.services || [], entries }, null, 2) + String.fromCharCode(10)); -console.log(JSON.stringify({ event: "g14-ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], reusedServices: plan.reusedServices || [], buildSkippedCount: plan.buildSkippedCount || 0 })); +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 })); NODE `; } @@ -1292,6 +1295,7 @@ export HWLAB_ARTIFACT_LANE="$(params.lane)" export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)" export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.json" export HWLAB_ARTIFACT_IMAGE_TAG_MODE="$(params.image-tag-mode)" +mkdir -p /workspace/source/service-results if [ -s /workspace/source/affected-services.json ] && ! node -e 'const fs=require("node:fs"); const p=JSON.parse(fs.readFileSync("/workspace/source/affected-services.json","utf8")); const service=process.argv[1]; const item=(p.entries||[]).find((entry)=>entry.serviceId===service); process.exit(item && item.affected ? 0 : 1);' "$(params.service-id)"; then node - "$(params.service-id)" <<'NODE' const fs = require("node:fs"); @@ -1365,7 +1369,7 @@ if [ "$buildkit_ready" != "1" ]; then echo '{"event":"buildkit-sidecar-not-ready","serviceId":"'"$(params.service-id)"'","addr":"'"$HWLAB_BUILDKIT_ADDR"'"}' >&2 exit 1 fi -node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --no-report --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR" +node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report "/workspace/source/service-results/$(params.service-id).json" --tekton-results-dir /tekton/results --quiet-build --concurrency 1 --services "$(params.service-id)" --buildkit-command /workspace/buildkit-bin/buildctl --buildkit-addr "$HWLAB_BUILDKIT_ADDR" `; } @@ -1386,7 +1390,7 @@ export HWLAB_ARTIFACT_LANE="$(params.lane)" export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)" export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.json" export HWLAB_ARTIFACT_IMAGE_TAG_MODE="$(params.image-tag-mode)" -node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-results-env +node scripts/g14-artifact-publish.mjs --publish --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --registry-prefix "$(params.registry-prefix)" --report /workspace/source/dev-artifacts.json --quiet-build --concurrency 1 --services "$(params.services)" --external-service-report-dir /workspace/source/service-results node scripts/refresh-artifact-catalog.mjs --lane "$(params.lane)" --catalog-path "$(params.catalog-path)" --deploy-json deploy/deploy.json --image-tag-mode "$(params.image-tag-mode)" --target-ref HEAD --publish-report /workspace/source/dev-artifacts.json --no-write `; } @@ -2140,6 +2144,7 @@ function perServiceBuildTask(serviceId, { runAfter = ["plan-artifacts"] } = {}) name: buildTaskName(serviceId), runAfter, workspaces: [{ name: "source", workspace: "source" }], + when: [{ input: `$(tasks.plan-artifacts.results.${affectedResultName(serviceId)})`, operator: "in", values: ["true"] }], taskSpec: { params: [ { name: "revision" }, @@ -2191,11 +2196,10 @@ function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) { { name: "image-tag-mode" }, { name: "registry-prefix" }, { name: "services" }, - { name: "base-image" }, - ...serviceResultParams(serviceIds) + { name: "base-image" } ], workspaces: [{ name: "source" }], - steps: [{ name: "collect", image: ciToolsRunnerImage, env: [...proxyEnv(), ...serviceResultEnv(serviceIds)], script: collectArtifactsScript() }] + steps: [{ name: "collect", image: ciToolsRunnerImage, env: proxyEnv(), script: collectArtifactsScript() }] }, params: [ { name: "revision", value: "$(params.revision)" }, @@ -2204,8 +2208,7 @@ function collectArtifactsTask({ serviceIds = defaultServiceIds } = {}) { { name: "image-tag-mode", value: "$(params.image-tag-mode)" }, { name: "registry-prefix", value: "$(params.registry-prefix)" }, { name: "services", value: "$(params.services)" }, - { name: "base-image", value: "$(params.base-image)" }, - ...serviceResultParamValues(serviceIds) + { name: "base-image", value: "$(params.base-image)" } ] }; } @@ -2310,10 +2313,10 @@ async function runtimeItems() { } function runtimeSourceCommit(item) { - return item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || - item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || - item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || + return item.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || item.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || + item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"] || + item.spec?.template?.metadata?.annotations?.["hwlab.pikastech.local/source-commit"] || null; } diff --git a/scripts/g14-gitops-render.test.ts b/scripts/g14-gitops-render.test.ts index ad10caa8..3e0be770 100644 --- a/scripts/g14-gitops-render.test.ts +++ b/scripts/g14-gitops-render.test.ts @@ -46,7 +46,7 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.match(pipeline, /node scripts\/run-bun\.mjs test cmd\/hwlab-codex-api-responses-forwarder\/main\.test\.ts/u); assert.doesNotMatch(pipeline, /cmd\/hwlab-codex-api-responses-forwarder\/main\.mjs/u); assert.match(pipeline, /process\.exitCode = 1/u); - assert.match(pipeline, /item\.spec\?\.template\?\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u); + assert.match(pipeline, /item\.metadata\?\.labels\?\.\[\\"hwlab\.pikastech\.local\/source-commit\\"\]/u); const runtimeReady = taskByName(pipelineJson, "runtime-ready"); const runtimeReadyScript = runtimeReady.taskSpec.steps[0].script; @@ -113,8 +113,16 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots const task = taskByName(pipelineJson, `build-${serviceId}`); const expectedRunAfter = index === 0 ? ["plan-artifacts"] : [`build-${retainedServiceIds[index - 1]}`]; assert.deepEqual(task.runAfter, expectedRunAfter); + assert.deepEqual(task.when, [{ input: `$(tasks.plan-artifacts.results.affected-${serviceId})`, operator: "in", values: ["true"] }]); } + const collectArtifacts = taskByName(pipelineJson, "collect-artifacts"); + const collectScript = collectArtifacts.taskSpec.steps[0].script; + assert.match(collectScript, /--external-service-report-dir \/workspace\/source\/service-results/u); + assert.doesNotMatch(JSON.stringify(collectArtifacts), /tasks\.build-hwlab-device-pod\.results/u); + const devicePodBuildScript = taskByName(pipelineJson, "build-hwlab-device-pod").taskSpec.steps.find((step) => step.name === "publish")?.script ?? ""; + assert.match(devicePodBuildScript, /--report "\/workspace\/source\/service-results\/\$\(params\.service-id\)\.json"/u); + const workloads = await readFile(path.join(outDir, "runtime-v02", "workloads.yaml"), "utf8"); const workloadsJson = JSON.parse(workloads); const services = await readFile(path.join(outDir, "runtime-v02", "services.yaml"), "utf8"); @@ -140,10 +148,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.equal(agentWorkerEnv.some((entry) => entry.name === "HWLAB_BOOT_COMMIT"), false, "agent-worker must not be switched to env reuse by device-pod work"); const workloadTemplates = workloadsJson.items.filter((item) => item.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/source-commit"]); assert.ok(workloadTemplates.length > 0, "expected rendered pod-template source labels"); - for (const item of workloadTemplates) { - assert.equal(item.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision); - assert.equal(item.spec.template.metadata.annotations["hwlab.pikastech.local/source-commit"], sourceRevision); - } + assert.equal(devicePod.spec.template.metadata.labels["hwlab.pikastech.local/source-commit"], sourceRevision); + assert.equal(devicePod.spec.template.metadata.annotations["hwlab.pikastech.local/source-commit"], sourceRevision); assert.ok(workloadTemplates.some((item) => item.spec.template.metadata.labels["hwlab.pikastech.local/artifact-source-commit"]), "expected artifact source labels on pod templates"); assert.doesNotMatch(workloads, /HWLAB_GATEWAY_SIMU_URL/u); assert.doesNotMatch(workloads, /HWLAB_BOX_SIMU_URL/u); @@ -152,7 +158,8 @@ test("v02 render follows TypeScript runtime checks and does not self-patch boots assert.doesNotMatch(workloads, /HWLAB_TUNNEL_CLIENT_URL/u); assert.doesNotMatch(pipeline, /hwlab-gateway-simu-status/u); - assert.match(pipeline, /hwlab-cloud-api-status/u); + assert.doesNotMatch(pipeline, /tasks\.build-hwlab-cloud-api\.results/u); + assert.match(pipeline, /--external-service-report-dir \/workspace\/source\/service-results/u); const application = await readFile(path.join(outDir, "argocd", "application-v02.yaml"), "utf8"); assert.match(application, /"prune": true/u); diff --git a/scripts/src/g14-ci-plan-lib.mjs b/scripts/src/g14-ci-plan-lib.mjs index 18651784..f4fab236 100644 --- a/scripts/src/g14-ci-plan-lib.mjs +++ b/scripts/src/g14-ci-plan-lib.mjs @@ -145,11 +145,12 @@ export async function createG14CiPlan(options = {}) { const environmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null; const environmentReady = envReuse && /^sha256:[a-f0-9]{64}$/u.test(environmentDigest ?? "") && Boolean(environmentImage); const envChanged = envReuse ? relevantEnvMatches.length > 0 || !environmentReady || catalogRecord?.environmentInputHash !== environmentInputHash : null; - const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash || catalogRecord?.bootCommit !== sourceCommitId : null; + const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash : null; const catalogReuse = envReuse ? envReuseCandidate(catalogRecord, envChanged) : reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0); const affected = envReuse ? Boolean(envChanged || codeChanged) : imageRelevantChangedPaths.length > 0 || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest"; + const buildRequired = envReuse ? envChanged === true : affected; const componentInputPaths = uniqueSorted([ ...model.componentPaths, ...model.sharedPaths, @@ -183,6 +184,7 @@ export async function createG14CiPlan(options = {}) { buildArgsHash, changedPaths: imageRelevantChangedPaths, affected, + buildRequired, runtimeMode: envReuse ? V02_ENV_REUSE_RUNTIME_MODE : "service-image", envReuse, envChanged, @@ -204,6 +206,7 @@ export async function createG14CiPlan(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.affected).map((service) => service.serviceId); return { planVersion: G14_CI_PLAN_VERSION, @@ -222,6 +225,7 @@ export async function createG14CiPlan(options = {}) { serviceIdSource: serviceIdResolution.source, v02EnvReuseServiceIds: [...envReuseServices], defaultComponentLazyBuild: true, + envReuseCodeOnlyFastLane: true, fullBuildFallback: false }, inputs: { @@ -231,16 +235,21 @@ export async function createG14CiPlan(options = {}) { }, changedPaths: normalizedChangedPaths, changedPathSummary: globalChange, - imageBuildRequired: affectedServices.length > 0, + imageBuildRequired: buildServices.length > 0, affectedServices, + rolloutServices: affectedServices, + buildServices, reusedServices, - buildSkippedCount: reusedServices.length, + buildSkippedCount: services.length - buildServices.length, services, ciCdPlan: { - willBuild: affectedServices, + willBuild: buildServices, + willRollout: affectedServices, willReuse: reusedServices, willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0, - noImageBuildReason: affectedServices.length === 0 ? globalChange.summary : null + noImageBuildReason: buildServices.length === 0 + ? (affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary) + : null } }; } @@ -354,7 +363,7 @@ export async function hashGitPaths(repoRoot, targetRef, paths, options = {}) { return !options.skipPath?.(filePath); }) .sort(); - return stableHash({ targetRef, entries: lines }); + return stableHash({ entries: lines }); } export async function lastCommitForPaths(repoRoot, targetRef, paths) {