fix: expose AgentRun branch-follower CI reuse evidence

This commit is contained in:
Codex
2026-07-04 08:08:41 +00:00
parent 111aa222bb
commit 3a0407f87c
5 changed files with 562 additions and 44 deletions
+92 -2
View File
@@ -18,6 +18,8 @@ const pipelineRunPrefix = process.env.PIPELINE_RUN_PREFIX || "";
const argoNamespace = process.env.ARGO_NAMESPACE || "";
const argoApplication = process.env.ARGO_APPLICATION || "";
const runtimeNamespace = process.env.RUNTIME_NAMESPACE || "";
const stateNamespace = process.env.STATE_NAMESPACE || "";
const stateConfigMap = process.env.STATE_CONFIGMAP || "";
const workloads = parseWorkloads(process.env.WORKLOADS_B64 || "");
const healthUrl = process.env.HEALTH_URL || "";
const slowTaskSeconds = requiredPositiveIntEnv("SLOW_TASK_SECONDS");
@@ -91,8 +93,9 @@ async function ciTaskRunEvidence(commit) {
const taskRuns = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(tektonNamespace)}/taskruns?labelSelector=${encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRunName}`)}`, false);
const taskSummary = taskRunsSummary(taskRuns);
const planArtifacts = planArtifactsEvidence(pipelineRunName);
const storedCiConsumption = await storedAgentRunCiConsumption(commit);
const buildTaskRunServices = buildTaskServices(taskRuns);
const ciConsumption = ciConsumptionSummary(reusePlan.decisions || [], planArtifacts, buildTaskRunServices);
const ciConsumption = ciConsumptionSummary(reusePlan.decisions || [], planArtifacts, buildTaskRunServices, storedCiConsumption);
return {
ok: prStatus.succeeded === true && taskSummary.failedCount === 0 && taskSummary.activeCount === 0 && ciConsumption.ok === true,
reusePlan: {
@@ -100,6 +103,7 @@ async function ciTaskRunEvidence(commit) {
decisionSummary: compactDecisionSummary(reusePlan.decisionSummary),
},
planArtifacts,
storedCiConsumption,
ciConsumption,
pipelineRun: prStatus,
pipeline: {
@@ -250,9 +254,55 @@ function planArtifactsEvidence(pipelineRunName) {
}
}
function ciConsumptionSummary(decisions, planArtifacts, buildTaskRunServices) {
function ciConsumptionSummary(decisions, planArtifacts, buildTaskRunServices, storedCiConsumption) {
const skipExpected = decisions.filter((item) => item.skipImageBuild === true).map((item) => item.serviceId).sort();
const buildExpected = decisions.filter((item) => item.skipImageBuild !== true).map((item) => item.serviceId).sort();
if (/agentrun/u.test(follower) && storedCiConsumption?.present === true && storedCiConsumption.ok !== true) {
return {
ok: false,
reason: storedCiConsumption.reason || "ci-consumption-evidence-missing",
source: "branch-follower-compact-state",
expected: {
skipImageBuildCount: skipExpected.length,
buildImageCount: buildExpected.length,
},
observed: {
buildServicesCount: 0,
buildTaskRunServices,
reusedServicesCount: 0,
skippedOrReusedServicesCount: 0,
buildSkippedCount: null,
},
mismatches: [],
};
}
if (storedCiConsumption?.ok === true) {
const observed = storedCiConsumption.observed || {};
const unexpectedStoredBuild = skipExpected.filter((serviceId) => observed.imageBuildDecision === "buildImage" && serviceId === "agentrun-mgr");
const missingStoredBuild = buildExpected.filter((serviceId) => observed.imageBuildDecision !== "buildImage" && serviceId === "agentrun-mgr");
const storedOk = unexpectedStoredBuild.length === 0 && missingStoredBuild.length === 0;
return {
ok: storedOk,
reason: storedOk ? "ci-consumed-reuse-plan" : "ci-consumption-mismatch",
source: "branch-follower-compact-state",
expected: {
skipImageBuildCount: skipExpected.length,
buildImageCount: buildExpected.length,
},
observed: {
buildServicesCount: numberOrNull(observed.buildServicesCount),
buildTaskRunServices,
reusedServicesCount: numberOrNull(observed.reusedServicesCount),
skippedOrReusedServicesCount: numberOrNull(observed.reusedServicesCount),
buildSkippedCount: numberOrNull(observed.buildSkippedCount),
imageBuildDecision: str(observed.imageBuildDecision),
imageBuildStatus: str(observed.imageBuildStatus),
imageBuildCreated: observed.imageBuildCreated === true,
imageBuildReused: observed.imageBuildReused === true,
},
mismatches: compactCiMismatches(unexpectedStoredBuild, missingStoredBuild),
};
}
const buildObserved = uniqueStrings([...strings(planArtifacts?.buildServices), ...buildTaskRunServices]).sort();
const reusedObserved = strings(planArtifacts?.reusedServices).sort();
const skippedObserved = uniqueStrings(reusedObserved).sort();
@@ -278,6 +328,33 @@ function ciConsumptionSummary(decisions, planArtifacts, buildTaskRunServices) {
};
}
async function storedAgentRunCiConsumption(commit) {
if (!stateNamespace || !stateConfigMap || !follower || !commit) return null;
const cm = await getJson(`/api/v1/namespaces/${encodeURIComponent(stateNamespace)}/configmaps/${encodeURIComponent(stateConfigMap)}`, false);
const raw = cm?.data?.[follower];
if (typeof raw !== "string" || raw.length === 0) return { ok: false, present: false, reason: "compact-state-missing" };
const state = parseJson(raw);
if (state === null) return { ok: false, present: true, reason: "state-json-parse-failed" };
const command = state?.command || {};
const payload = command.payload || {};
const agentrun = payload.agentrun || {};
const sourceCommit = str(agentrun.sourceCommit) || str(command.sourceCommit);
const ci = agentrun.ciConsumption || null;
if (sourceCommit === null && (!ci || typeof ci !== "object")) return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit: null };
if (sourceCommit !== commit) return { ok: false, present: true, reason: "state-source-commit-mismatch", sourceCommit };
if (!ci || typeof ci !== "object") return { ok: false, present: true, reason: "ci-consumption-evidence-missing", sourceCommit };
return {
ok: ci.ok === true,
present: true,
sourceCommit,
source: str(ci.source),
expected: ci.expected || null,
observed: ci.observed || null,
mismatches: Array.isArray(ci.mismatches) ? ci.mismatches.slice(0, 6) : [],
valuesRedacted: true,
};
}
function compactCiMismatches(unexpectedBuild, missingBuild) {
return [
...(unexpectedBuild.length > 0 ? [{ reason: "expected-skipImageBuild-but-ci-buildServices-includes-service", serviceIds: unexpectedBuild }] : []),
@@ -692,6 +769,19 @@ function str(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
function parseJson(value) {
try {
const parsed = JSON.parse(value);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
} catch {
return null;
}
}
function numberOrNull(value) {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function strings(value) {
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
}
+422
View File
@@ -0,0 +1,422 @@
// SPEC: PJ2026-01060703 CI/CD branch follower AgentRun reuse decisions.
// Responsibility: compute AgentRun per-service reuse decisions and bounded CI consumption evidence.
import { createHash } from "node:crypto";
import { repoRoot } from "./config";
import { runCommand } from "./command";
import type { AgentRunArtifactService } from "./agentrun-manifests";
import type { AgentRunLaneSpec } from "./agentrun-lanes";
import type { FollowerSpec, NativeK8sJobResult } from "./cicd-types";
import { RUNTIME_REUSE_CONFIG_PATH, runtimeReuseService, type RuntimeReuseConfig } from "./cicd-reuse-config";
export interface AgentRunReusePlan {
ok: boolean;
sourceCommit: string;
stageRef: string;
decisions: AgentRunReuseDecision[];
decisionSummary: {
serviceCount: number;
skipImageBuildCount: number;
buildImageCount: number;
skipImageBuildServices: string[];
buildImageServices: string[];
};
errors: string[];
valuesRedacted: true;
}
export interface AgentRunReuseDecision {
serviceId: string;
sourceIdentity: AgentRunIdentityComparison;
envIdentity: AgentRunIdentityComparison;
runtimeReuse: { enabled: boolean; hit: boolean; reason: string };
envReuse: { enabled: boolean; hit: boolean; reason: string };
skipImageBuild: boolean;
buildDecision: "skipImageBuild" | "buildImage";
reusableImageRef: string | null;
reusableImageRefKnown: boolean;
reason: string;
}
interface AgentRunIdentityComparison {
configured: boolean;
pathCount: number;
hit: boolean;
status: "not-configured" | "base-missing" | "hit" | "miss";
currentMissingCount: number;
previousMissingCount: number | null;
}
export function buildAgentRunReusePlan(follower: FollowerSpec, reuseConfig: RuntimeReuseConfig, sourceCommit: string): AgentRunReusePlan {
const stageRef = `${follower.source.snapshotPrefix.replace(/\/+$/u, "")}/${sourceCommit}`;
const service = runtimeReuseService(reuseConfig, ["agentrun-mgr", "manager"]);
const decisions = service === null ? [] : [agentRunReuseDecision(follower.nativeStatus.source.repoPath, stageRef, service)];
const summary = agentRunDecisionSummary(decisions);
return {
ok: reuseConfig.ok && decisions.length > 0,
sourceCommit,
stageRef,
decisions,
decisionSummary: summary,
errors: service === null ? [`${RUNTIME_REUSE_CONFIG_PATH} must declare service agentrun-mgr|manager`] : reuseConfig.errors.slice(0, 5),
valuesRedacted: true,
};
}
export function compactAgentRunReusePlan(plan: AgentRunReusePlan): Record<string, unknown> {
return {
ok: plan.ok,
sourceCommit: plan.sourceCommit,
stageRef: plan.stageRef,
decisionSummary: plan.decisionSummary,
decisions: plan.decisions.slice(0, 8).map((decision) => ({
serviceId: decision.serviceId,
sourceIdentity: compactIdentity(decision.sourceIdentity),
envIdentity: compactIdentity(decision.envIdentity),
runtimeReuse: { enabled: decision.runtimeReuse.enabled, hit: decision.runtimeReuse.hit },
envReuse: { enabled: decision.envReuse.enabled, hit: decision.envReuse.hit },
skipImageBuild: decision.skipImageBuild,
buildDecision: decision.buildDecision,
reusableImageRef: decision.reusableImageRef,
reusableImageRefKnown: decision.reusableImageRefKnown,
reason: decision.reason,
})),
errors: plan.errors.slice(0, 5),
valuesRedacted: true,
};
}
export function agentRunManagerDecision(plan: AgentRunReusePlan): AgentRunReuseDecision | null {
return plan.decisions.find((decision) => decision.serviceId === "agentrun-mgr" || decision.serviceId === "manager") ?? null;
}
export function applyAgentRunReuseConfig(spec: AgentRunLaneSpec, reuseConfig: RuntimeReuseConfig): AgentRunLaneSpec {
const service = runtimeReuseService(reuseConfig, ["agentrun-mgr", "manager"]);
const envReuse = service?.envReuse;
if (envReuse === undefined || envReuse === null) return spec;
if (envReuse.enabled === false) return spec;
const imageBuild = spec.deployment.manager.imageBuild;
return {
...spec,
deployment: {
...spec.deployment,
manager: {
...spec.deployment.manager,
imageBuild: {
...imageBuild,
buildArgs: Object.keys(envReuse.buildArgs).length === 0 ? imageBuild.buildArgs : envReuse.buildArgs,
envIdentityFiles: envReuse.envIdentityFiles.length === 0 ? imageBuild.envIdentityFiles : envReuse.envIdentityFiles,
},
},
},
};
}
export function reusableAgentRunImageArtifact(spec: AgentRunLaneSpec, repoPath: string, sourceCommit: string): { image: AgentRunArtifactService | null; evidence: Record<string, unknown> } {
const artifactRef = `refs/heads/${spec.gitops.branch}:${spec.deployment.artifactCatalogPath}`;
const result = runCommand(["git", "--git-dir", repoPath, "show", artifactRef], repoRoot, { timeoutMs: 5_000 });
if (result.exitCode !== 0) {
return {
image: null,
evidence: {
ok: false,
status: "missing",
artifactRef,
reason: shortText(result.stderr || result.stdout || "artifact catalog missing"),
valuesRedacted: true,
},
};
}
const parsed = parseJsonRecord(result.stdout);
const services = Array.isArray(parsed?.services) ? parsed.services : [];
const service = services.find((item) => record(item).serviceId === "agentrun-mgr");
const row = record(service);
const digest = stringOrNull(row.digest) ?? stringOrNull(row.envDigest);
const envIdentity = stringOrNull(row.envIdentity) ?? stringOrNull(row.imageTag);
const image = stringOrNull(row.image) ?? stringOrNull(row.envImage);
if (digest === null || envIdentity === null || image === null) {
return {
image: null,
evidence: {
ok: false,
status: "invalid",
artifactRef,
reason: "artifact catalog lacks image/digest/envIdentity for agentrun-mgr",
valuesRedacted: true,
},
};
}
const repositoryDigest = stringOrNull(row.repositoryDigest) ?? `${imageRepository(image)}@${digest}`;
const artifact: AgentRunArtifactService = {
serviceId: "agentrun-mgr",
artifactKind: stringOrNull(row.artifactKind) ?? "env-reuse",
status: "reused",
image,
digest,
repositoryDigest,
imageTag: envIdentity,
envIdentity,
envImage: stringOrNull(row.envImage) ?? image,
envDigest: stringOrNull(row.envDigest) ?? digest,
envRepositoryDigest: stringOrNull(row.envRepositoryDigest) ?? repositoryDigest,
bootCommit: sourceCommit,
bootScript: stringOrNull(row.bootScript) ?? "deploy/runtime/boot/agentrun-boot.sh",
provenance: {
sourceCommitId: sourceCommit,
previousSourceCommitId: stringOrNull(parsed?.sourceCommitId),
source: "unidesk-yaml-only-reuse",
artifactRef,
valuesPrinted: false,
},
};
return {
image: artifact,
evidence: {
ok: true,
status: "reused",
artifactRef,
previousSourceCommitId: stringOrNull(parsed?.sourceCommitId),
image,
digest,
envIdentity,
valuesRedacted: true,
},
};
}
export function skippedAgentRunImageBuildResult(namespace: string, jobName: string, summary: Record<string, unknown>): NativeK8sJobResult {
return {
ok: true,
completed: true,
failed: false,
timedOut: false,
created: false,
reused: true,
jobName,
namespace,
polls: 0,
elapsedMs: 0,
logsTail: null,
summary,
conditionReason: "Skipped",
conditionMessage: "reuse-plan skipImageBuild consumed by AgentRun trigger",
statusAuthority: "kubernetes-api-serviceaccount",
parsedDownstreamCliOutput: false,
};
}
export function agentRunCiConsumptionEvidence(input: {
sourceCommit: string;
plan: AgentRunReusePlan;
imageBuild: { jobName: string; result: NativeK8sJobResult; payload: Record<string, unknown> };
gitopsPublish: { jobName: string; result: NativeK8sJobResult; payload: Record<string, unknown> };
}): Record<string, unknown> {
const decision = agentRunManagerDecision(input.plan);
const expectedSkip = input.plan.decisionSummary.skipImageBuildCount;
const expectedBuild = input.plan.decisionSummary.buildImageCount;
const skipped = decision?.skipImageBuild === true;
const imageBuildConsumed = skipped
? input.imageBuild.result.reused === true && input.imageBuild.payload.status === "skipped"
: input.imageBuild.result.completed === true && input.imageBuild.payload.status !== "skipped";
return {
ok: input.plan.ok && imageBuildConsumed && input.gitopsPublish.result.completed === true,
sourceCommit: input.sourceCommit,
source: "agentrun-native-trigger",
expected: {
skipImageBuildCount: expectedSkip,
buildImageCount: expectedBuild,
skipImageBuildServices: input.plan.decisionSummary.skipImageBuildServices,
buildImageServices: input.plan.decisionSummary.buildImageServices,
},
observed: {
imageBuildDecision: skipped ? "skipImageBuild" : "buildImage",
imageBuildStatus: stringOrNull(input.imageBuild.payload.status),
imageBuildJobName: input.imageBuild.jobName,
imageBuildCreated: input.imageBuild.result.created,
imageBuildReused: input.imageBuild.result.reused,
gitopsPublishJobName: input.gitopsPublish.jobName,
gitopsPublishCompleted: input.gitopsPublish.result.completed,
buildServicesCount: skipped ? 0 : 1,
reusedServicesCount: skipped ? 1 : 0,
buildSkippedCount: skipped ? 1 : 0,
},
mismatches: imageBuildConsumed ? [] : [{ reason: "image-build-stage-did-not-consume-reuse-plan", serviceIds: ["agentrun-mgr"] }],
valuesRedacted: true,
};
}
export function compactAgentRunPayload(value: Record<string, unknown> | null): Record<string, unknown> | null {
if (value === null) return null;
return {
configPath: stringOrNull(value.configPath),
sourceCommit: stringOrNull(value.sourceCommit),
reusePlan: compactRecord(value.reusePlan),
ciConsumption: compactRecord(value.ciConsumption),
gitMirrorSync: compactStageRecord(value.gitMirrorSync),
imageBuild: compactStageRecord(value.imageBuild),
gitopsPublish: compactStageRecord(value.gitopsPublish),
gitMirrorFlush: compactStageRecord(value.gitMirrorFlush),
valuesRedacted: true,
};
}
function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable<ReturnType<typeof runtimeReuseService>>): AgentRunReuseDecision {
const baseRef = parentRef(repoPath, stageRef);
const runtime = service.runtimeReuse;
const envReuse = service.envReuse;
const codePaths = runtime?.codeIdentityPaths ?? [];
const envPaths = uniqueStrings([...(runtime?.envIdentityPaths ?? []), ...(envReuse?.envIdentityFiles ?? [])]);
const sourceIdentity = identityComparison(repoPath, stageRef, baseRef, codePaths);
const envIdentity = identityComparison(repoPath, stageRef, baseRef, envPaths);
const runtimeEnabled = runtime?.enabled !== false;
const envEnabled = envReuse?.enabled !== false;
const runtimeHit = runtimeEnabled && sourceIdentity.hit && envIdentity.hit;
const envHit = envEnabled && envIdentity.hit;
const skipImageBuild = runtimeHit || envHit;
return {
serviceId: service.id,
sourceIdentity,
envIdentity,
runtimeReuse: { enabled: runtimeEnabled, hit: runtimeHit, reason: runtimeHit ? "source-and-env-identity-hit" : missReason(sourceIdentity, envIdentity, runtimeEnabled) },
envReuse: { enabled: envEnabled, hit: envHit, reason: envHit ? "env-identity-hit" : missReason(null, envIdentity, envEnabled) },
skipImageBuild,
buildDecision: skipImageBuild ? "skipImageBuild" : "buildImage",
reusableImageRef: null,
reusableImageRefKnown: false,
reason: runtimeHit ? "runtime-reuse-hit" : envHit ? "env-reuse-hit" : "reuse-miss",
};
}
function identityComparison(repoPath: string, currentRef: string, baseRef: string | null, paths: string[]): AgentRunIdentityComparison {
const current = identityDigest(repoPath, currentRef, paths);
const previous = baseRef === null ? null : identityDigest(repoPath, baseRef, paths);
const configured = paths.length > 0;
const hit = configured && previous !== null && current.sha256 !== null && previous.sha256 !== null && current.sha256 === previous.sha256;
return {
configured,
pathCount: paths.length,
hit,
status: !configured ? "not-configured" : previous === null ? "base-missing" : hit ? "hit" : "miss",
currentMissingCount: current.missingCount,
previousMissingCount: previous?.missingCount ?? null,
};
}
function identityDigest(repoPath: string, ref: string, paths: string[]): { sha256: string | null; missingCount: number } {
if (paths.length === 0) return { sha256: null, missingCount: 0 };
const hash = createHash("sha256");
let missingCount = 0;
for (const path of paths) {
const result = runCommand(["git", "--git-dir", repoPath, "ls-tree", "-r", "-z", "--full-tree", ref, "--", path], repoRoot, { timeoutMs: 5_000 });
const entries = result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
if (entries.length === 0) missingCount += 1;
hash.update(path);
hash.update("\0");
for (const entry of entries) {
hash.update(entry);
hash.update("\0");
}
}
return { sha256: hash.digest("hex"), missingCount };
}
function parentRef(repoPath: string, ref: string): string | null {
const result = runCommand(["git", "--git-dir", repoPath, "rev-parse", "--verify", `${ref}^`], repoRoot, { timeoutMs: 5_000 });
const value = result.stdout.trim();
return result.exitCode === 0 && /^[0-9a-f]{40}$/iu.test(value) ? value : null;
}
function agentRunDecisionSummary(decisions: AgentRunReuseDecision[]): AgentRunReusePlan["decisionSummary"] {
const skip = decisions.filter((item) => item.skipImageBuild).map((item) => item.serviceId).sort();
const build = decisions.filter((item) => !item.skipImageBuild).map((item) => item.serviceId).sort();
return {
serviceCount: decisions.length,
skipImageBuildCount: skip.length,
buildImageCount: build.length,
skipImageBuildServices: skip,
buildImageServices: build,
};
}
function missReason(sourceIdentity: AgentRunIdentityComparison | null, envIdentity: AgentRunIdentityComparison, enabled: boolean): string {
if (!enabled) return "reuse-disabled";
if (sourceIdentity?.configured === false || envIdentity.configured === false) return "identity-not-configured";
if (sourceIdentity?.status === "base-missing" || envIdentity.status === "base-missing") return "base-identity-missing";
if (sourceIdentity?.status === "miss") return "source-identity-miss";
if (envIdentity.status === "miss") return "env-identity-miss";
return "identity-miss";
}
function compactIdentity(value: AgentRunIdentityComparison): Record<string, unknown> {
return {
configured: value.configured,
pathCount: value.pathCount,
hit: value.hit,
status: value.status,
missing: {
current: value.currentMissingCount,
previous: value.previousMissingCount,
},
};
}
function compactStageRecord(value: unknown): Record<string, unknown> | null {
const stage = record(value);
if (Object.keys(stage).length === 0) return null;
const result = record(stage.result);
const payload = record(stage.payload);
return {
jobName: stringOrNull(stage.jobName),
result: Object.keys(result).length === 0 ? null : {
ok: result.ok === true,
completed: result.completed === true,
failed: result.failed === true,
timedOut: result.timedOut === true,
created: result.created === true,
reused: result.reused === true,
jobName: stringOrNull(result.jobName),
namespace: stringOrNull(result.namespace),
elapsedMs: typeof result.elapsedMs === "number" ? result.elapsedMs : null,
conditionReason: stringOrNull(result.conditionReason),
conditionMessage: stringOrNull(result.conditionMessage),
statusAuthority: stringOrNull(result.statusAuthority),
parsedDownstreamCliOutput: false,
},
payload: compactRecord(payload),
};
}
function compactRecord(value: unknown): Record<string, unknown> | null {
const row = record(value);
return Object.keys(row).length === 0 ? null : JSON.parse(JSON.stringify(row)) as Record<string, unknown>;
}
function parseJsonRecord(text: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(text) as unknown;
return record(parsed);
} catch {
return null;
}
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function uniqueStrings(values: string[]): string[] {
return [...new Set(values.filter((value) => value.length > 0))];
}
function imageRepository(image: string): string {
const index = image.lastIndexOf(":");
return index > 0 ? image.slice(0, index) : image;
}
function shortText(value: string): string {
const text = value.replace(/\s+/gu, " ").trim();
return text.length <= 300 ? text : text.slice(0, 300);
}
+31 -42
View File
@@ -24,11 +24,12 @@ import { configRefGraph, resolveConfigRefString } from "./ops/config-refs";
import { renderControllerManifests, renderControllerReconcileJob, waitForJobShell } from "./cicd-controller-render";
import { buildDebugStep } from "./cicd-debug";
import { runNativeHwlabControlPlaneRefresh } from "./cicd-hwlab-refresh";
import { agentRunCiConsumptionEvidence, agentRunManagerDecision, applyAgentRunReuseConfig, buildAgentRunReusePlan, compactAgentRunPayload, compactAgentRunReusePlan, reusableAgentRunImageArtifact, skippedAgentRunImageBuildResult } from "./cicd-agentrun-reuse";
import { nativeCicdScriptLoadShell, readNativeObjectBundle } from "./cicd-native-bundle";
import { compactRefreshEvidence, followerEvidenceSummary } from "./cicd-evidence";
import { runNativeK8sJob, runNativeTektonPipelineRun } from "./cicd-native";
import { argoApplicationReady, nativeArgoSummary, nativeGitMirrorReady, nativeGitMirrorRequired, nativeGitMirrorSummary, nativePipelineRunSummary, nativeRuntimeSummary, pipelineRunSucceeded, runtimeTargetShaFromWorkloads, runtimeWorkloadsReady } from "./cicd-native-summary";
import { invalidRuntimeReuseConfig, missingRuntimeReuseConfig, parseRuntimeReuseConfig, RUNTIME_REUSE_CONFIG_PATH, runtimeReuseService, summarizeRuntimeReuseConfig, type RuntimeReuseConfig } from "./cicd-reuse-config";
import { invalidRuntimeReuseConfig, missingRuntimeReuseConfig, parseRuntimeReuseConfig, requiredReuseServiceError, RUNTIME_REUSE_CONFIG_PATH, summarizeRuntimeReuseConfig, type RuntimeReuseConfig } from "./cicd-reuse-config";
import { prioritizedTaskRunItems } from "./cicd-taskruns";
import { runBranchFollowerTaskRunDrillDown } from "./cicd-taskrun-drilldown";
import { runBranchFollowerJobDrillDown, runBranchFollowerRuntimeDrillDown } from "./cicd-job-runtime-drilldown";
@@ -1275,15 +1276,30 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
const agentRunReuseError = requiredReuseServiceError(reuseConfig, ["agentrun-mgr", "manager"], "envReuse");
if (agentRunReuseError !== null) return nativeReuseConfigFailure(follower, observedSha, invalidRuntimeReuseConfig(reuseConfig, agentRunReuseError), startedAt);
const effectiveSpec = applyAgentRunReuseConfig(spec, reuseConfig);
const reusePlan = buildAgentRunReusePlan(follower, reuseConfig, observedSha);
const managerDecision = agentRunManagerDecision(reusePlan);
const buildJob = `${jobPrefix}-build-${observedSha.slice(0, 12)}`.slice(0, 63);
const build = runNativeK8sJob(effectiveSpec.ci.namespace, buildJob, yamlLaneK3sBuildImageJobManifest(effectiveSpec, observedSha, buildJob), Math.min(remainingSeconds(startedAt, timeoutSeconds), effectiveSpec.deployment.manager.imageBuild.timeoutSeconds), "buildkit", registry.controller.budgets);
const buildPayload = yamlLaneGitopsPublishPayloadFromProbe({ logsTail: stringOrNull(build.logsTail) ?? "" });
const buildSkipped = managerDecision?.skipImageBuild === true;
const reusedArtifact = buildSkipped ? reusableAgentRunImageArtifact(effectiveSpec, follower.nativeStatus.source.repoPath, observedSha) : null;
if (buildSkipped && reusedArtifact?.image === null) {
return nativeTriggerError(follower, `native AgentRun image reuse failed: ${stringOrNull(reusedArtifact.evidence.reason) ?? "reusable image artifact missing"}`, "agentrun-reusable-image-missing");
}
const build = buildSkipped && reusedArtifact !== null
? skippedAgentRunImageBuildResult(effectiveSpec.ci.namespace, buildJob, { decision: "skipImageBuild", reason: managerDecision?.reason ?? "reuse-plan-hit", artifact: reusedArtifact.evidence })
: runNativeK8sJob(effectiveSpec.ci.namespace, buildJob, yamlLaneK3sBuildImageJobManifest(effectiveSpec, observedSha, buildJob), Math.min(remainingSeconds(startedAt, timeoutSeconds), effectiveSpec.deployment.manager.imageBuild.timeoutSeconds), "buildkit", registry.controller.budgets);
const buildPayload = buildSkipped && reusedArtifact !== null
? { ok: true, status: "skipped", sourceCommit: observedSha, skipImageBuild: true, buildDecision: "skipImageBuild", reason: managerDecision?.reason ?? "reuse-plan-hit", artifact: reusedArtifact.evidence, valuesPrinted: false }
: yamlLaneGitopsPublishPayloadFromProbe({ logsTail: stringOrNull(build.logsTail) ?? "" });
const digest = stringOrNull(buildPayload.digest);
const envIdentity = stringOrNull(buildPayload.envIdentity);
if (!build.ok || digest === null || envIdentity === null) {
const image = buildSkipped && reusedArtifact !== null
? reusedArtifact.image
: digest !== null && envIdentity !== null
? agentRunImageArtifact(effectiveSpec, { sourceCommit: observedSha, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" })
: null;
if (!build.ok || image === null) {
return nativeK8sStageFailure(follower, observedSha, "image-build", buildJob, build, buildPayload, "native AgentRun image build failed", startedAt);
}
const image = agentRunImageArtifact(effectiveSpec, { sourceCommit: observedSha, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
const renderedFiles = renderAgentRunGitopsFiles(effectiveSpec, { sourceCommit: observedSha, image });
const publishJob = `${jobPrefix}-gitops-${observedSha.slice(0, 12)}`.slice(0, 63);
const publish = runNativeK8sJob(effectiveSpec.gitMirror.namespace, publishJob, yamlLaneGitopsPublishJobManifest(effectiveSpec, renderedFiles, publishJob), remainingSeconds(startedAt, timeoutSeconds), "publish", registry.controller.budgets);
@@ -1291,6 +1307,12 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
if (!publish.ok || publishPayload.ok === false || stringOrNull(publishPayload.gitopsCommit) === null) {
return nativeK8sStageFailure(follower, observedSha, "gitops-publish", publishJob, publish, publishPayload, "native AgentRun GitOps publish failed", startedAt);
}
const ciConsumption = agentRunCiConsumptionEvidence({
sourceCommit: observedSha,
plan: reusePlan,
imageBuild: { jobName: buildJob, result: build, payload: buildPayload },
gitopsPublish: { jobName: publishJob, result: publish, payload: publishPayload },
});
const flush = runNativeGitMirrorStage(registry, follower, observedSha, "flush", remainingSeconds(startedAt, timeoutSeconds));
if (flush !== null && !flush.result.ok) {
return nativeK8sStageFailure(follower, observedSha, "git-mirror-flush", flush.jobName, flush.result, { action: "flush" }, "native AgentRun git-mirror flush failed", startedAt);
@@ -1316,7 +1338,10 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo
...tektonPayload,
agentrun: {
configPath,
sourceCommit: observedSha,
reuseConfig: summarizeRuntimeReuseConfig(reuseConfig),
reusePlan: compactAgentRunReusePlan(reusePlan),
ciConsumption,
gitMirrorSync: sync === null ? null : { jobName: sync.jobName, payload: sync.result },
imageBuild: { jobName: buildJob, result: build, payload: buildPayload },
gitopsPublish: { jobName: publishJob, result: publish, payload: publishPayload },
@@ -1692,21 +1717,6 @@ function nativeReuseConfigFailure(follower: FollowerSpec, observedSha: string, r
};
}
function requiredReuseServiceError(reuseConfig: RuntimeReuseConfig, ids: readonly string[], mode: "runtimeReuse" | "envReuse"): string | null {
const service = runtimeReuseService(reuseConfig, ids);
if (service === null) return `${RUNTIME_REUSE_CONFIG_PATH} must declare service ${ids.join("|")}`;
if (mode === "runtimeReuse") {
if (service.runtimeReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare runtimeReuse`;
if (service.runtimeReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse is disabled`;
if (service.runtimeReuse.codeIdentityPaths.length === 0 && service.runtimeReuse.envIdentityPaths.length === 0) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse must declare codeIdentity or envIdentity paths`;
return null;
}
if (service.envReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare envReuse`;
if (service.envReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse is disabled`;
if (service.envReuse.envIdentityFiles.length === 0 && service.envReuse.nodeDepsPath === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse must declare envIdentityFiles or nodeDepsPath`;
return null;
}
function annotatePipelineRunReuseConfig(manifest: Record<string, unknown>, reuseConfig: RuntimeReuseConfig): Record<string, unknown> {
const metadata = asOptionalRecord(manifest.metadata) ?? {};
const annotations = asOptionalRecord(metadata.annotations) ?? {};
@@ -1719,28 +1729,6 @@ function annotatePipelineRunReuseConfig(manifest: Record<string, unknown>, reuse
return manifest;
}
function applyAgentRunReuseConfig(spec: ReturnType<typeof resolveAgentRunLaneTarget>["spec"], reuseConfig: RuntimeReuseConfig): ReturnType<typeof resolveAgentRunLaneTarget>["spec"] {
const service = runtimeReuseService(reuseConfig, ["agentrun-mgr", "manager"]);
const envReuse = service?.envReuse;
if (envReuse === undefined || envReuse === null) return spec;
if (envReuse.enabled === false) return spec;
const imageBuild = spec.deployment.manager.imageBuild;
return {
...spec,
deployment: {
...spec.deployment,
manager: {
...spec.deployment.manager,
imageBuild: {
...imageBuild,
buildArgs: Object.keys(envReuse.buildArgs).length === 0 ? imageBuild.buildArgs : envReuse.buildArgs,
envIdentityFiles: envReuse.envIdentityFiles.length === 0 ? imageBuild.envIdentityFiles : envReuse.envIdentityFiles,
},
},
},
};
}
function shouldFlushNativeGitMirrorDuringCloseout(follower: FollowerSpec, gitMirror: Record<string, unknown> | null): boolean {
if (!nativeGitMirrorRequired(follower) || gitMirror === null) return false;
if (gitMirror.sourceSnapshotReady === false) return false;
@@ -2333,6 +2321,7 @@ function compactNativePayload(payload: Record<string, unknown> | null): Record<s
pipeline: asOptionalRecord(payload.pipeline),
taskRuns: compactTaskRunsPayload(asOptionalRecord(payload.taskRuns)),
planArtifacts: compactPlanArtifactsPayload(asOptionalRecord(payload.planArtifacts)),
agentrun: compactAgentRunPayload(asOptionalRecord(payload.agentrun)),
argo: asOptionalRecord(payload.argo),
runtime: asOptionalRecord(payload.runtime),
refreshEvidence: compactRefreshEvidence(asOptionalRecord(asOptionalRecord(payload.nativeCapabilities)?.controlPlaneRefresh)),
+2
View File
@@ -102,6 +102,8 @@ function gateJobManifest(registry: BranchFollowerRegistry, follower: FollowerSpe
{ name: "ARGO_NAMESPACE", value: follower.nativeStatus.argo?.namespace ?? "" },
{ name: "ARGO_APPLICATION", value: follower.nativeStatus.argo?.application ?? "" },
{ name: "RUNTIME_NAMESPACE", value: follower.nativeStatus.runtime?.namespace ?? "" },
{ name: "STATE_NAMESPACE", value: registry.controller.namespace },
{ name: "STATE_CONFIGMAP", value: registry.controller.stateConfigMapName },
{ name: "WORKLOADS_B64", value: Buffer.from(JSON.stringify(workloads), "utf8").toString("base64") },
{ name: "HEALTH_URL", value: healthUrl },
{ name: "SLOW_TASK_SECONDS", value: String(gatePolicy.slowTaskSeconds) },
+15
View File
@@ -114,6 +114,21 @@ export function runtimeReuseService(config: RuntimeReuseConfig | null, ids: read
return config.services.find((service) => wanted.has(service.id)) ?? null;
}
export function requiredReuseServiceError(reuseConfig: RuntimeReuseConfig, ids: readonly string[], mode: "runtimeReuse" | "envReuse"): string | null {
const service = runtimeReuseService(reuseConfig, ids);
if (service === null) return `${RUNTIME_REUSE_CONFIG_PATH} must declare service ${ids.join("|")}`;
if (mode === "runtimeReuse") {
if (service.runtimeReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare runtimeReuse`;
if (service.runtimeReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse is disabled`;
if (service.runtimeReuse.codeIdentityPaths.length === 0 && service.runtimeReuse.envIdentityPaths.length === 0) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse must declare codeIdentity or envIdentity paths`;
return null;
}
if (service.envReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare envReuse`;
if (service.envReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse is disabled`;
if (service.envReuse.envIdentityFiles.length === 0 && service.envReuse.nodeDepsPath === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse must declare envIdentityFiles or nodeDepsPath`;
return null;
}
function parseServices(spec: Record<string, unknown>, errors: string[]): RuntimeReuseService[] {
const raw = spec.services;
const items = Array.isArray(raw)