/** * Phase-aware watchdog + push-progress aggregator for one buildx invocation. * * A single fixed timeout over the whole `docker buildx build --push` run * conflates two phases with opposite failure profiles: the compile phase * (bounded work, a hang means a broken toolchain) and the publish phase * (image push + registry cache export, whose duration scales with layer * size and uplink speed — a dep-bump build re-uploads multi-GB layers and * legitimately pushes for 25+ minutes). Three incidents (2026-07-06, * 2026-07-16 ×2) killed healthy deploys mid-push at the old whole-run cap. * * Split budgets: * - Compile phase: absolute budget (`FJALL_BUILD_TIMEOUT_MS`, default * `DEFAULT_BUILD_TIMEOUT_MS`). Runs from spawn until the exporter * starts. SBOM/provenance generation is deliberately part of this * phase — it is compute, not upload. * - Publish phase: reset-on-progress stall budget * (`FJALL_PUSH_STALL_TIMEOUT_MS`, default * `DEFAULT_PUSH_STALL_TIMEOUT_MS`). A push that keeps making progress * is never killed, however long it takes; a genuinely hung push * (dead network, wedged registry) goes quiet and trips the budget. * * "Progress" means a CHANGE in the rawjson stream — a status whose * current/total/completed moved, a vertex that started/completed, a log * or warning line. A re-emitted status with identical numbers does not * reset the stall budget, so a heartbeating-but-stuck buildkit still * times out. */ import type { RawjsonVertexEvent } from "./rawjsonToVertexEvent.js"; import { type Result } from "./result.js"; export declare const BUILD_TIMEOUT_ENV_VAR = "FJALL_BUILD_TIMEOUT_MS"; export declare const PUSH_STALL_TIMEOUT_ENV_VAR = "FJALL_PUSH_STALL_TIMEOUT_MS"; export interface BuildxBudgets { readonly compilePhaseTimeoutMs: number; readonly publishStallTimeoutMs: number; } export type BuildxTimeoutPhase = "build" | "publish"; export interface SyntheticBuildxProgress { readonly message: string; readonly percentage?: number; } /** * Resolve the two buildx budgets from the environment. Fails (rather than * falling back) on an empty or malformed override. */ export declare function resolveBuildxBudgets(env: NodeJS.ProcessEnv): Result; export declare class BuildxBuildMonitor { private readonly budgets; private readonly controller; private readonly now; private compileTimer; private stallTimer; private lastActivityAt; private lastEmitAt; private lastEmitMessage; private pendingPhaseFlipMessage; private publishStartedFlag; private disposed; private timedOutPhaseValue; /** Last-seen numbers per status, keyed `vertex|id` — change detection. */ private readonly statusMemos; /** Last-seen start/complete/error stamp per vertex digest. */ private readonly vertexStamps; /** Transfer statuses only (push + cache write) — progress aggregation. */ private readonly transfers; constructor(budgets: BuildxBudgets, opts?: { readonly now?: () => number; }); get signal(): AbortSignal; get timedOutPhase(): BuildxTimeoutPhase | undefined; get publishStarted(): boolean; /** * Feed one normalised rawjson event. Returns a throttled synthetic * progress message for the publish phase (or the one-off phase-flip * message), when one is due. No-op after `dispose()` (including a * watchdog trip) — a late-flushed line must not re-arm a timer that * nothing will clear. */ observe(event: RawjsonVertexEvent): SyntheticBuildxProgress | undefined; /** * Human-readable snapshot of how far the publish got — for the stall * error message. Undefined until a transfer status has been observed. */ progressSummary(): string | undefined; dispose(): void; private aggregateTransfers; private maybeSyntheticProgress; private beginPublishPhase; private armStallTimer; private clearStallTimer; private trip; } export declare function buildPhaseTimeoutMessage(budgets: BuildxBudgets): string; export declare function publishStallTimeoutMessage(budgets: BuildxBudgets, summary: string | undefined): string;