fix: consume reuse plan in tekton build tasks

This commit is contained in:
UniDesk Codex
2026-07-04 14:59:39 +08:00
parent a5baf520da
commit 996c1d44f6
3 changed files with 85 additions and 9 deletions
+13 -5
View File
@@ -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" },
+71 -3
View File
@@ -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`);
+1 -1
View File
@@ -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}`);
}