fix(cicd): use follower budget for AgentRun reuse reads

This commit is contained in:
Codex
2026-07-04 08:20:21 +00:00
parent 3a0407f87c
commit c40e0c4723
2 changed files with 25 additions and 19 deletions
+19 -15
View File
@@ -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<string, unknown> } {
export function reusableAgentRunImageArtifact(spec: AgentRunLaneSpec, repoPath: string, sourceCommit: string, timeoutSeconds: number): { 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 });
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<string, unknown> | null): R
};
}
function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable<ReturnType<typeof runtimeReuseService>>): AgentRunReuseDecision {
const baseRef = parentRef(repoPath, stageRef);
function agentRunReuseDecision(repoPath: string, stageRef: string, service: NonNullable<ReturnType<typeof runtimeReuseService>>, 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;
}
+6 -4
View File
@@ -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");
}