From 996c1d44f6456029f02b2bccb99bad6d44afd070 Mon Sep 17 00:00:00 2001 From: UniDesk Codex Date: Sat, 4 Jul 2026 14:59:39 +0800 Subject: [PATCH] fix: consume reuse plan in tekton build tasks --- scripts/gitops-render.mjs | 18 +++++--- scripts/src/ci-plan-lib.mjs | 74 +++++++++++++++++++++++++++++-- scripts/src/structured-config.mjs | 2 +- 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/scripts/gitops-render.mjs b/scripts/gitops-render.mjs index 2a5777f5..c12edb7a 100644 --- a/scripts/gitops-render.mjs +++ b/scripts/gitops-render.mjs @@ -2085,7 +2085,7 @@ const entries = allServices.map((serviceId) => { return { serviceId, selected: serviceSelected, - affected: buildRequired, + affected: rolloutAffected, buildRequired, rolloutAffected, runtimeMode: service.runtimeMode || "service-image", @@ -2095,6 +2095,7 @@ const entries = allServices.map((serviceId) => { }); for (const entry of entries) { fs.writeFileSync("/tekton/results/affected-" + entry.serviceId, entry.affected ? "true" : "false"); + fs.writeFileSync("/tekton/results/build-" + entry.serviceId, entry.buildRequired ? "true" : "false"); } fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({ sourceCommitId: plan.sourceCommitId, @@ -2108,7 +2109,7 @@ fs.writeFileSync("/workspace/source/affected-services.json", JSON.stringify({ services: plan.services || [], entries }, null, 2) + String.fromCharCode(10)); -console.log(JSON.stringify({ event: "ci-plan", sourceCommitId: plan.sourceCommitId, affectedServices: plan.affectedServices || [], rolloutServices: plan.rolloutServices || plan.affectedServices || [], buildServices: plan.buildServices || [], 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 `; } @@ -2135,7 +2136,7 @@ export HWLAB_ARTIFACT_CATALOG_PATH="$(params.catalog-path)" export HWLAB_DEPLOY_MANIFEST_PATH="deploy/deploy.yaml" 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 +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.buildRequired ? 0 : 1);' "$(params.service-id)"; then node - "$(params.service-id)" <<'NODE' const fs = require("node:fs"); const serviceId = process.argv[2]; @@ -3012,6 +3013,10 @@ function affectedResultName(serviceId) { return `affected-${serviceId}`; } +function buildResultName(serviceId) { + return `build-${serviceId}`; +} + const serviceResultFields = Object.freeze([ "status", "service-id", @@ -3118,7 +3123,10 @@ function planArtifactsTask({ serviceIds = defaultServiceIds } = {}) { { name: "registry-prefix" }, { name: "services" } ], - results: serviceIds.map((serviceId) => ({ name: affectedResultName(serviceId), description: `${serviceId} changed according to ci-plan` })), + results: serviceIds.flatMap((serviceId) => [ + { name: affectedResultName(serviceId), description: `${serviceId} rollout affected according to ci-plan` }, + { name: buildResultName(serviceId), description: `${serviceId} image build required according to ci-plan` } + ]), workspaces: [{ name: "source" }], steps: [{ name: "plan", @@ -3150,7 +3158,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"] }], + when: [{ input: `$(tasks.plan-artifacts.results.${buildResultName(serviceId)})`, operator: "in", values: ["true"] }], taskSpec: { params: [ { name: "revision" }, diff --git a/scripts/src/ci-plan-lib.mjs b/scripts/src/ci-plan-lib.mjs index 7847264c..159f5ded 100644 --- a/scripts/src/ci-plan-lib.mjs +++ b/scripts/src/ci-plan-lib.mjs @@ -71,6 +71,8 @@ export async function createCiPlan(options = {}) { assertLaneEnvReuseConfig(laneConfig, lane); const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane); const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null); + const runtimeReuseConfigPath = options.runtimeReuseConfigPath ?? "gitops/reuse.ymal"; + const runtimeReuseByService = runtimeReuseServices(await readStructuredFileIfPresent(repoRoot, runtimeReuseConfigPath, null)); const sourceCommitId = await gitValue(repoRoot, ["rev-parse", targetRef]); const shortCommitId = await gitValue(repoRoot, ["rev-parse", "--short=7", targetRef]); const changedPaths = await changedPathsBetween(repoRoot, baseRef, targetRef); @@ -230,10 +232,23 @@ export async function createCiPlan(options = {}) { const catalogReuse = envReuse ? envReuseCandidate({ catalogRecord, envChanged, environmentImage, environmentDigest, environmentInputHash }) : reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged); - const affected = envReuse + const plannedAffected = envReuse ? Boolean(envChanged || codeChanged || runtimeConfigChanged) : imageRelevantChangedPaths.length > 0 || componentInputChanged || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest"; - const buildRequired = envReuse ? envChanged === true : affected; + const plannedBuildRequired = envReuse ? envChanged === true : plannedAffected; + const runtimeReuseDecision = await runtimeReuseDecisionForService({ + repoRoot, + baseRef, + targetRef, + serviceId: model.serviceId, + config: runtimeReuseByService.get(model.serviceId) ?? null + }); + const affected = runtimeReuseDecision.runtimeReuseHit === true + ? false + : runtimeReuseDecision.envReuseHit === true + ? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true + : plannedAffected; + const buildRequired = runtimeReuseDecision.skipImageBuild === true ? false : plannedBuildRequired; services.push({ serviceId: model.serviceId, runtimeKind: model.runtimeKind, @@ -260,7 +275,9 @@ export async function createCiPlan(options = {}) { changedPaths: imageRelevantChangedPaths, affected, buildRequired, + skipImageBuild: !buildRequired, componentInputChanged, + runtimeReuseDecision, runtimeMode: envReuse ? ENV_REUSE_RUNTIME_MODE : "service-image", envReuse, envChanged, @@ -290,7 +307,7 @@ 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.affected).map((service) => service.serviceId); + const reusedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId); return { planVersion: CI_PLAN_VERSION, sourceCommitId, @@ -655,6 +672,57 @@ export function enabledEnvReuseServices(deployJson, lane, serviceIds) { return new Set(values.filter((serviceId) => allowed.has(serviceId))); } +function runtimeReuseServices(config) { + const raw = config?.spec?.services ?? config?.services; + const entries = Array.isArray(raw) + ? raw.map((value, index) => [String(value?.id ?? index), value]) + : Object.entries(raw ?? {}); + return new Map(entries.flatMap(([id, value]) => { + const serviceId = String(value?.id ?? id ?? "").trim(); + return serviceId ? [[serviceId, value ?? {}]] : []; + })); +} + +async function runtimeReuseDecisionForService({ repoRoot, baseRef, targetRef, serviceId, config }) { + const runtimeReuse = config?.runtimeReuse ?? null; + const envReuse = config?.envReuse ?? null; + const codePaths = runtimeReusePaths(runtimeReuse?.codeIdentity?.paths ?? runtimeReuse?.codeIdentityPaths); + const envPaths = uniqueSorted([ + ...runtimeReusePaths(runtimeReuse?.envIdentity?.paths ?? runtimeReuse?.envIdentityPaths), + ...runtimeReusePaths(envReuse?.envIdentityFiles ?? envReuse?.envIdentity?.paths) + ]); + const sourceIdentityHit = codePaths.length > 0 ? await identityHit(repoRoot, baseRef, targetRef, codePaths) : null; + const envIdentityHit = envPaths.length > 0 ? await identityHit(repoRoot, baseRef, targetRef, envPaths) : null; + const runtimeReuseEnabled = runtimeReuse?.enabled !== false; + const envReuseEnabled = envReuse?.enabled !== false; + const runtimeReuseHit = runtimeReuseEnabled && sourceIdentityHit === true && envIdentityHit === true; + const envReuseHit = envReuseEnabled && envIdentityHit === true; + return { + source: "gitops/reuse.ymal", + serviceId, + configured: config !== null, + sourceIdentityHit, + envIdentityHit, + runtimeReuseHit, + envReuseHit, + skipImageBuild: runtimeReuseHit || envReuseHit, + buildDecision: runtimeReuseHit || envReuseHit ? "skipImageBuild" : "buildImage", + reason: runtimeReuseHit ? "runtime-reuse-hit" : envReuseHit ? "env-reuse-hit" : "reuse-miss" + }; +} + +function runtimeReusePaths(value) { + return Array.isArray(value) ? uniqueSorted(value.map(normalizeRepoPath).filter(Boolean)) : []; +} + +async function identityHit(repoRoot, baseRef, targetRef, paths) { + const [baseHash, targetHash] = await Promise.all([ + hashGitPaths(repoRoot, baseRef, paths), + hashGitPaths(repoRoot, targetRef, paths) + ]); + return baseHash === targetHash; +} + export function bootShForService(deployJson, serviceId, lane = "v02") { const configured = deployJson?.lanes?.[lane]?.bootScripts?.[serviceId]; if (!configured) throw new Error(`deploy.lanes.${lane}.bootScripts.${serviceId} is required`); diff --git a/scripts/src/structured-config.mjs b/scripts/src/structured-config.mjs index 0ffdff6c..e96c9f77 100644 --- a/scripts/src/structured-config.mjs +++ b/scripts/src/structured-config.mjs @@ -6,7 +6,7 @@ import * as YAML from "yaml"; export function structuredConfigFormatForPath(relativePath) { const extension = path.extname(relativePath).toLowerCase(); if (extension === ".json") return "json"; - if (extension === ".yaml" || extension === ".yml") return "yaml"; + if (extension === ".yaml" || extension === ".yml" || extension === ".ymal") return "yaml"; throw new Error(`unsupported structured config extension for ${relativePath}`); }