Merge pull request #2381 from pikasTech/fix/1515-hwlab-reuse-plan-ci
fix: consume reuse plan in Tekton build tasks
This commit is contained in:
@@ -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,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`);
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user