fix(cicd): use follower budget for AgentRun reuse reads
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user