@go-to-k/cdkd 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/dist/index.js CHANGED
@@ -613,6 +613,22 @@ var LiveRenderer = class {
613
613
  if (this.active)
614
614
  this.draw();
615
615
  }
616
+ /**
617
+ * Replace the label of a previously-added task in place. No-op if the
618
+ * task is not currently tracked (e.g. it already finished). Used by the
619
+ * per-resource deadline wrapper to surface a "[taking longer than
620
+ * expected, Nm+]" suffix without disturbing the elapsed-time counter
621
+ * the renderer tracks via `startedAt`.
622
+ */
623
+ updateTaskLabel(id, label) {
624
+ const stackName = getCurrentStackName();
625
+ const task = this.tasks.get(scopedKey(id, stackName));
626
+ if (!task)
627
+ return;
628
+ task.label = label;
629
+ if (this.active)
630
+ this.draw();
631
+ }
616
632
  /**
617
633
  * Print content above the live area. Clears the live area, runs the writer,
618
634
  * then redraws the live area. When the renderer is inactive, the writer
@@ -898,6 +914,38 @@ var ProvisioningError = class _ProvisioningError extends CdkdError {
898
914
  Object.setPrototypeOf(this, _ProvisioningError.prototype);
899
915
  }
900
916
  };
917
+ var ResourceTimeoutError = class _ResourceTimeoutError extends CdkdError {
918
+ constructor(logicalId, resourceType, region, elapsedMs, operation, timeoutMs) {
919
+ const elapsedLabel = formatDuration(elapsedMs);
920
+ const timeoutLabel = formatDuration(timeoutMs);
921
+ super(
922
+ `Resource ${logicalId} (${resourceType}) in ${region} timed out after ${timeoutLabel} during ${operation} (elapsed ${elapsedLabel}).
923
+ This may indicate a stuck Cloud Control polling loop, hung Custom Resource, or
924
+ slow ENI provisioning. Re-run with --resource-timeout 1h if the resource genuinely
925
+ needs more time, or --verbose to see the underlying provider activity.`,
926
+ "RESOURCE_TIMEOUT"
927
+ );
928
+ this.logicalId = logicalId;
929
+ this.resourceType = resourceType;
930
+ this.region = region;
931
+ this.elapsedMs = elapsedMs;
932
+ this.operation = operation;
933
+ this.timeoutMs = timeoutMs;
934
+ this.name = "ResourceTimeoutError";
935
+ Object.setPrototypeOf(this, _ResourceTimeoutError.prototype);
936
+ }
937
+ };
938
+ function formatDuration(ms) {
939
+ if (ms < 6e4) {
940
+ return `${Math.round(ms / 1e3)}s`;
941
+ }
942
+ const totalMinutes = Math.round(ms / 6e4);
943
+ if (totalMinutes < 60)
944
+ return `${totalMinutes}m`;
945
+ const hours = Math.floor(totalMinutes / 60);
946
+ const minutes = totalMinutes % 60;
947
+ return minutes === 0 ? `${hours}h` : `${hours}h${minutes}m`;
948
+ }
901
949
  var DependencyError = class _DependencyError extends CdkdError {
902
950
  constructor(message, cause) {
903
951
  super(message, "DEPENDENCY_ERROR", cause);
@@ -8108,7 +8156,85 @@ async function withRetry(operation, logicalId, opts = {}) {
8108
8156
  throw lastError;
8109
8157
  }
8110
8158
 
8159
+ // src/deployment/resource-deadline.ts
8160
+ var InvalidResourceDeadlineError = class extends Error {
8161
+ constructor(message) {
8162
+ super(message);
8163
+ this.name = "InvalidResourceDeadlineError";
8164
+ }
8165
+ };
8166
+ function validateOptions(opts) {
8167
+ const { warnAfterMs, timeoutMs } = opts;
8168
+ if (!Number.isFinite(warnAfterMs) || !Number.isFinite(timeoutMs) || warnAfterMs <= 0 || timeoutMs <= 0) {
8169
+ throw new InvalidResourceDeadlineError(
8170
+ `withResourceDeadline: warnAfterMs and timeoutMs must be positive finite numbers (got warnAfterMs=${warnAfterMs}, timeoutMs=${timeoutMs})`
8171
+ );
8172
+ }
8173
+ if (warnAfterMs >= timeoutMs) {
8174
+ throw new InvalidResourceDeadlineError(
8175
+ `withResourceDeadline: warnAfterMs (${warnAfterMs}ms) must be less than timeoutMs (${timeoutMs}ms)`
8176
+ );
8177
+ }
8178
+ }
8179
+ async function withResourceDeadline(operation, opts) {
8180
+ validateOptions(opts);
8181
+ const startedAt = Date.now();
8182
+ return new Promise((resolve4, reject) => {
8183
+ let settled = false;
8184
+ let warnTimer;
8185
+ let timeoutTimer;
8186
+ const cleanup = () => {
8187
+ if (warnTimer !== void 0)
8188
+ clearTimeout(warnTimer);
8189
+ if (timeoutTimer !== void 0)
8190
+ clearTimeout(timeoutTimer);
8191
+ warnTimer = void 0;
8192
+ timeoutTimer = void 0;
8193
+ };
8194
+ if (opts.onWarn) {
8195
+ warnTimer = setTimeout(() => {
8196
+ if (settled)
8197
+ return;
8198
+ try {
8199
+ opts.onWarn(Date.now() - startedAt);
8200
+ } catch {
8201
+ }
8202
+ }, opts.warnAfterMs);
8203
+ if (typeof warnTimer.unref === "function")
8204
+ warnTimer.unref();
8205
+ }
8206
+ timeoutTimer = setTimeout(() => {
8207
+ if (settled)
8208
+ return;
8209
+ settled = true;
8210
+ cleanup();
8211
+ const elapsed = Date.now() - startedAt;
8212
+ reject(opts.onTimeout(elapsed));
8213
+ }, opts.timeoutMs);
8214
+ if (typeof timeoutTimer.unref === "function")
8215
+ timeoutTimer.unref();
8216
+ Promise.resolve().then(() => operation()).then(
8217
+ (value) => {
8218
+ if (settled)
8219
+ return;
8220
+ settled = true;
8221
+ cleanup();
8222
+ resolve4(value);
8223
+ },
8224
+ (err) => {
8225
+ if (settled)
8226
+ return;
8227
+ settled = true;
8228
+ cleanup();
8229
+ reject(err);
8230
+ }
8231
+ );
8232
+ });
8233
+ }
8234
+
8111
8235
  // src/deployment/deploy-engine.ts
8236
+ var DEFAULT_RESOURCE_WARN_AFTER_MS = 5 * 60 * 1e3;
8237
+ var DEFAULT_RESOURCE_TIMEOUT_MS = 30 * 60 * 1e3;
8112
8238
  var InterruptedError = class extends Error {
8113
8239
  constructor() {
8114
8240
  super("Deployment interrupted by user (Ctrl+C)");
@@ -8129,6 +8255,8 @@ var DeployEngine = class {
8129
8255
  this.options.dryRun = options.dryRun ?? false;
8130
8256
  this.options.lockTimeout = options.lockTimeout ?? 5 * 60 * 1e3;
8131
8257
  this.options.noRollback = options.noRollback ?? false;
8258
+ this.options.resourceWarnAfterMs = options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
8259
+ this.options.resourceTimeoutMs = options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
8132
8260
  }
8133
8261
  logger = getLogger().child("DeployEngine");
8134
8262
  resolver;
@@ -8727,259 +8855,305 @@ var DeployEngine = class {
8727
8855
  */
8728
8856
  async provisionResource(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
8729
8857
  const resourceType = change.resourceType;
8730
- const provider = this.providerRegistry.getProvider(resourceType);
8731
8858
  const renderer = getLiveRenderer();
8732
8859
  const needsReplacement = change.changeType === "UPDATE" && (change.propertyChanges?.some((pc) => pc.requiresReplacement) ?? false);
8733
8860
  const verb = change.changeType === "CREATE" ? "Creating" : change.changeType === "DELETE" ? "Deleting" : needsReplacement ? "Replacing" : "Updating";
8734
- renderer.addTask(logicalId, `${verb} ${logicalId} (${resourceType})`);
8861
+ const baseLabel = `${verb} ${logicalId} (${resourceType})`;
8862
+ renderer.addTask(logicalId, baseLabel);
8863
+ const operationKind = change.changeType === "CREATE" ? "CREATE" : change.changeType === "DELETE" ? "DELETE" : "UPDATE";
8864
+ const warnAfterMs = this.options.resourceWarnAfterByType?.[resourceType] ?? this.options.resourceWarnAfterMs ?? DEFAULT_RESOURCE_WARN_AFTER_MS;
8865
+ const timeoutMs = this.options.resourceTimeoutByType?.[resourceType] ?? this.options.resourceTimeoutMs ?? DEFAULT_RESOURCE_TIMEOUT_MS;
8735
8866
  try {
8736
- switch (change.changeType) {
8737
- case "CREATE": {
8738
- const desiredProps = change.desiredProperties || {};
8739
- const context = {
8867
+ await withResourceDeadline(
8868
+ async () => {
8869
+ await this.provisionResourceBody(
8870
+ logicalId,
8871
+ change,
8872
+ stateResources,
8873
+ stackName,
8740
8874
  template,
8741
- resources: stateResources,
8742
- ...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
8743
- ...conditions && Object.keys(conditions).length > 0 && { conditions },
8744
- stateBackend: this.stateBackend,
8745
- stackName
8746
- };
8747
- const resolvedProps = await this.resolver.resolve(desiredProps, context);
8748
- const { provider: createProvider, properties: createProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
8749
- const result = await this.withRetry(
8750
- () => createProvider.create(logicalId, resourceType, createProps),
8875
+ parameterValues,
8876
+ conditions,
8877
+ counts,
8878
+ progress
8879
+ );
8880
+ },
8881
+ {
8882
+ warnAfterMs,
8883
+ timeoutMs,
8884
+ onWarn: (elapsedMs) => {
8885
+ const minutes = Math.max(1, Math.round(elapsedMs / 6e4));
8886
+ const warnSuffix = ` [taking longer than expected, ${minutes}m+]`;
8887
+ renderer.updateTaskLabel(logicalId, `${baseLabel}${warnSuffix}`);
8888
+ renderer.printAbove(() => {
8889
+ this.logger.warn(
8890
+ `${logicalId} (${resourceType}) has been ${operationKind === "CREATE" ? "creating" : operationKind === "DELETE" ? "deleting" : "updating"} for ${minutes}m \u2014 still waiting`
8891
+ );
8892
+ });
8893
+ },
8894
+ onTimeout: (elapsedMs) => new ResourceTimeoutError(
8895
+ logicalId,
8896
+ resourceType,
8897
+ this.stackRegion,
8898
+ elapsedMs,
8899
+ operationKind,
8900
+ timeoutMs
8901
+ )
8902
+ }
8903
+ );
8904
+ } catch (error) {
8905
+ renderer.removeTask(logicalId);
8906
+ const message = error instanceof Error ? error.message : String(error);
8907
+ this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
8908
+ throw new ProvisioningError(
8909
+ `Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`,
8910
+ resourceType,
8911
+ logicalId,
8912
+ stateResources[logicalId]?.physicalId,
8913
+ error instanceof Error ? error : void 0
8914
+ );
8915
+ } finally {
8916
+ renderer.removeTask(logicalId);
8917
+ }
8918
+ }
8919
+ /**
8920
+ * Inner body of provisionResource, extracted so the outer wrapper can
8921
+ * apply the per-resource deadline (`withResourceDeadline`) without
8922
+ * having the timeout / warn timer code dwarf the real provisioning
8923
+ * logic. Behaviour is unchanged from the pre-deadline implementation.
8924
+ */
8925
+ async provisionResourceBody(logicalId, change, stateResources, stackName, template, parameterValues, conditions, counts, progress) {
8926
+ const resourceType = change.resourceType;
8927
+ const provider = this.providerRegistry.getProvider(resourceType);
8928
+ const renderer = getLiveRenderer();
8929
+ switch (change.changeType) {
8930
+ case "CREATE": {
8931
+ const desiredProps = change.desiredProperties || {};
8932
+ const context = {
8933
+ template,
8934
+ resources: stateResources,
8935
+ ...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
8936
+ ...conditions && Object.keys(conditions).length > 0 && { conditions },
8937
+ stateBackend: this.stateBackend,
8938
+ stackName
8939
+ };
8940
+ const resolvedProps = await this.resolver.resolve(desiredProps, context);
8941
+ const { provider: createProvider, properties: createProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
8942
+ const result = await this.withRetry(
8943
+ () => createProvider.create(logicalId, resourceType, createProps),
8944
+ logicalId
8945
+ );
8946
+ const dependencies = this.extractAllDependencies(template, logicalId);
8947
+ stateResources[logicalId] = {
8948
+ physicalId: result.physicalId,
8949
+ resourceType,
8950
+ properties: resolvedProps,
8951
+ ...result.attributes && { attributes: result.attributes },
8952
+ ...dependencies && dependencies.length > 0 && { dependencies }
8953
+ };
8954
+ if (counts)
8955
+ counts.created++;
8956
+ if (progress)
8957
+ progress.current++;
8958
+ const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8959
+ renderer.removeTask(logicalId);
8960
+ this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
8961
+ break;
8962
+ }
8963
+ case "UPDATE": {
8964
+ const currentResource = stateResources[logicalId];
8965
+ if (!currentResource) {
8966
+ throw new Error(`Cannot update ${logicalId}: resource not found in state`);
8967
+ }
8968
+ const desiredProps = change.desiredProperties || {};
8969
+ const currentProps = change.currentProperties || {};
8970
+ const context = {
8971
+ template,
8972
+ resources: stateResources,
8973
+ ...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
8974
+ ...conditions && Object.keys(conditions).length > 0 && { conditions },
8975
+ stateBackend: this.stateBackend,
8976
+ stackName
8977
+ };
8978
+ const resolvedProps = await this.resolver.resolve(desiredProps, context);
8979
+ if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
8980
+ this.logger.debug(
8981
+ `Skipping ${logicalId}: no actual changes after intrinsic function resolution`
8982
+ );
8983
+ if (counts)
8984
+ counts.skipped++;
8985
+ break;
8986
+ }
8987
+ const needsReplacement = change.propertyChanges?.some((pc) => pc.requiresReplacement);
8988
+ const dependencies = this.extractAllDependencies(template, logicalId);
8989
+ if (needsReplacement) {
8990
+ const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
8991
+ this.logger.info(
8992
+ `Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
8993
+ );
8994
+ this.logger.info(` Creating new ${logicalId}...`);
8995
+ const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
8996
+ const createResult = await this.withRetry(
8997
+ () => replaceProvider.create(logicalId, resourceType, replaceProps),
8751
8998
  logicalId
8752
8999
  );
8753
- const dependencies = this.extractAllDependencies(template, logicalId);
9000
+ const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
9001
+ if (updateReplacePolicy === "Retain") {
9002
+ this.logger.info(
9003
+ ` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`
9004
+ );
9005
+ } else {
9006
+ this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
9007
+ try {
9008
+ await provider.delete(
9009
+ logicalId,
9010
+ currentResource.physicalId,
9011
+ resourceType,
9012
+ currentResource.properties,
9013
+ { expectedRegion: this.stackRegion }
9014
+ );
9015
+ this.logger.info(` \u2713 Old resource deleted`);
9016
+ } catch (deleteError) {
9017
+ this.logger.warn(
9018
+ ` \u26A0 Failed to delete old resource ${logicalId} (${currentResource.physicalId}): ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
9019
+ );
9020
+ }
9021
+ }
8754
9022
  stateResources[logicalId] = {
8755
- physicalId: result.physicalId,
9023
+ physicalId: createResult.physicalId,
8756
9024
  resourceType,
8757
9025
  properties: resolvedProps,
8758
- ...result.attributes && { attributes: result.attributes },
9026
+ ...createResult.attributes && { attributes: createResult.attributes },
8759
9027
  ...dependencies && dependencies.length > 0 && { dependencies }
8760
9028
  };
8761
9029
  if (counts)
8762
- counts.created++;
9030
+ counts.updated++;
8763
9031
  if (progress)
8764
9032
  progress.current++;
8765
- const createPrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
9033
+ const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8766
9034
  renderer.removeTask(logicalId);
8767
- this.logger.info(`${createPrefix}\u2705 ${logicalId} (${resourceType}) created`);
8768
- break;
8769
- }
8770
- case "UPDATE": {
8771
- const currentResource = stateResources[logicalId];
8772
- if (!currentResource) {
8773
- throw new Error(`Cannot update ${logicalId}: resource not found in state`);
8774
- }
8775
- const desiredProps = change.desiredProperties || {};
8776
- const currentProps = change.currentProperties || {};
8777
- const context = {
8778
- template,
8779
- resources: stateResources,
8780
- ...parameterValues && Object.keys(parameterValues).length > 0 && { parameters: parameterValues },
8781
- ...conditions && Object.keys(conditions).length > 0 && { conditions },
8782
- stateBackend: this.stateBackend,
8783
- stackName
8784
- };
8785
- const resolvedProps = await this.resolver.resolve(desiredProps, context);
8786
- if (JSON.stringify(resolvedProps) === JSON.stringify(currentProps)) {
8787
- this.logger.debug(
8788
- `Skipping ${logicalId}: no actual changes after intrinsic function resolution`
8789
- );
8790
- if (counts)
8791
- counts.skipped++;
8792
- break;
8793
- }
8794
- const needsReplacement2 = change.propertyChanges?.some((pc) => pc.requiresReplacement);
8795
- const dependencies = this.extractAllDependencies(template, logicalId);
8796
- if (needsReplacement2) {
8797
- const replacedProps = change.propertyChanges?.filter((pc) => pc.requiresReplacement).map((pc) => pc.path).join(", ");
8798
- this.logger.info(
8799
- `Replacing ${logicalId} (${resourceType}) - immutable properties changed: ${replacedProps}`
8800
- );
8801
- this.logger.info(` Creating new ${logicalId}...`);
8802
- const { provider: replaceProvider, properties: replaceProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
8803
- const createResult = await this.withRetry(
8804
- () => replaceProvider.create(logicalId, resourceType, replaceProps),
9035
+ this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
9036
+ } else {
9037
+ this.logger.debug(`Updating ${logicalId} (${resourceType})`);
9038
+ const { provider: updateProvider, properties: updateProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
9039
+ let result;
9040
+ try {
9041
+ result = await this.withRetry(
9042
+ () => updateProvider.update(
9043
+ logicalId,
9044
+ currentResource.physicalId,
9045
+ resourceType,
9046
+ updateProps,
9047
+ currentProps
9048
+ ),
8805
9049
  logicalId
8806
9050
  );
8807
- const updateReplacePolicy = template?.Resources?.[logicalId]?.UpdateReplacePolicy;
8808
- if (updateReplacePolicy === "Retain") {
9051
+ } catch (updateError) {
9052
+ const msg = updateError instanceof Error ? updateError.message : String(updateError);
9053
+ if (msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE")) {
8809
9054
  this.logger.info(
8810
- ` Retaining old ${logicalId} (${currentResource.physicalId}) - UpdateReplacePolicy: Retain`
9055
+ `UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE \u2192 CREATE)`
8811
9056
  );
8812
- } else {
8813
- this.logger.info(` Deleting old ${logicalId} (${currentResource.physicalId})...`);
8814
9057
  try {
8815
9058
  await provider.delete(
8816
9059
  logicalId,
8817
9060
  currentResource.physicalId,
8818
9061
  resourceType,
8819
- currentResource.properties,
9062
+ currentProps,
8820
9063
  { expectedRegion: this.stackRegion }
8821
9064
  );
8822
- this.logger.info(` \u2713 Old resource deleted`);
8823
9065
  } catch (deleteError) {
8824
- this.logger.warn(
8825
- ` \u26A0 Failed to delete old resource ${logicalId} (${currentResource.physicalId}): ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
8826
- );
8827
- }
8828
- }
8829
- stateResources[logicalId] = {
8830
- physicalId: createResult.physicalId,
8831
- resourceType,
8832
- properties: resolvedProps,
8833
- ...createResult.attributes && { attributes: createResult.attributes },
8834
- ...dependencies && dependencies.length > 0 && { dependencies }
8835
- };
8836
- if (counts)
8837
- counts.updated++;
8838
- if (progress)
8839
- progress.current++;
8840
- const replacePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8841
- renderer.removeTask(logicalId);
8842
- this.logger.info(`${replacePrefix}\u2705 ${logicalId} (${resourceType}) replaced`);
8843
- } else {
8844
- this.logger.debug(`Updating ${logicalId} (${resourceType})`);
8845
- const { provider: updateProvider, properties: updateProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
8846
- let result;
8847
- try {
8848
- result = await this.withRetry(
8849
- () => updateProvider.update(
8850
- logicalId,
8851
- currentResource.physicalId,
8852
- resourceType,
8853
- updateProps,
8854
- currentProps
8855
- ),
8856
- logicalId
8857
- );
8858
- } catch (updateError) {
8859
- const msg = updateError instanceof Error ? updateError.message : String(updateError);
8860
- if (msg.includes("UnsupportedActionException") || msg.includes("does not support UPDATE")) {
8861
- this.logger.info(
8862
- `UPDATE not supported for ${logicalId} (${resourceType}), replacing (DELETE \u2192 CREATE)`
8863
- );
8864
- try {
8865
- await provider.delete(
8866
- logicalId,
8867
- currentResource.physicalId,
8868
- resourceType,
8869
- currentProps,
8870
- { expectedRegion: this.stackRegion }
9066
+ const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
9067
+ if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) {
9068
+ this.logger.debug(
9069
+ `Old resource ${logicalId} already gone, proceeding with CREATE`
8871
9070
  );
8872
- } catch (deleteError) {
8873
- const deleteMsg = deleteError instanceof Error ? deleteError.message : String(deleteError);
8874
- if (deleteMsg.includes("does not exist") || deleteMsg.includes("not found") || deleteMsg.includes("NotFound")) {
8875
- this.logger.debug(
8876
- `Old resource ${logicalId} already gone, proceeding with CREATE`
8877
- );
8878
- } else {
8879
- throw deleteError;
8880
- }
9071
+ } else {
9072
+ throw deleteError;
8881
9073
  }
8882
- const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(
8883
- provider,
8884
- resourceType,
8885
- resolvedProps,
8886
- logicalId
8887
- );
8888
- const createResult = await this.withRetry(
8889
- () => replProvider.create(logicalId, resourceType, replProps),
8890
- logicalId
8891
- );
8892
- result = {
8893
- physicalId: createResult.physicalId,
8894
- attributes: createResult.attributes,
8895
- wasReplaced: true
8896
- };
8897
- } else {
8898
- throw updateError;
8899
9074
  }
8900
- }
8901
- if (result.wasReplaced) {
8902
- this.logger.info(
8903
- `Resource ${logicalId} was replaced: ${currentResource.physicalId} -> ${result.physicalId}`
9075
+ const { provider: replProvider, properties: replProps } = this.selectProviderWithSafetyNet(provider, resourceType, resolvedProps, logicalId);
9076
+ const createResult = await this.withRetry(
9077
+ () => replProvider.create(logicalId, resourceType, replProps),
9078
+ logicalId
8904
9079
  );
9080
+ result = {
9081
+ physicalId: createResult.physicalId,
9082
+ attributes: createResult.attributes,
9083
+ wasReplaced: true
9084
+ };
9085
+ } else {
9086
+ throw updateError;
8905
9087
  }
8906
- stateResources[logicalId] = {
8907
- physicalId: result.physicalId,
8908
- resourceType,
8909
- properties: resolvedProps,
8910
- ...result.attributes && { attributes: result.attributes },
8911
- ...dependencies && dependencies.length > 0 && { dependencies }
8912
- };
8913
- if (counts)
8914
- counts.updated++;
8915
- if (progress)
8916
- progress.current++;
8917
- const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8918
- renderer.removeTask(logicalId);
8919
- this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
8920
- }
8921
- break;
8922
- }
8923
- case "DELETE": {
8924
- const currentResource = stateResources[logicalId];
8925
- if (!currentResource) {
8926
- throw new Error(`Cannot delete ${logicalId}: resource not found in state`);
8927
9088
  }
8928
- const deletionPolicy = template?.Resources?.[logicalId]?.DeletionPolicy;
8929
- if (deletionPolicy === "Retain") {
8930
- this.logger.info(`Retaining ${logicalId} (${resourceType}) - DeletionPolicy: Retain`);
8931
- delete stateResources[logicalId];
8932
- break;
8933
- }
8934
- this.logger.debug(`Deleting ${logicalId} (${resourceType})`);
8935
- try {
8936
- await this.withRetry(
8937
- () => provider.delete(
8938
- logicalId,
8939
- currentResource.physicalId,
8940
- resourceType,
8941
- currentResource.properties,
8942
- { expectedRegion: this.stackRegion }
8943
- ),
8944
- logicalId,
8945
- 3,
8946
- // fewer retries for DELETE
8947
- 5e3
9089
+ if (result.wasReplaced) {
9090
+ this.logger.info(
9091
+ `Resource ${logicalId} was replaced: ${currentResource.physicalId} -> ${result.physicalId}`
8948
9092
  );
8949
- } catch (deleteError) {
8950
- const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
8951
- if (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException")) {
8952
- this.logger.debug(
8953
- `Resource ${logicalId} already deleted (${msg}), removing from state`
8954
- );
8955
- } else {
8956
- throw deleteError;
8957
- }
8958
9093
  }
8959
- delete stateResources[logicalId];
9094
+ stateResources[logicalId] = {
9095
+ physicalId: result.physicalId,
9096
+ resourceType,
9097
+ properties: resolvedProps,
9098
+ ...result.attributes && { attributes: result.attributes },
9099
+ ...dependencies && dependencies.length > 0 && { dependencies }
9100
+ };
8960
9101
  if (counts)
8961
- counts.deleted++;
9102
+ counts.updated++;
8962
9103
  if (progress)
8963
9104
  progress.current++;
8964
- const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
9105
+ const updatePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
8965
9106
  renderer.removeTask(logicalId);
8966
- this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
9107
+ this.logger.info(`${updatePrefix}\u2705 ${logicalId} (${resourceType}) updated`);
9108
+ }
9109
+ break;
9110
+ }
9111
+ case "DELETE": {
9112
+ const currentResource = stateResources[logicalId];
9113
+ if (!currentResource) {
9114
+ throw new Error(`Cannot delete ${logicalId}: resource not found in state`);
9115
+ }
9116
+ const deletionPolicy = template?.Resources?.[logicalId]?.DeletionPolicy;
9117
+ if (deletionPolicy === "Retain") {
9118
+ this.logger.info(`Retaining ${logicalId} (${resourceType}) - DeletionPolicy: Retain`);
9119
+ delete stateResources[logicalId];
8967
9120
  break;
8968
9121
  }
9122
+ this.logger.debug(`Deleting ${logicalId} (${resourceType})`);
9123
+ try {
9124
+ await this.withRetry(
9125
+ () => provider.delete(
9126
+ logicalId,
9127
+ currentResource.physicalId,
9128
+ resourceType,
9129
+ currentResource.properties,
9130
+ { expectedRegion: this.stackRegion }
9131
+ ),
9132
+ logicalId,
9133
+ 3,
9134
+ // fewer retries for DELETE
9135
+ 5e3
9136
+ );
9137
+ } catch (deleteError) {
9138
+ const msg = deleteError instanceof Error ? deleteError.message : String(deleteError);
9139
+ if (msg.includes("does not exist") || msg.includes("was not found") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException") || msg.includes("ResourceNotFoundException")) {
9140
+ this.logger.debug(
9141
+ `Resource ${logicalId} already deleted (${msg}), removing from state`
9142
+ );
9143
+ } else {
9144
+ throw deleteError;
9145
+ }
9146
+ }
9147
+ delete stateResources[logicalId];
9148
+ if (counts)
9149
+ counts.deleted++;
9150
+ if (progress)
9151
+ progress.current++;
9152
+ const deletePrefix = progress ? `[${progress.current}/${progress.total}] ` : " ";
9153
+ renderer.removeTask(logicalId);
9154
+ this.logger.info(`${deletePrefix}\u2705 ${logicalId} (${resourceType}) deleted`);
9155
+ break;
8969
9156
  }
8970
- } catch (error) {
8971
- renderer.removeTask(logicalId);
8972
- const message = error instanceof Error ? error.message : String(error);
8973
- this.logger.error(`Failed to ${change.changeType.toLowerCase()} ${logicalId}: ${message}`);
8974
- throw new ProvisioningError(
8975
- `Failed to ${change.changeType.toLowerCase()} resource ${logicalId}`,
8976
- resourceType,
8977
- logicalId,
8978
- stateResources[logicalId]?.physicalId,
8979
- error instanceof Error ? error : void 0
8980
- );
8981
- } finally {
8982
- renderer.removeTask(logicalId);
8983
9157
  }
8984
9158
  }
8985
9159
  /**