fix: keep running containers until stack pull/build succeeds (#1657)

* fix: keep running containers until stack pull/build succeeds

Acquire images before reconcile, capture a recovery generation for
compensation, and only remove classified orphans after handoff.

* fix: address recovery audit blockers for safe stack updates

Retire abandoned and expired recovery artifacts, probe compensated
runtimes before reporting rollback success, preserve local Docker when
deleting a node, validate the exact Compose invocation before capture,
and repair updateStack return-contract fixtures.

* fix: resolve ESLint errors blocking CI on this branch

Unused-import and unused-variable errors left over from the stack
deletion refactor: MeshService in stacks.ts (its opt-out cascade moved
into DeployedStackDeletionService), a redundant pruneVolumes
destructure in deleteDeployedStack (the real one is re-derived from
the same input object inside runDeletionBody), and an unused beforeAll
import in a Docker-integration test stub. Also scopes the webhook
pull-action case body in a block to satisfy no-case-declarations;
purely syntactic, no behavior change.

* fix: harden recovery probe, cleanup retry, and failed-pull Docker test

Reject absent or unhealthy expected replicas before reporting rollback
success, keep cleanup records until artifacts are actually removed, fail
closed when a mesh override cannot be generated, and assert a real
failed pull leaves the original container running.

* fix: verify recovery probe image identity and stack-scoped override paths

Reject recovered runtimes that use the wrong image or leave scale-zero
services running, and confine tombstone override deletion to the intent
stack directory so forged cross-stack paths cannot be swept.

* test: batch notification cap fixtures in a SQLite transaction

Unbatched 1200-row inserts were timing out at the default 30s under
CI load even though the same assertions pass in under 2s when green.
This commit is contained in:
Anso
2026-07-21 12:18:01 -04:00
committed by GitHub
parent b1decbb32a
commit 3f1f15a6f4
41 changed files with 3087 additions and 244 deletions
@@ -47,7 +47,7 @@ export interface ServiceUpdateOptions {
}
export type OrchestratorResult =
| { kind: 'stack_compose_done' }
| { kind: 'stack_compose_done'; recoveryId: string | null }
| {
kind: 'service_done';
serviceName: string;
@@ -182,10 +182,11 @@ export class StackUpdateOrchestrator {
ctx: UpdateOperationContext,
options: StackComposeOptions,
): Promise<OrchestratorResult> {
await ComposeService.getInstance(ctx.nodeId).updateStack(
const updateResult = await ComposeService.getInstance(ctx.nodeId).updateStack(
ctx.stackName, options.terminalWs ?? undefined, options.atomic,
);
return { kind: 'stack_compose_done' };
const recoveryId = updateResult?.recoveryId ?? null;
return { kind: 'stack_compose_done', recoveryId };
}
private async executeServiceUpdate(
@@ -195,6 +196,16 @@ export class StackUpdateOrchestrator {
): Promise<OrchestratorResult> {
const { nodeId, stackName } = ctx;
{
const { StackUpdateRecoveryService } = await import('./StackUpdateRecoveryService');
if (StackUpdateRecoveryService.getInstance().isRestoredCurrentPinActive(nodeId, stackName)) {
return serviceFailed(
'stack_recovery_pin_active',
'This stack is pinned to a restored recovery generation. Run a full-stack Update to continue.',
);
}
}
const loaded = await this.loadServiceSpec(nodeId, stackName, serviceName);
if (!loaded.ok) return loaded.result;
const { spec, services } = loaded;
@@ -308,6 +319,16 @@ export class StackUpdateOrchestrator {
const { nodeId, stackName } = ctx;
const recoveryId = options.recoveryId as string;
{
const { StackUpdateRecoveryService } = await import('./StackUpdateRecoveryService');
if (StackUpdateRecoveryService.getInstance().isRestoredCurrentPinActive(nodeId, stackName)) {
return serviceFailed(
'stack_recovery_pin_active',
'This stack is pinned to a restored recovery generation. Run a full-stack Update to continue.',
{ recoveryId },
);
}
}
const recovery = ServiceUpdateRecoveryService.getInstance().get(recoveryId);
if (
!recovery ||