From c40e0c4723ad7f543e0fb9c1590b44af04b3236a Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 08:20:21 +0000 Subject: [PATCH] fix(cicd): use follower budget for AgentRun reuse reads --- scripts/src/cicd-agentrun-reuse.ts | 34 ++++++++++++++++------------- scripts/src/cicd-branch-follower.ts | 10 +++++---- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/scripts/src/cicd-agentrun-reuse.ts b/scripts/src/cicd-agentrun-reuse.ts index 377e2644..6a91a90a 100644 --- a/scripts/src/cicd-agentrun-reuse.ts +++ b/scripts/src/cicd-agentrun-reuse.ts @@ -46,10 +46,10 @@ interface AgentRunIdentityComparison { previousMissingCount: number | null; } -export function buildAgentRunReusePlan(follower: FollowerSpec, reuseConfig: RuntimeReuseConfig, sourceCommit: string): AgentRunReusePlan { +export function buildAgentRunReusePlan(follower: FollowerSpec, reuseConfig: RuntimeReuseConfig, sourceCommit: string, timeoutSeconds: number): 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 decisions = service === null ? [] : [agentRunReuseDecision(follower.nativeStatus.source.repoPath, stageRef, service, timeoutSeconds)]; const summary = agentRunDecisionSummary(decisions); return { ok: reuseConfig.ok && decisions.length > 0, @@ -111,9 +111,9 @@ export function applyAgentRunReuseConfig(spec: AgentRunLaneSpec, reuseConfig: Ru }; } -export function reusableAgentRunImageArtifact(spec: AgentRunLaneSpec, repoPath: string, sourceCommit: string): { image: AgentRunArtifactService | null; evidence: Record } { +export function reusableAgentRunImageArtifact(spec: AgentRunLaneSpec, repoPath: string, sourceCommit: string, timeoutSeconds: number): { image: AgentRunArtifactService | null; evidence: Record } { const artifactRef = `refs/heads/${spec.gitops.branch}:${spec.deployment.artifactCatalogPath}`; - const result = runCommand(["git", "--git-dir", repoPath, "show", artifactRef], repoRoot, { timeoutMs: 5_000 }); + const result = runCommand(["git", "--git-dir", repoPath, "show", artifactRef], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) }); if (result.exitCode !== 0) { return { image: null, @@ -259,14 +259,14 @@ export function compactAgentRunPayload(value: Record | null): R }; } -function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable>): AgentRunReuseDecision { - const baseRef = parentRef(repoPath, stageRef); +function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable>, timeoutSeconds: number): AgentRunReuseDecision { + const baseRef = parentRef(repoPath, stageRef, timeoutSeconds); 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 sourceIdentity = identityComparison(repoPath, stageRef, baseRef, codePaths, timeoutSeconds); + const envIdentity = identityComparison(repoPath, stageRef, baseRef, envPaths, timeoutSeconds); const runtimeEnabled = runtime?.enabled !== false; const envEnabled = envReuse?.enabled !== false; const runtimeHit = runtimeEnabled && sourceIdentity.hit && envIdentity.hit; @@ -286,9 +286,9 @@ function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonN }; } -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); +function identityComparison(repoPath: string, currentRef: string, baseRef: string | null, paths: string[], timeoutSeconds: number): AgentRunIdentityComparison { + const current = identityDigest(repoPath, currentRef, paths, timeoutSeconds); + const previous = baseRef === null ? null : identityDigest(repoPath, baseRef, paths, timeoutSeconds); const configured = paths.length > 0; const hit = configured && previous !== null && current.sha256 !== null && previous.sha256 !== null && current.sha256 === previous.sha256; return { @@ -301,12 +301,12 @@ function identityComparison(repoPath: string, currentRef: string, baseRef: strin }; } -function identityDigest(repoPath: string, ref: string, paths: string[]): { sha256: string | null; missingCount: number } { +function identityDigest(repoPath: string, ref: string, paths: string[], timeoutSeconds: number): { 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 result = runCommand(["git", "--git-dir", repoPath, "ls-tree", "-r", "-z", "--full-tree", ref, "--", path], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) }); const entries = result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : []; if (entries.length === 0) missingCount += 1; hash.update(path); @@ -319,8 +319,8 @@ function identityDigest(repoPath: string, ref: string, paths: string[]): { sha25 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 }); +function parentRef(repoPath: string, ref: string, timeoutSeconds: number): string | null { + const result = runCommand(["git", "--git-dir", repoPath, "rev-parse", "--verify", `${ref}^`], repoRoot, { timeoutMs: budgetMs(timeoutSeconds) }); const value = result.stdout.trim(); return result.exitCode === 0 && /^[0-9a-f]{40}$/iu.test(value) ? value : null; } @@ -420,3 +420,7 @@ function shortText(value: string): string { const text = value.replace(/\s+/gu, " ").trim(); return text.length <= 300 ? text : text.slice(0, 300); } + +function budgetMs(timeoutSeconds: number): number { + return Math.max(1, timeoutSeconds) * 1000; +} diff --git a/scripts/src/cicd-branch-follower.ts b/scripts/src/cicd-branch-follower.ts index 9cb94415..7e8114a2 100644 --- a/scripts/src/cicd-branch-follower.ts +++ b/scripts/src/cicd-branch-follower.ts @@ -1211,7 +1211,8 @@ async function executeNativeHwlabNodeTrigger(registry: BranchFollowerRegistry, f if (sync !== null && !sync.result.ok) { return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native git-mirror sync failed", startedAt); } - const reuseConfig = requireFollowerRuntimeReuseConfig(follower, observedSha, Math.min(remainingSeconds(startedAt, timeoutSeconds), follower.budgets.statusSeconds)); + const reuseReadTimeoutSeconds = Math.min(remainingSeconds(startedAt, timeoutSeconds), follower.budgets.statusSeconds); + const reuseConfig = requireFollowerRuntimeReuseConfig(follower, observedSha, reuseReadTimeoutSeconds); if (!reuseConfig.ok) return nativeReuseConfigFailure(follower, observedSha, reuseConfig, startedAt); const hwlabReuseError = requiredReuseServiceError(reuseConfig, ["hwlab-cloud-api", "hwlab-runtime"], "runtimeReuse"); if (hwlabReuseError !== null) return nativeReuseConfigFailure(follower, observedSha, invalidRuntimeReuseConfig(reuseConfig, hwlabReuseError), startedAt); @@ -1271,16 +1272,17 @@ async function executeNativeAgentRunTrigger(registry: BranchFollowerRegistry, fo if (sync !== null && !sync.result.ok) { return nativeK8sStageFailure(follower, observedSha, "git-mirror-sync", sync.jobName, sync.result, { action: "sync" }, "native AgentRun git-mirror sync failed", startedAt); } - const reuseConfig = requireFollowerRuntimeReuseConfig(follower, observedSha, Math.min(remainingSeconds(startedAt, timeoutSeconds), follower.budgets.statusSeconds)); + const reuseReadTimeoutSeconds = Math.min(remainingSeconds(startedAt, timeoutSeconds), follower.budgets.statusSeconds); + const reuseConfig = requireFollowerRuntimeReuseConfig(follower, observedSha, reuseReadTimeoutSeconds); if (!reuseConfig.ok) return nativeReuseConfigFailure(follower, observedSha, reuseConfig, startedAt); 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 reusePlan = buildAgentRunReusePlan(follower, reuseConfig, observedSha, reuseReadTimeoutSeconds); const managerDecision = agentRunManagerDecision(reusePlan); const buildJob = `${jobPrefix}-build-${observedSha.slice(0, 12)}`.slice(0, 63); const buildSkipped = managerDecision?.skipImageBuild === true; - const reusedArtifact = buildSkipped ? reusableAgentRunImageArtifact(effectiveSpec, follower.nativeStatus.source.repoPath, observedSha) : null; + const reusedArtifact = buildSkipped ? reusableAgentRunImageArtifact(effectiveSpec, follower.nativeStatus.source.repoPath, observedSha, reuseReadTimeoutSeconds) : 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"); }