From 133e15f429e54ad7f970f2cc46083b14e67404b4 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Tue, 26 May 2026 20:50:39 +0000 Subject: [PATCH 1/2] fix(deploy): skip CDK diff for new stacks in TUI deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CDK's diff() creates a temporary changeset against a non-existent stack, which briefly materializes a ghost stack in REVIEW_IN_PROGRESS state. For projects without file assets (e.g. dataset-only), the subsequent deploy() call races against the ghost stack deletion and fails with "No template found for Stack". Skip diff entirely for new stacks since there's nothing to compare against — everything would show as an addition anyway. --- src/cli/tui/screens/deploy/useDeployFlow.ts | 54 +++++++++++++-------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index 8b0295744..0ef16400a 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -12,6 +12,7 @@ import { parsePolicyEngineOutputs, parsePolicyOutputs, } from '../../../cloudformation'; +import { checkStackStatus } from '../../../cloudformation/stack-status'; import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from '../../../commands/deploy/utils.js'; import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors'; import { ExecLogger } from '../../../logging'; @@ -606,33 +607,46 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState const attrs = context ? computeDeployAttrs(context.projectSpec, 'deploy') : { ...DEFAULT_DEPLOY_ATTRS }; const run = async (): Promise<{ success: true } | { success: false; error: Error }> => { - // Run diff before deploy to capture pre-deploy differences + // Run diff before deploy to capture pre-deploy differences. + // Skip diff for new stacks — there's nothing to compare against, and CDK's diff + // creates a temporary changeset that leaves a ghost stack which races with deploy. if (!isDiffRunningRef.current) { isDiffRunningRef.current = true; setIsDiffLoading(true); setPreDeployDiffStep(prev => ({ ...prev, status: 'running' })); logger.startStep('Computing diff changes...'); - switchableIoHost?.setOnRawMessage((code, _level, message, data) => { - logger.logDiff(code, message); - if (code === 'CDK_TOOLKIT_I4002') { - setDiffSummaries(prev => [...prev, parseStackDiff(data, message)]); - } else if (code === 'CDK_TOOLKIT_I4001') { - setNumStacksWithChanges(parseDiffResult(data).numStacksWithChanges); + + const target = context?.awsTargets[0]; + const currentStackName = stackNames?.[0]; + const stackStatus = target && currentStackName ? await checkStackStatus(target.region, currentStackName) : null; + const isNewStack = stackStatus ? !stackStatus.exists : false; + + if (isNewStack) { + logger.log('New stack — skipping diff (nothing to compare against)'); + } else { + switchableIoHost?.setOnRawMessage((code, _level, message, data) => { + logger.logDiff(code, message); + if (code === 'CDK_TOOLKIT_I4002') { + setDiffSummaries(prev => [...prev, parseStackDiff(data, message)]); + } else if (code === 'CDK_TOOLKIT_I4001') { + setNumStacksWithChanges(parseDiffResult(data).numStacksWithChanges); + } + }); + switchableIoHost?.setVerbose(true); + try { + await cdkToolkitWrapper.diff(); + } catch { + // Diff failure is non-fatal — deploy will proceed + } finally { + switchableIoHost?.setVerbose(false); + switchableIoHost?.setOnRawMessage(null); } - }); - switchableIoHost?.setVerbose(true); - try { - await cdkToolkitWrapper.diff(); - } catch { - // Diff failure is non-fatal — deploy will proceed - } finally { - switchableIoHost?.setVerbose(false); - switchableIoHost?.setOnRawMessage(null); - isDiffRunningRef.current = false; - setIsDiffLoading(false); - logger.endStep('success'); - setPreDeployDiffStep(prev => ({ ...prev, status: 'success' })); } + + isDiffRunningRef.current = false; + setIsDiffLoading(false); + logger.endStep('success'); + setPreDeployDiffStep(prev => ({ ...prev, status: 'success' })); } setPublishAssetsStep(prev => ({ ...prev, status: 'running' })); From 93bc37201ae08f252340344bfede87cffb91956f Mon Sep 17 00:00:00 2001 From: jariy17 Date: Tue, 26 May 2026 21:59:41 +0000 Subject: [PATCH 2/2] fix(deploy): wrap diff phase in try/finally for resilience Wrap the entire diff phase (checkStackStatus + diff) in a try/finally so that transient failures (network, throttling, expired creds) in the stack status check never leave the UI stuck on the diff spinner. If the status check fails, fall through to the existing diff path (worst case: the old race condition, which was already tolerated before this fix). --- src/cli/tui/screens/deploy/useDeployFlow.ts | 23 ++++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/cli/tui/screens/deploy/useDeployFlow.ts b/src/cli/tui/screens/deploy/useDeployFlow.ts index 0ef16400a..4ff82aca9 100644 --- a/src/cli/tui/screens/deploy/useDeployFlow.ts +++ b/src/cli/tui/screens/deploy/useDeployFlow.ts @@ -607,21 +607,28 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState const attrs = context ? computeDeployAttrs(context.projectSpec, 'deploy') : { ...DEFAULT_DEPLOY_ATTRS }; const run = async (): Promise<{ success: true } | { success: false; error: Error }> => { - // Run diff before deploy to capture pre-deploy differences. - // Skip diff for new stacks — there's nothing to compare against, and CDK's diff - // creates a temporary changeset that leaves a ghost stack which races with deploy. + // Run diff before deploy to capture pre-deploy differences if (!isDiffRunningRef.current) { isDiffRunningRef.current = true; setIsDiffLoading(true); setPreDeployDiffStep(prev => ({ ...prev, status: 'running' })); logger.startStep('Computing diff changes...'); - const target = context?.awsTargets[0]; - const currentStackName = stackNames?.[0]; - const stackStatus = target && currentStackName ? await checkStackStatus(target.region, currentStackName) : null; - const isNewStack = stackStatus ? !stackStatus.exists : false; + // Skip diff for new stacks — there's nothing to compare against, and CDK's diff + // creates a temporary changeset that leaves a ghost stack which races with deploy. + let skipDiff = false; + try { + const target = context?.awsTargets[0]; + const currentStackName = stackNames?.[0]; + if (target && currentStackName) { + const status = await checkStackStatus(target.region, currentStackName); + skipDiff = !status.exists; + } + } catch { + // Status check failed — fall through to diff (tolerate old race) + } - if (isNewStack) { + if (skipDiff) { logger.log('New stack — skipping diff (nothing to compare against)'); } else { switchableIoHost?.setOnRawMessage((code, _level, message, data) => {