@go-to-k/cdkd 0.284.24 → 0.284.26

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.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { $ as disableInstanceApiTermination, $n as NestedStackChildDirectDestroyError, $t as ensureAssetStorage, A as isStatefulRecreateTargetSync, An as uploadCfnTemplate, At as s3BucketWebsiteUrl, B as collectPublishedOutputNames, Bn as getAwsClients, Bt as rebuildClientForBucketRegion, C as createPreDeleteFinalSnapshot, Cn as resolveUseCdkBootstrapAssets, Ct as maskSecretsInText, D as makeCanonicalizePropertiesFn, Dn as CFN_TEMPLATE_URL_LIMIT, Dt as s3BucketDomainName, E as unsupportedFinalSnapshotError, En as CFN_TEMPLATE_BODY_LIMIT, Et as s3BucketArn, F as gray, Fn as AssemblyReader, Ft as withRetry, G as IAMRoleProvider, Gt as buildAssetRedirectMap, H as isExportAliasCollision, Hn as setAwsClients, Ht as AssetPublisher, I as green, In as processStackMessages, It as DagBuilder, J as ProviderRegistry, Jn as LocalInvokeBuildError$1, Jt as rewriteTemplateAssetReferences, K as collectInlinePolicyNamesManagedBySiblings, Kt as createAssetRedirectResolver, L as red, Lt as TemplateParser, M as formatResourceLine, Mn as PARTITION_TABLE, Mt as DiffCalculator, N as bold, Nn as canonicalizeRegion, Nt as INTRINSIC_KEYS, O as extractDeploymentEventError, On as MIGRATE_TMP_PREFIX, Ot as s3BucketDualStackDomainName, P as cyan, Pn as derivePartitionAndUrlSuffix, Pt as describeTypeWithThrottleRetry, Q as slowCcOperationTimeoutMs, Qn as MissingCdkCliError, Qt as BOOTSTRAP_MARKER_PREFIX, R as yellow, Rn as resolveBucketRegion, Rt as LockManager, S as ccRoutedFinalSnapshotError, Sn as resolveStateBucketWithDefaultAndSource, St as isSingleDynamicReferenceToken, T as refusesFinalSnapshot, Tn as warnDeprecatedNoPrefixCliFlag, Tt as scrubResourceRecord, U as secretBearingStateKeyWarning, Ut as stringifyValue, V as exportAliasCollisionScrubWarning, Vn as resetAwsClients, Vt as shouldRetainResource, W as stateKeySecretExposure, Wn as CdkdError, Wt as WorkGraph, X as findSilentDropProperties, Xn as LocalStartServiceError, Xt as stripControlChars, Y as findActionableSilentDrops, Yn as LocalMigrateError, Yt as escapeRegExp, Z as CloudControlProvider, Zt as AssetModeResolver, _ as IMPLICIT_DELETE_DEPENDENCIES, _n as resolveApp, _t as STATE_SOURCED_CROSS_GENERATION_RULES, a as DeploymentEventsStore, an as buildDenyExternalAccessPolicy, ar as StackTerminationProtectionError, at as WAFv2WebACLProvider, b as PRE_DELETE_SNAPSHOT_TYPES, bn as resolveSkipPrefix, bt as createSecretMasker, c as producerRegionsFromState, cn as getDockerCmd, ct as assertRegionMatch, d as updatePartialMessage, dn as AssetManifestLoader, dr as withErrorHandling, dt as configStringRefusal, en as getBootstrapMarkerKey, er as PartialFailureError, et as isTerminationProtectionPropagationError, f as updatePartialReason, fn as getDockerImageBySourceHash, fr as isMarkedNonRetryable, ft as readConfigString, g as maskingRetryLogger, gn as getLegacyStateBucketName, gt as requireConfigString, h as withResourceDeadline, hn as getDefaultStateBucketName, hr as markNonRetryable, ht as requireConfigObject, i as DeploymentEventsReader, in as validateContainerRepoName, ir as StackHasActiveImportsError, it as refStateLookupFromResource, j as renderStatefulReason, jn as expectedOwnerParam, jt as applyRoleArnIfSet, k as MULTI_REGION_RECREATE_BLOCKED_TYPES, kn as findLargeInlineResources, kt as s3BucketRegionalDomainName, l as replayFailedOperations, ln as runDockerForeground, lt as coerceCfnBoolean, m as deleteSkipReason, mn as synthesisStatusMessage, mr as isThrottlingError, mt as requireConfigArray, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as readBootstrapMarkerBody, nr as ResourceTimeoutError, nt as cfnRefValueFromPhysicalId, o as planFailedOps, on as buildDockerImage, ot as normalizeAwsTagsToCfn, p as UNSPECIFIED_SKIP_REASON, pn as Synthesizer, pr as isRetryableTransientError, pt as replayWarn, q as clearOnUpdateRemoval, qn as DeployCancelledError, qt as loadPublishableAssetManifest, r as DeployEngine, rn as validateAssetBucketName, rr as ResourceUpdateNotSupportedError, rt as getAccountInfo, s as planRollback, sn as formatDockerLoginError, st as resolveExplicitPhysicalId, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as parseBootstrapMarker, tr as ProvisioningError, tt as IntrinsicFunctionResolver, u as replayRollback, un as runDockerStreaming, ur as normalizeAwsError, ut as configBooleanRefusal, v as computeImplicitDeleteEdges, vn as resolveAutoAssetStorage, vt as STATE_SOURCED_READBACK_RULES, w as isFinalSnapshotError, wn as stateBucketExistenceConfirmed, wt as redactSecretsForState, x as buildFinalSnapshotIdentifier, xn as resolveStateBucketWithDefault, xt as dynamicReferenceTokens, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as resolveCaptureObservedState, yt as TEMPLATE_SOURCED_RULES, z as collectDeclaredOutputNames, zn as AwsClients, zt as S3StateBackend } from "./deploy-engine-BJQbni1s.js";
2
+ import { $ as isInterruptedWaitError, $n as LocalMigrateError, $t as escapeRegExp, A as isStatefulRecreateTargetSync, An as CFN_TEMPLATE_BODY_LIMIT, At as s3BucketArn, B as collectPublishedOutputNames, Bn as processStackMessages, Bt as DagBuilder, C as createPreDeleteFinalSnapshot, Cn as resolveCaptureObservedState, Ct as TEMPLATE_SOURCED_RULES, D as makeCanonicalizePropertiesFn, Dn as resolveUseCdkBootstrapAssets, Dt as maskSecretsInText, E as unsupportedFinalSnapshotError, En as resolveStateBucketWithDefaultAndSource, Et as isSingleDynamicReferenceToken, F as gray, Fn as expectedOwnerParam, Ft as applyRoleArnIfSet, G as IAMRoleProvider, Gn as resetAwsClients, Gt as shouldRetainResource, H as isExportAliasCollision, Hn as resolveBucketRegion, Ht as LockManager, I as green, In as PARTITION_TABLE, It as DiffCalculator, J as ProviderRegistry, Jn as CdkdError, Jt as WorkGraph, K as collectInlinePolicyNamesManagedBySiblings, Kn as setAwsClients, Kt as AssetPublisher, L as red, Ln as canonicalizeRegion, Lt as INTRINSIC_KEYS, M as formatResourceLine, Mn as MIGRATE_TMP_PREFIX, Mt as s3BucketDualStackDomainName, N as bold, Nn as findLargeInlineResources, Nt as s3BucketRegionalDomainName, O as extractDeploymentEventError, On as stateBucketExistenceConfirmed, Ot as redactSecretsForState, P as cyan, Pn as uploadCfnTemplate, Pt as s3BucketWebsiteUrl, Q as endCommandInterruptScope, Qn as LocalInvokeBuildError$1, Qt as rewriteTemplateAssetReferences, R as yellow, Rn as derivePartitionAndUrlSuffix, Rt as describeTypeWithThrottleRetry, S as ccRoutedFinalSnapshotError, Sn as resolveAutoAssetStorage, St as STATE_SOURCED_READBACK_RULES, T as refusesFinalSnapshot, Tn as resolveStateBucketWithDefault, Tt as dynamicReferenceTokens, U as secretBearingStateKeyWarning, Un as AwsClients, Ut as S3StateBackend, V as exportAliasCollisionScrubWarning, Vt as TemplateParser, W as stateKeySecretExposure, Wn as getAwsClients, Wt as rebuildClientForBucketRegion, X as findSilentDropProperties, Xt as createAssetRedirectResolver, Y as findActionableSilentDrops, Yt as buildAssetRedirectMap, Z as beginCommandInterruptScope, Zn as DeployCancelledError, Zt as loadPublishableAssetManifest, _ as IMPLICIT_DELETE_DEPENDENCIES, _n as Synthesizer, _r as isRetryableTransientError, _t as replayWarn, a as DeploymentEventsStore, an as parseBootstrapMarker, ar as ProvisioningError, at as IntrinsicFunctionResolver, b as PRE_DELETE_SNAPSHOT_TYPES, bn as getLegacyStateBucketName, bt as requireConfigString, c as producerRegionsFromState, cn as validateContainerRepoName, cr as StackHasActiveImportsError, ct as refStateLookupFromResource, d as updatePartialMessage, dn as formatDockerLoginError, dt as resolveExplicitPhysicalId, en as stripControlChars, er as LocalStartServiceError, et as startInterruptWatch, f as updatePartialReason, fn as getDockerCmd, ft as assertRegionMatch, g as maskingRetryLogger, gn as getDockerImageBySourceHash, gr as isMarkedNonRetryable, gt as readConfigString, h as withResourceDeadline, hn as AssetManifestLoader, hr as withErrorHandling, ht as configStringRefusal, i as DeploymentEventsReader, in as getBootstrapMarkerKey, ir as PartialFailureError, it as isTerminationProtectionPropagationError, j as renderStatefulReason, jn as CFN_TEMPLATE_URL_LIMIT, jt as s3BucketDomainName, k as MULTI_REGION_RECREATE_BLOCKED_TYPES, kn as warnDeprecatedNoPrefixCliFlag, kt as scrubResourceRecord, l as replayFailedOperations, ln as buildDenyExternalAccessPolicy, lr as StackTerminationProtectionError, lt as WAFv2WebACLProvider, m as deleteSkipReason, mn as runDockerStreaming, mr as normalizeAwsError, mt as configBooleanRefusal, n as DEFAULT_RESOURCE_WARN_AFTER_MS, nn as BOOTSTRAP_MARKER_PREFIX, nr as MissingCdkCliError, nt as slowCcOperationTimeoutMs, o as planFailedOps, on as readBootstrapMarkerBody, or as ResourceTimeoutError, ot as cfnRefValueFromPhysicalId, p as UNSPECIFIED_SKIP_REASON, pn as runDockerForeground, pt as coerceCfnBoolean, q as clearOnUpdateRemoval, qt as stringifyValue, r as DeployEngine, rn as ensureAssetStorage, rr as NestedStackChildDirectDestroyError, rt as disableInstanceApiTermination, s as planRollback, sn as validateAssetBucketName, sr as ResourceUpdateNotSupportedError, st as getAccountInfo, t as DEFAULT_RESOURCE_TIMEOUT_MS, tn as AssetModeResolver, tt as CloudControlProvider, u as replayRollback, un as buildDockerImage, ut as normalizeAwsTagsToCfn, v as computeImplicitDeleteEdges, vn as synthesisStatusMessage, vr as isThrottlingError, vt as requireConfigArray, w as isFinalSnapshotError, wn as resolveSkipPrefix, wt as createSecretMasker, x as buildFinalSnapshotIdentifier, xn as resolveApp, xt as STATE_SOURCED_CROSS_GENERATION_RULES, y as ATOMIC_FINAL_SNAPSHOT_TYPES, yn as getDefaultStateBucketName, yr as markNonRetryable, yt as requireConfigObject, z as collectDeclaredOutputNames, zn as AssemblyReader, zt as withRetry } from "./deploy-engine-DNCs229s.js";
3
3
  import { a as getLiveRenderer, c as PATTERN_B_RESOURCE_TYPES, d as generateResourceNameWithFallback, f as getCurrentStackName, h as withStackName, i as runStackBuffered, m as withSkipPrefix, n as getLogger, o as PATTERN_B_NAME_OPTIONS, s as PATTERN_B_NAME_PROPERTIES, u as generateResourceName } from "./logger-zRrlbaQt.js";
4
- import { t as ASGProvider } from "./asg-provider-Bh0odlSk.js";
4
+ import { t as ASGProvider } from "./asg-provider-CS5j-5z6.js";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
7
  import { CopyObjectCommand, CreateBucketCommand, DeleteBucketAnalyticsConfigurationCommand, DeleteBucketCommand, DeleteBucketCorsCommand, DeleteBucketEncryptionCommand, DeleteBucketIntelligentTieringConfigurationCommand, DeleteBucketInventoryConfigurationCommand, DeleteBucketLifecycleCommand, DeleteBucketMetricsConfigurationCommand, DeleteBucketOwnershipControlsCommand, DeleteBucketPolicyCommand, DeleteBucketReplicationCommand, DeleteBucketTaggingCommand, DeleteBucketWebsiteCommand, DeleteObjectsCommand, GetBucketAccelerateConfigurationCommand, GetBucketCorsCommand, GetBucketEncryptionCommand, GetBucketLifecycleConfigurationCommand, GetBucketLocationCommand, GetBucketLoggingCommand, GetBucketNotificationConfigurationCommand, GetBucketOwnershipControlsCommand, GetBucketPolicyCommand, GetBucketReplicationCommand, GetBucketTaggingCommand, GetBucketVersioningCommand, GetBucketWebsiteCommand, GetObjectCommand, GetObjectLockConfigurationCommand, GetPublicAccessBlockCommand, HeadBucketCommand, ListBucketAnalyticsConfigurationsCommand, ListBucketIntelligentTieringConfigurationsCommand, ListBucketInventoryConfigurationsCommand, ListBucketMetricsConfigurationsCommand, ListObjectVersionsCommand, ListObjectsV2Command, NoSuchBucket, PutBucketAccelerateConfigurationCommand, PutBucketAnalyticsConfigurationCommand, PutBucketCorsCommand, PutBucketEncryptionCommand, PutBucketIntelligentTieringConfigurationCommand, PutBucketInventoryConfigurationCommand, PutBucketLifecycleConfigurationCommand, PutBucketLoggingCommand, PutBucketMetricsConfigurationCommand, PutBucketNotificationConfigurationCommand, PutBucketOwnershipControlsCommand, PutBucketPolicyCommand, PutBucketReplicationCommand, PutBucketTaggingCommand, PutBucketVersioningCommand, PutBucketWebsiteCommand, PutObjectCommand, PutObjectLockConfigurationCommand, PutPublicAccessBlockCommand, S3Client, S3ServiceException } from "@aws-sdk/client-s3";
@@ -865,6 +865,165 @@ function namedCliRegion(optionRegion) {
865
865
  function rawCliRegion(optionRegion) {
866
866
  return optionRegion || process.env["AWS_REGION"] || void 0;
867
867
  }
868
+ /**
869
+ * The region a command OPERATES on.
870
+ *
871
+ * Order: `--region` -> `AWS_REGION` -> `AWS_DEFAULT_REGION` -> the region the
872
+ * AWS SDK's own chain resolves (i.e. the selected profile's `region =` line)
873
+ * -> `us-east-1`.
874
+ *
875
+ * The profile step is what issue
876
+ * [#2029](https://github.com/go-to-k/cdkd/issues/2029) adds, and it removes an
877
+ * incoherence rather than adding a preference. cdkd ALREADY consulted the
878
+ * profile: every command builds its pre-flight bag as
879
+ * `new AwsClients({ ...(options.region && { region }) })`, so with no flag the
880
+ * bag is region-less and the SDK resolves the profile. Only the region VALUE -
881
+ * the one that keys the state file, keys the bootstrap marker and answers
882
+ * `AWS::Region` in every user template - fell back to the literal. Measured on
883
+ * an env-agnostic stack with `region = ap-northeast-1` in the profile and no
884
+ * flag or env var:
885
+ *
886
+ * ```text
887
+ * State bucket '...' is in 'us-east-1' (client was 'ap-northeast-1'); rebuilding S3 client.
888
+ * Getting state for stack: CdkdBasicExample (us-east-1)
889
+ * Resolved Ref to pseudo parameter: AWS::Region -> us-east-1
890
+ * ```
891
+ *
892
+ * One command, two regions: the pre-flight talked to the profile's region
893
+ * while the resources, their ARNs and their state landed in us-east-1. Only an
894
+ * ENV-AGNOSTIC stack is affected - a stack with `env: { region }` pinned in CDK
895
+ * carries its own region through the Cloud Assembly and never reaches this
896
+ * fallback (`assembly-reader.ts` maps CDK's `unknown-region` to `undefined`).
897
+ *
898
+ * `AWS_DEFAULT_REGION` sits between `AWS_REGION` and the profile because the
899
+ * AWS CLI honours it and the JS SDK does NOT (measured: with only that variable
900
+ * set, `STSClient({}).config.region()` returns the PROFILE's region, not the
901
+ * variable's). Reading it here is what stops cdkd disagreeing with the CLI a
902
+ * user just ran.
903
+ *
904
+ * Returning the SOURCE, not just the region, is what lets a caller tell "the
905
+ * user asked for this region" from "we inferred it" - which is the difference
906
+ * that decides whether the reconciliation in
907
+ * {@link reconcileRegionWithLegacyDefault} may move the answer back.
908
+ */
909
+ async function resolveEffectiveRegion(options) {
910
+ const flag = canonicalizeRegion(options.region);
911
+ if (flag) return {
912
+ region: flag,
913
+ source: "flag"
914
+ };
915
+ const awsRegion = canonicalizeRegion(process.env["AWS_REGION"]);
916
+ if (awsRegion) return {
917
+ region: awsRegion,
918
+ source: "env"
919
+ };
920
+ const defaultRegionEnv = canonicalizeRegion(process.env["AWS_DEFAULT_REGION"]);
921
+ if (defaultRegionEnv) return {
922
+ region: defaultRegionEnv,
923
+ source: "default-env"
924
+ };
925
+ const fromProfile = canonicalizeRegion(await resolveSdkDefaultRegion(options.profile));
926
+ if (fromProfile) return {
927
+ region: fromProfile,
928
+ source: "profile"
929
+ };
930
+ return {
931
+ region: LEGACY_DEFAULT_REGION,
932
+ source: "default"
933
+ };
934
+ }
935
+ /**
936
+ * The region cdkd used to fall back to before issue #2029, and still falls
937
+ * back to when nothing at all answers.
938
+ *
939
+ * Named rather than inlined because the reconciliation below has to ask
940
+ * "is there state under the OLD default?", and that question only means
941
+ * something while the two are the same constant.
942
+ */
943
+ const LEGACY_DEFAULT_REGION = "us-east-1";
944
+ /**
945
+ * Ask the AWS SDK's own chain which region it would use.
946
+ *
947
+ * A throwaway client, sampled once and destroyed: `src/utils/aws-clients.ts`
948
+ * is explicit that a region-less bag's lazy members resolve independently and
949
+ * need not agree with each other, so nothing here is reused for real work.
950
+ * Returns `undefined` rather than throwing when the chain answers nothing -
951
+ * a client with no resolvable region REJECTS, and that rejection is the normal
952
+ * "no region configured" signal rather than an error worth surfacing.
953
+ */
954
+ async function resolveSdkDefaultRegion(profile) {
955
+ const { STSClient } = await import("@aws-sdk/client-sts");
956
+ let client;
957
+ try {
958
+ client = new STSClient({ ...profile && { profile } });
959
+ return await client.config.region() || void 0;
960
+ } catch {
961
+ return;
962
+ } finally {
963
+ client?.destroy?.();
964
+ }
965
+ }
966
+ /**
967
+ * The marker-key twin of {@link reconcileRegionWithLegacyDefault}.
968
+ *
969
+ * `cdkd bootstrap` WRITES `cdkd-bootstrap/{region}.json`; `cdkd gc` and
970
+ * `cdkd bootstrap --destroy` READ it. Issue
971
+ * [#2029](https://github.com/go-to-k/cdkd/issues/2029) moves what `{region}`
972
+ * resolves to when the user names none, and a previous attempt at it moved the
973
+ * READ side alone — which silently broke that pairing: for a user with a
974
+ * non-us-east-1 profile, `cdkd gc` reported "not opted in" and the teardown
975
+ * "nothing to delete" while the asset bucket and ECR repo stayed alive and
976
+ * billing. So all three commands resolve through THIS function, and the pairing
977
+ * holds by construction rather than by three call sites agreeing.
978
+ *
979
+ * The reconciliation itself is the same rule as the state-key one: an inferred
980
+ * region yields to an existing marker under the old default, a named region
981
+ * never does, and a first bootstrap goes to the profile's region because there
982
+ * is nothing to strand.
983
+ *
984
+ * Note this is deliberately NOT `readBootstrapMarkerBody`: that helper answers
985
+ * "read the marker for THIS region, trying both spellings", which is a
986
+ * different question and would conflate a case difference with a region
987
+ * difference. Here both probes use the canonical key, because the question is
988
+ * only which REGION owns a marker.
989
+ */
990
+ async function reconcileMarkerRegionWithLegacyDefault(input) {
991
+ const { effective, probe, markerKeyFor, logger } = input;
992
+ if (!regionNeedsReconciliation(effective)) return effective.region;
993
+ const markerExists = async (region) => {
994
+ try {
995
+ return await probe.getRawObject(markerKeyFor(region)) !== null;
996
+ } catch (error) {
997
+ if (error.name !== "NoSuchBucket") throw error;
998
+ logger.debug(`[region] no state bucket yet, so no bootstrap marker for ${region} (first bootstrap).`);
999
+ return false;
1000
+ }
1001
+ };
1002
+ if (await markerExists(effective.region)) {
1003
+ logger.debug(`[region] asset storage exists in ${effective.region} (from your AWS profile); using it.`);
1004
+ return effective.region;
1005
+ }
1006
+ if (!await markerExists("us-east-1")) return effective.region;
1007
+ logger.info(`cdkd asset storage exists in ${LEGACY_DEFAULT_REGION}, but your AWS profile resolves ${effective.region}. Continuing to use ${LEGACY_DEFAULT_REGION} so the existing storage is not orphaned. Pass '--region ${effective.region}' to target your profile's region, or '--region ${LEGACY_DEFAULT_REGION}' to silence this message.`);
1008
+ return LEGACY_DEFAULT_REGION;
1009
+ }
1010
+ /**
1011
+ * Whether {@link resolveEffectiveRegion}'s answer is subject to the
1012
+ * reconciliation — i.e. whether cdkd INFERRED it rather than being told.
1013
+ *
1014
+ * `flag` and `env` (`AWS_REGION`) are the user speaking, and are obeyed as
1015
+ * given. `default-env` (`AWS_DEFAULT_REGION`) is NOT: cdkd never read that
1016
+ * variable before, so acting on it is a new inference. `default` is already
1017
+ * the legacy region, so there is nothing to reconcile against.
1018
+ *
1019
+ * Exported so a caller can skip CONSTRUCTING a probe client it will not use —
1020
+ * the reconciliation functions return early on the same condition, but by then
1021
+ * the caller has already paid for an S3 client and a state backend.
1022
+ */
1023
+ function regionNeedsReconciliation(effective) {
1024
+ if (effective.source === "flag" || effective.source === "env") return false;
1025
+ return effective.region !== LEGACY_DEFAULT_REGION;
1026
+ }
868
1027
 
869
1028
  //#endregion
870
1029
  //#region src/cli/commands/state-file-keys.ts
@@ -1159,15 +1318,43 @@ async function bootstrapDestroyCommand(options) {
1159
1318
  roleArn: options.roleArn,
1160
1319
  region: namedCliRegion(options.region)
1161
1320
  });
1162
- const region = namedCliRegion(options.region) ?? "us-east-1";
1321
+ const effective = await resolveEffectiveRegion(options);
1322
+ const accountClients = new AwsClients({
1323
+ region: effective.region,
1324
+ ...options.profile && { profile: options.profile }
1325
+ });
1326
+ const accountId = (await accountClients.sts.send(new GetCallerIdentityCommand({}))).Account;
1327
+ const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
1328
+ let region = effective.region;
1329
+ if (regionNeedsReconciliation(effective)) {
1330
+ const probeBackend = new S3StateBackend(new S3Client({
1331
+ region: effective.region,
1332
+ ...options.profile && { profile: options.profile }
1333
+ }), {
1334
+ bucket: bucketName,
1335
+ prefix: STATE_PREFIX
1336
+ }, {
1337
+ region: effective.region,
1338
+ ...options.profile && { profile: options.profile }
1339
+ });
1340
+ try {
1341
+ region = await reconcileMarkerRegionWithLegacyDefault({
1342
+ effective,
1343
+ probe: probeBackend,
1344
+ markerKeyFor: getBootstrapMarkerKey,
1345
+ logger
1346
+ });
1347
+ } finally {
1348
+ probeBackend.destroyClient();
1349
+ }
1350
+ }
1163
1351
  const rawRegion = rawCliRegion(options.region) ?? region;
1164
- const awsClients = new AwsClients({
1352
+ const awsClients = region === effective.region ? accountClients : new AwsClients({
1165
1353
  region,
1166
1354
  ...options.profile && { profile: options.profile }
1167
1355
  });
1356
+ if (awsClients !== accountClients) accountClients.destroy();
1168
1357
  setAwsClients(awsClients);
1169
- const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
1170
- const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
1171
1358
  const markerS3Client = new S3Client({
1172
1359
  region,
1173
1360
  ...options.profile && { profile: options.profile }
@@ -1291,19 +1478,19 @@ async function bootstrapCommand(options) {
1291
1478
  if (options.verbose) logger.setLevel("debug");
1292
1479
  logger.info("Starting cdkd bootstrap...");
1293
1480
  logger.debug("Options:", options);
1294
- const rawRegion = rawCliRegion(options.region) ?? "us-east-1";
1481
+ const rawRegion = rawCliRegion(options.region);
1295
1482
  foldRegionOption(options);
1296
1483
  await applyRoleArnIfSet({
1297
1484
  roleArn: options.roleArn,
1298
1485
  region: options.region
1299
1486
  });
1300
- const awsClients = new AwsClients({
1301
- ...options.region && { region: options.region },
1487
+ const effective = await resolveEffectiveRegion(options);
1488
+ let awsClients = new AwsClients({
1489
+ region: effective.region,
1302
1490
  ...options.profile && { profile: options.profile }
1303
1491
  });
1304
1492
  setAwsClients(awsClients);
1305
- const s3Client = awsClients.s3;
1306
- const region = namedCliRegion(options.region) ?? "us-east-1";
1493
+ let s3Client = awsClients.s3;
1307
1494
  let bucketName;
1308
1495
  let accountId;
1309
1496
  if (options.stateBucket) {
@@ -1315,6 +1502,39 @@ async function bootstrapCommand(options) {
1315
1502
  bucketName = getDefaultStateBucketName(accountId);
1316
1503
  logger.info(`Using default state bucket: ${bucketName}`);
1317
1504
  }
1505
+ let region = effective.region;
1506
+ if (regionNeedsReconciliation(effective)) {
1507
+ const probeBackend = new S3StateBackend(new S3Client({
1508
+ region: effective.region,
1509
+ ...options.profile && { profile: options.profile }
1510
+ }), {
1511
+ bucket: bucketName,
1512
+ prefix: "cdkd"
1513
+ }, {
1514
+ region: effective.region,
1515
+ ...options.profile && { profile: options.profile }
1516
+ });
1517
+ try {
1518
+ region = await reconcileMarkerRegionWithLegacyDefault({
1519
+ effective,
1520
+ probe: probeBackend,
1521
+ markerKeyFor: getBootstrapMarkerKey,
1522
+ logger
1523
+ });
1524
+ } finally {
1525
+ probeBackend.destroyClient();
1526
+ }
1527
+ }
1528
+ if (region !== effective.region) {
1529
+ const held = new AwsClients({
1530
+ region,
1531
+ ...options.profile && { profile: options.profile }
1532
+ });
1533
+ awsClients.destroy();
1534
+ awsClients = held;
1535
+ setAwsClients(awsClients);
1536
+ s3Client = awsClients.s3;
1537
+ }
1318
1538
  const rebuiltStateBucketClient = await rebuildClientForBucketRegion(s3Client, bucketName, { ...options.profile && { profile: options.profile } });
1319
1539
  const stateBucketS3 = rebuiltStateBucketClient ?? s3Client;
1320
1540
  try {
@@ -1401,7 +1621,7 @@ async function bootstrapCommand(options) {
1401
1621
  ecrClient,
1402
1622
  stateBackend,
1403
1623
  accountId,
1404
- region: rawRegion,
1624
+ region: rawRegion ?? region,
1405
1625
  force: options.force,
1406
1626
  ...options.assetBucket && { assetBucketName: options.assetBucket },
1407
1627
  ...options.containerRepo && { containerRepoName: options.containerRepo }
@@ -1774,6 +1994,7 @@ function createListCommand() {
1774
1994
  * handler of its own).
1775
1995
  */
1776
1996
  function forwardSigtermToSigint() {
1997
+ beginCommandInterruptScope();
1777
1998
  const handler = () => {
1778
1999
  if (process.listenerCount("SIGINT") === 0) process.exit(143);
1779
2000
  process.emit("SIGINT", "SIGINT");
@@ -1781,6 +2002,7 @@ function forwardSigtermToSigint() {
1781
2002
  process.on("SIGTERM", handler);
1782
2003
  return () => {
1783
2004
  process.removeListener("SIGTERM", handler);
2005
+ endCommandInterruptScope();
1784
2006
  };
1785
2007
  }
1786
2008
 
@@ -14839,28 +15061,37 @@ const DELETE_INDEX_WAIT_PROCEED_NOTE = `the DeleteTable goes ahead anyway and AW
14839
15061
  async function waitForIndexesSettled(opts) {
14840
15062
  const { tableName, logicalId, logger, describeTable, maxAttempts, proceedNote } = opts;
14841
15063
  const budgetWait = opts.budgetWait;
15064
+ const watch = startInterruptWatch(`index settle wait on ${tableName}`);
14842
15065
  let attempt = 0;
14843
- while (budgetWait ? budgetWait.nextPoll() : attempt < maxAttempts) {
14844
- attempt += 1;
14845
- try {
14846
- if (!hasTransitionalIndex((await describeTable()).Table?.GlobalSecondaryIndexes ?? [])) return;
14847
- } catch (err) {
14848
- const message = err instanceof Error ? err.message : String(err);
14849
- if (isThrottlingError(err)) {
14850
- logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
14851
- await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
14852
- continue;
15066
+ try {
15067
+ while (budgetWait ? budgetWait.nextPoll() : attempt < maxAttempts) {
15068
+ attempt += 1;
15069
+ if (watch.isInterrupted()) {
15070
+ logger.debug(`Interrupted while waiting for indexes on ${tableName} (${logicalId}) after ${attempt - 1} DescribeTable polls; ${proceedNote}`);
15071
+ return;
14853
15072
  }
14854
- if (err instanceof ResourceNotFoundException$1) {
14855
- logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
15073
+ try {
15074
+ if (!hasTransitionalIndex((await describeTable()).Table?.GlobalSecondaryIndexes ?? [])) return;
15075
+ } catch (err) {
15076
+ const message = err instanceof Error ? err.message : String(err);
15077
+ if (isThrottlingError(err)) {
15078
+ logger.debug(`DescribeTable throttled while waiting for indexes on ${tableName} (attempt ${attempt}/${maxAttempts}); still waiting: ${message}`);
15079
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
15080
+ continue;
15081
+ }
15082
+ if (err instanceof ResourceNotFoundException$1) {
15083
+ logger.debug(`Table ${tableName} (${logicalId}) no longer exists while waiting for indexes`);
15084
+ return;
15085
+ }
15086
+ const errorName = err instanceof Error ? err.name : typeof err;
15087
+ logger.debug(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}): ${message}`);
15088
+ logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
14856
15089
  return;
14857
15090
  }
14858
- const errorName = err instanceof Error ? err.name : typeof err;
14859
- logger.debug(`DescribeTable failed while waiting for indexes on ${tableName} (${logicalId}): ${message}`);
14860
- logger.warn(`DescribeTable failed (${errorName}) while waiting for indexes on ${tableName} (${logicalId}), so cdkd stopped waiting; ${proceedNote} Re-run with --verbose for AWS's own message.`);
14861
- return;
15091
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
14862
15092
  }
14863
- await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
15093
+ } finally {
15094
+ watch.dispose();
14864
15095
  }
14865
15096
  const pollSeconds = INDEX_SETTLE_POLL_INTERVAL_MS / 1e3;
14866
15097
  const pollsRun = budgetWait ? budgetWait.pollsRun : maxAttempts;
@@ -14965,24 +15196,31 @@ function indexBusyRetryWarning(opts) {
14965
15196
  */
14966
15197
  async function deleteTableWithIndexBusyRetry(opts) {
14967
15198
  let deleteAttempts = 0;
14968
- await withRetry(async () => {
14969
- if (deleteAttempts++ > 0) {
14970
- if (deleteAttempts === 2) opts.logger.warn(indexBusyRetryWarning({
14971
- typeLabel: opts.typeLabel,
14972
- logicalId: opts.logicalId,
14973
- physicalId: opts.physicalId,
14974
- attemptNumber: deleteAttempts,
14975
- maxRetries: opts.maxRetries
14976
- }));
14977
- await opts.reArm();
14978
- }
14979
- await opts.deleteTable();
14980
- }, opts.logicalId, {
14981
- maxRetries: opts.maxRetries,
14982
- isRetryable: (message) => isIndexBusyDeleteError(message) && (opts.shouldKeepRetrying?.() ?? true),
14983
- logger: opts.logger,
14984
- sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
14985
- });
15199
+ const watch = startInterruptWatch(`DynamoDB ${opts.typeLabel} ${opts.logicalId} delete`);
15200
+ try {
15201
+ await withRetry(async () => {
15202
+ if (deleteAttempts++ > 0) {
15203
+ if (deleteAttempts === 2) opts.logger.warn(indexBusyRetryWarning({
15204
+ typeLabel: opts.typeLabel,
15205
+ logicalId: opts.logicalId,
15206
+ physicalId: opts.physicalId,
15207
+ attemptNumber: deleteAttempts,
15208
+ maxRetries: opts.maxRetries
15209
+ }));
15210
+ await opts.reArm();
15211
+ }
15212
+ await opts.deleteTable();
15213
+ }, opts.logicalId, {
15214
+ maxRetries: opts.maxRetries,
15215
+ isRetryable: (message) => isIndexBusyDeleteError(message) && (opts.shouldKeepRetrying?.() ?? true),
15216
+ logger: opts.logger,
15217
+ isInterrupted: watch.isInterrupted,
15218
+ onInterrupted: watch.onInterrupted,
15219
+ sleep: (ms) => opts.sleepSeam.sleep ? opts.sleepSeam.sleep(ms) : new Promise((resolve) => setTimeout(resolve, ms))
15220
+ });
15221
+ } finally {
15222
+ watch.dispose();
15223
+ }
14986
15224
  }
14987
15225
 
14988
15226
  //#endregion
@@ -18658,94 +18896,111 @@ var DynamoDBGlobalTableProvider = class {
18658
18896
  const oldEnabled = oldSettings !== void 0 && oldSettings !== null;
18659
18897
  const newEnabled = newSettings !== void 0 && newSettings !== null;
18660
18898
  if (!oldEnabled && !newEnabled) return;
18661
- if (newEnabled) {
18662
- const minCapacity = Number(newSettings["MinCapacity"] ?? 0);
18663
- const maxCapacity = Number(newSettings["MaxCapacity"] ?? 0);
18664
- if (!Number.isFinite(minCapacity) || !Number.isFinite(maxCapacity)) {
18665
- warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
18899
+ const watch = startInterruptWatch(`DynamoDB auto-scaling for ${tableName}`);
18900
+ try {
18901
+ if (newEnabled) {
18902
+ const minCapacity = Number(newSettings["MinCapacity"] ?? 0);
18903
+ const maxCapacity = Number(newSettings["MaxCapacity"] ?? 0);
18904
+ if (!Number.isFinite(minCapacity) || !Number.isFinite(maxCapacity)) {
18905
+ warn(`Cannot apply auto-scaling diff on ${tableName} (${dimension}): MinCapacity / MaxCapacity must be numbers, got ${maskSecrets(String(newSettings["MinCapacity"]))} / ${maskSecrets(String(newSettings["MaxCapacity"]))}`);
18906
+ return;
18907
+ }
18908
+ try {
18909
+ await withRetry(() => asClient.send(new RegisterScalableTargetCommand({
18910
+ ServiceNamespace: "dynamodb",
18911
+ ResourceId: resourceId,
18912
+ ScalableDimension: dimension,
18913
+ MinCapacity: minCapacity,
18914
+ MaxCapacity: maxCapacity
18915
+ })), `${resourceId} (${dimension})`, {
18916
+ isRetryable: (_message, error) => isThrottlingError(error),
18917
+ maxRetries: asMaxRetries,
18918
+ isInterrupted: watch.isInterrupted,
18919
+ onInterrupted: watch.onInterrupted,
18920
+ ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18921
+ });
18922
+ } catch (err) {
18923
+ if (isInterruptedWaitError(err)) throw err;
18924
+ warn(`Could not register auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling register-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --min-capacity ${minCapacity} --max-capacity ${maxCapacity}`);
18925
+ return;
18926
+ }
18927
+ const tttCfg = newSettings["TargetTrackingScalingPolicyConfiguration"] ?? {};
18928
+ const targetValue = Number(tttCfg["TargetValue"]);
18929
+ if (!Number.isFinite(targetValue)) {
18930
+ warn(`Auto-scaling target registered on ${tableName} (${dimension}) but TargetValue is missing or non-numeric — skipping PutScalingPolicy. Provide TargetTrackingScalingPolicyConfiguration.TargetValue in the template.`);
18931
+ return;
18932
+ }
18933
+ const targetTrackingConfig = {
18934
+ PredefinedMetricSpecification: { PredefinedMetricType: metricType },
18935
+ TargetValue: targetValue
18936
+ };
18937
+ if (tttCfg["ScaleInCooldown"] !== void 0) targetTrackingConfig["ScaleInCooldown"] = Number(tttCfg["ScaleInCooldown"]);
18938
+ if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
18939
+ if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
18940
+ try {
18941
+ await withRetry(() => asClient.send(new PutScalingPolicyCommand({
18942
+ PolicyName: policyName,
18943
+ ServiceNamespace: "dynamodb",
18944
+ ResourceId: resourceId,
18945
+ ScalableDimension: dimension,
18946
+ PolicyType: "TargetTrackingScaling",
18947
+ TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
18948
+ })), `${policyName}`, {
18949
+ isRetryable: (_message, error) => isThrottlingError(error),
18950
+ maxRetries: asMaxRetries,
18951
+ isInterrupted: watch.isInterrupted,
18952
+ onInterrupted: watch.onInterrupted,
18953
+ ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18954
+ });
18955
+ this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
18956
+ } catch (err) {
18957
+ if (isInterruptedWaitError(err)) throw err;
18958
+ warn(`Could not put auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling put-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --policy-type TargetTrackingScaling`);
18959
+ }
18666
18960
  return;
18667
18961
  }
18962
+ const isObjectNotFound = (err) => {
18963
+ if (!(err instanceof Error)) return false;
18964
+ const name = err.name ?? "";
18965
+ const msg = err.message ?? "";
18966
+ return name === "ObjectNotFoundException" || msg.includes("No scaling policy found") || msg.includes("No scalable target found");
18967
+ };
18668
18968
  try {
18669
- await withRetry(() => asClient.send(new RegisterScalableTargetCommand({
18969
+ await withRetry(() => asClient.send(new DeleteScalingPolicyCommand({
18970
+ PolicyName: policyName,
18670
18971
  ServiceNamespace: "dynamodb",
18671
18972
  ResourceId: resourceId,
18672
- ScalableDimension: dimension,
18673
- MinCapacity: minCapacity,
18674
- MaxCapacity: maxCapacity
18973
+ ScalableDimension: dimension
18675
18974
  })), `${resourceId} (${dimension})`, {
18676
18975
  isRetryable: (_message, error) => isThrottlingError(error),
18677
18976
  maxRetries: asMaxRetries,
18977
+ isInterrupted: watch.isInterrupted,
18978
+ onInterrupted: watch.onInterrupted,
18678
18979
  ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18679
18980
  });
18680
18981
  } catch (err) {
18681
- warn(`Could not register auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling register-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --min-capacity ${minCapacity} --max-capacity ${maxCapacity}`);
18682
- return;
18982
+ if (isInterruptedWaitError(err)) throw err;
18983
+ if (!isObjectNotFound(err)) warn(`Could not delete auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling delete-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18683
18984
  }
18684
- const tttCfg = newSettings["TargetTrackingScalingPolicyConfiguration"] ?? {};
18685
- const targetValue = Number(tttCfg["TargetValue"]);
18686
- if (!Number.isFinite(targetValue)) {
18687
- warn(`Auto-scaling target registered on ${tableName} (${dimension}) but TargetValue is missing or non-numeric — skipping PutScalingPolicy. Provide TargetTrackingScalingPolicyConfiguration.TargetValue in the template.`);
18688
- return;
18689
- }
18690
- const targetTrackingConfig = {
18691
- PredefinedMetricSpecification: { PredefinedMetricType: metricType },
18692
- TargetValue: targetValue
18693
- };
18694
- if (tttCfg["ScaleInCooldown"] !== void 0) targetTrackingConfig["ScaleInCooldown"] = Number(tttCfg["ScaleInCooldown"]);
18695
- if (tttCfg["ScaleOutCooldown"] !== void 0) targetTrackingConfig["ScaleOutCooldown"] = Number(tttCfg["ScaleOutCooldown"]);
18696
- if (tttCfg["DisableScaleIn"] !== void 0) targetTrackingConfig["DisableScaleIn"] = Boolean(tttCfg["DisableScaleIn"]);
18697
18985
  try {
18698
- await withRetry(() => asClient.send(new PutScalingPolicyCommand({
18699
- PolicyName: policyName,
18986
+ await withRetry(() => asClient.send(new DeregisterScalableTargetCommand({
18700
18987
  ServiceNamespace: "dynamodb",
18701
18988
  ResourceId: resourceId,
18702
- ScalableDimension: dimension,
18703
- PolicyType: "TargetTrackingScaling",
18704
- TargetTrackingScalingPolicyConfiguration: targetTrackingConfig
18705
- })), `${policyName}`, {
18989
+ ScalableDimension: dimension
18990
+ })), `${resourceId} (${dimension})`, {
18706
18991
  isRetryable: (_message, error) => isThrottlingError(error),
18707
18992
  maxRetries: asMaxRetries,
18993
+ isInterrupted: watch.isInterrupted,
18994
+ onInterrupted: watch.onInterrupted,
18708
18995
  ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18709
18996
  });
18710
- this.logger.debug(`Upserted auto-scaling policy ${policyName} on ${tableName} (${dimension})`);
18997
+ this.logger.debug(`Deregistered auto-scaling target ${resourceId} (${dimension})`);
18711
18998
  } catch (err) {
18712
- warn(`Could not put auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling put-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension} --policy-type TargetTrackingScaling`);
18999
+ if (isInterruptedWaitError(err)) throw err;
19000
+ if (!isObjectNotFound(err)) warn(`Could not deregister auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling deregister-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18713
19001
  }
18714
- return;
18715
- }
18716
- const isObjectNotFound = (err) => {
18717
- if (!(err instanceof Error)) return false;
18718
- const name = err.name ?? "";
18719
- const msg = err.message ?? "";
18720
- return name === "ObjectNotFoundException" || msg.includes("No scaling policy found") || msg.includes("No scalable target found");
18721
- };
18722
- try {
18723
- await withRetry(() => asClient.send(new DeleteScalingPolicyCommand({
18724
- PolicyName: policyName,
18725
- ServiceNamespace: "dynamodb",
18726
- ResourceId: resourceId,
18727
- ScalableDimension: dimension
18728
- })), `${resourceId} (${dimension})`, {
18729
- isRetryable: (_message, error) => isThrottlingError(error),
18730
- maxRetries: asMaxRetries,
18731
- ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18732
- });
18733
- } catch (err) {
18734
- if (!isObjectNotFound(err)) warn(`Could not delete auto-scaling policy on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling delete-scaling-policy --policy-name ${policyName} --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
18735
- }
18736
- try {
18737
- await withRetry(() => asClient.send(new DeregisterScalableTargetCommand({
18738
- ServiceNamespace: "dynamodb",
18739
- ResourceId: resourceId,
18740
- ScalableDimension: dimension
18741
- })), `${resourceId} (${dimension})`, {
18742
- isRetryable: (_message, error) => isThrottlingError(error),
18743
- maxRetries: asMaxRetries,
18744
- ...autoScalingRetryDelays.sleep ? { sleep: autoScalingRetryDelays.sleep } : {}
18745
- });
18746
- this.logger.debug(`Deregistered auto-scaling target ${resourceId} (${dimension})`);
18747
- } catch (err) {
18748
- if (!isObjectNotFound(err)) warn(`Could not deregister auto-scaling target on ${tableName} (${dimension}): ${err instanceof Error ? err.message : String(err)}. Run: aws application-autoscaling deregister-scalable-target --service-namespace dynamodb --resource-id ${resourceId} --scalable-dimension ${dimension}`);
19002
+ } finally {
19003
+ watch.dispose();
18749
19004
  }
18750
19005
  }
18751
19006
  /**
@@ -18943,6 +19198,7 @@ var DynamoDBGlobalTableProvider = class {
18943
19198
  }
18944
19199
  } catch (describeErr) {
18945
19200
  if (describeErr instanceof ProvisioningError) throw describeErr;
19201
+ if (isInterruptedWaitError(describeErr)) throw describeErr;
18946
19202
  if (!(describeErr instanceof ResourceNotFoundException$1)) {
18947
19203
  const cause = describeErr instanceof Error ? describeErr : void 0;
18948
19204
  throw new ProvisioningError(`Failed to describe DynamoDB GlobalTable ${logicalId} before delete: ${describeErr instanceof Error ? describeErr.message : String(describeErr)}`, resourceType, logicalId, physicalId, cause);
@@ -19654,12 +19910,20 @@ var DynamoDBGlobalTableProvider = class {
19654
19910
  */
19655
19911
  async waitForReplicaGone(tableName, region, logicalId, maxAttempts = REPLICA_GONE_WAIT_ATTEMPTS, budget) {
19656
19912
  const wait = beginDeleteWait(maxAttempts, budget);
19657
- while (wait.nextPoll()) try {
19658
- if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
19659
- await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
19660
- } catch (err) {
19661
- if (err instanceof ResourceNotFoundException$1) return;
19662
- throw err;
19913
+ const watch = startInterruptWatch(`DynamoDB replica ${region} teardown on ${tableName}`);
19914
+ try {
19915
+ while (wait.nextPoll()) {
19916
+ if (watch.isInterrupted()) throw watch.onInterrupted();
19917
+ try {
19918
+ if (!(await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }))).Table?.Replicas?.find((r) => r.RegionName === region)) return;
19919
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
19920
+ } catch (err) {
19921
+ if (err instanceof ResourceNotFoundException$1) return;
19922
+ throw err;
19923
+ }
19924
+ }
19925
+ } finally {
19926
+ watch.dispose();
19663
19927
  }
19664
19928
  throw new ProvisioningError(`Replica ${region} for table ${tableName} did not disappear within ${wait.pollsRun}s${wait.note()}`, "AWS::DynamoDB::GlobalTable", logicalId, tableName);
19665
19929
  }
@@ -19679,12 +19943,23 @@ var DynamoDBGlobalTableProvider = class {
19679
19943
  */
19680
19944
  async waitForTableGone(tableName, logicalId, maxAttempts = 600, budget) {
19681
19945
  const wait = beginDeleteWait(maxAttempts, budget);
19682
- while (wait.nextPoll()) try {
19683
- await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
19684
- await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
19685
- } catch (err) {
19686
- if (err instanceof ResourceNotFoundException$1) return;
19687
- throw err;
19946
+ const watch = startInterruptWatch(`DynamoDB table ${tableName} deletion wait`);
19947
+ try {
19948
+ while (wait.nextPoll()) {
19949
+ if (watch.isInterrupted()) {
19950
+ this.logger.warn(`DynamoDB GlobalTable ${logicalId}: interrupted while waiting for ${tableName} to disappear. AWS ACCEPTED the DeleteTable and will finish it; cdkd stopped watching.`);
19951
+ return;
19952
+ }
19953
+ try {
19954
+ await this.dynamoDBClient.send(new DescribeTableCommand({ TableName: tableName }));
19955
+ await new Promise((resolve) => setTimeout(resolve, INDEX_SETTLE_POLL_INTERVAL_MS));
19956
+ } catch (err) {
19957
+ if (err instanceof ResourceNotFoundException$1) return;
19958
+ throw err;
19959
+ }
19960
+ }
19961
+ } finally {
19962
+ watch.dispose();
19688
19963
  }
19689
19964
  if (wait.cutShort) {
19690
19965
  this.logger.warn(`DynamoDB GlobalTable ${logicalId}: DeleteTable on ${tableName} was ACCEPTED by AWS but cdkd stopped waiting for the table to disappear${wait.note()} The delete completes on AWS's side; a later describe (or a re-run of this destroy) sees it gone.`);
@@ -35399,13 +35674,23 @@ var ELBv2Provider = class {
35399
35674
  try {
35400
35675
  const listenerAttributes = this.normalizeAttributes(properties["ListenerAttributes"]);
35401
35676
  if (listenerAttributes.length > 0) {
35402
- await withRetry(() => this.getClient().send(new ModifyListenerAttributesCommand({
35403
- ListenerArn: listenerArn,
35404
- Attributes: listenerAttributes
35405
- })), logicalId, { logger: this.maskedRetryLogger(maskSecrets) });
35677
+ const watch = startInterruptWatch(`ELBv2 Listener ${logicalId} attributes`);
35678
+ try {
35679
+ await withRetry(() => this.getClient().send(new ModifyListenerAttributesCommand({
35680
+ ListenerArn: listenerArn,
35681
+ Attributes: listenerAttributes
35682
+ })), logicalId, {
35683
+ logger: this.maskedRetryLogger(maskSecrets),
35684
+ isInterrupted: watch.isInterrupted,
35685
+ onInterrupted: watch.onInterrupted
35686
+ });
35687
+ } finally {
35688
+ watch.dispose();
35689
+ }
35406
35690
  this.logger.debug(`Applied ${listenerAttributes.length} ListenerAttribute(s) for ${logicalId}`);
35407
35691
  }
35408
35692
  } catch (innerError) {
35693
+ if (isInterruptedWaitError(innerError)) this.logger.warn(maskerOrIdentity(maskSecrets)(`Interrupted after creating Listener ${logicalId} (${listenerArn}) but before its attributes were applied. Nothing in cdkd state refers to it, so cdkd is deleting it now — left behind it would fail the next deploy with DuplicateListener and neither rollback nor destroy could reach it. If that delete does not complete, remove it manually: aws elbv2 delete-listener --listener-arn ${listenerArn}`));
35409
35694
  try {
35410
35695
  await this.getClient().send(new DeleteListenerCommand({ ListenerArn: listenerArn }));
35411
35696
  this.logger.debug(`Cleaned up partially-created Listener ${logicalId} (${listenerArn}) after attributes-wiring failure`);
@@ -35442,10 +35727,19 @@ var ELBv2Provider = class {
35442
35727
  }));
35443
35728
  const submittedAttrs = this.diffAttributes(this.normalizeAttributes(properties["ListenerAttributes"]), this.normalizeAttributes(previousProperties["ListenerAttributes"]), this.attributeRemovalResolver(LISTENER_ATTRIBUTE_DEFAULTS));
35444
35729
  if (submittedAttrs.length > 0) {
35445
- await withRetry(() => this.getClient().send(new ModifyListenerAttributesCommand({
35446
- ListenerArn: physicalId,
35447
- Attributes: submittedAttrs
35448
- })), logicalId, { logger: this.maskedRetryLogger(maskSecrets) });
35730
+ const watch = startInterruptWatch(`ELBv2 Listener ${logicalId} attributes`);
35731
+ try {
35732
+ await withRetry(() => this.getClient().send(new ModifyListenerAttributesCommand({
35733
+ ListenerArn: physicalId,
35734
+ Attributes: submittedAttrs
35735
+ })), logicalId, {
35736
+ logger: this.maskedRetryLogger(maskSecrets),
35737
+ isInterrupted: watch.isInterrupted,
35738
+ onInterrupted: watch.onInterrupted
35739
+ });
35740
+ } finally {
35741
+ watch.dispose();
35742
+ }
35449
35743
  this.logger.debug(`Applied ${submittedAttrs.length} ListenerAttributes change(s) for ${logicalId}`);
35450
35744
  }
35451
35745
  await this.applyTagDiff(physicalId, previousProperties["Tags"], properties["Tags"]);
@@ -42539,16 +42833,23 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42539
42833
  this.logger.debug(`Deleting Cloud Map namespace ${logicalId}: ${physicalId}`);
42540
42834
  const client = this.getClient();
42541
42835
  try {
42542
- await withRetry(async () => {
42543
- const operationId = (await client.send(new DeleteNamespaceCommand({ Id: physicalId }))).OperationId;
42544
- if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42545
- }, logicalId, {
42546
- maxRetries: 24,
42547
- initialDelayMs: 3e3,
42548
- maxDelayMs: 15e3,
42549
- isRetryable: (message, error) => error instanceof ResourceInUse || /has associated services/i.test(message),
42550
- logger: this.logger
42551
- });
42836
+ const watch = startInterruptWatch(`Cloud Map namespace ${logicalId}`);
42837
+ try {
42838
+ await withRetry(async () => {
42839
+ const operationId = (await client.send(new DeleteNamespaceCommand({ Id: physicalId }))).OperationId;
42840
+ if (operationId) await this.pollOperation(operationId, logicalId, resourceType);
42841
+ }, logicalId, {
42842
+ maxRetries: 24,
42843
+ initialDelayMs: 3e3,
42844
+ maxDelayMs: 15e3,
42845
+ isRetryable: (message, error) => error instanceof ResourceInUse || /has associated services/i.test(message),
42846
+ logger: this.logger,
42847
+ isInterrupted: watch.isInterrupted,
42848
+ onInterrupted: watch.onInterrupted
42849
+ });
42850
+ } finally {
42851
+ watch.dispose();
42852
+ }
42552
42853
  this.logger.debug(`Successfully deleted Cloud Map namespace ${logicalId}`);
42553
42854
  } catch (error) {
42554
42855
  if (error instanceof NamespaceNotFound) {
@@ -42748,13 +43049,23 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42748
43049
  try {
42749
43050
  const attrs = this.normalizeServiceAttributes(properties["ServiceAttributes"]);
42750
43051
  if (Object.keys(attrs).length > 0) {
42751
- await withRetry(() => client.send(new UpdateServiceAttributesCommand({
42752
- ServiceId: serviceId,
42753
- Attributes: attrs
42754
- })), logicalId, { logger: this.maskedRetryLogger(maskSecrets) });
43052
+ const watch = startInterruptWatch(`Cloud Map service ${logicalId} attributes`);
43053
+ try {
43054
+ await withRetry(() => client.send(new UpdateServiceAttributesCommand({
43055
+ ServiceId: serviceId,
43056
+ Attributes: attrs
43057
+ })), logicalId, {
43058
+ logger: this.maskedRetryLogger(maskSecrets),
43059
+ isInterrupted: watch.isInterrupted,
43060
+ onInterrupted: watch.onInterrupted
43061
+ });
43062
+ } finally {
43063
+ watch.dispose();
43064
+ }
42755
43065
  this.logger.debug(`Applied ${Object.keys(attrs).length} ServiceAttribute(s) for ${logicalId}`);
42756
43066
  }
42757
43067
  } catch (innerError) {
43068
+ if (isInterruptedWaitError(innerError)) this.logger.warn(maskerOrIdentity(maskSecrets)(`Interrupted after creating ServiceDiscovery Service ${logicalId} (${serviceId}) but before its ServiceAttributes were applied. Nothing in cdkd state refers to it, so cdkd is deleting it now — left behind it would fail the next deploy on a name collision AND block deletion of its namespace with ResourceInUse. If that delete does not complete, remove it manually: aws servicediscovery delete-service --id ${serviceId}`));
42758
43069
  try {
42759
43070
  await client.send(new DeleteServiceCommand$1({ Id: serviceId }));
42760
43071
  this.logger.debug(`Cleaned up partially-created ServiceDiscovery Service ${logicalId} (${serviceId}) after ServiceAttributes wiring failure`);
@@ -42824,19 +43135,34 @@ var ServiceDiscoveryProvider = class ServiceDiscoveryProvider {
42824
43135
  }))).OperationId;
42825
43136
  if (operationId) await this.pollOperation(operationId, logicalId, resourceType, maskSecrets);
42826
43137
  }
42827
- if (hasAttrUpsert) {
42828
- await withRetry(() => client.send(new UpdateServiceAttributesCommand({
42829
- ServiceId: physicalId,
42830
- Attributes: upsertAttrs
42831
- })), logicalId, { logger: this.maskedRetryLogger(maskSecrets) });
42832
- this.logger.debug(`Applied ${Object.keys(upsertAttrs).length} ServiceAttribute change(s) for ${logicalId}`);
42833
- }
42834
- if (hasAttrRemove) {
42835
- await withRetry(() => client.send(new DeleteServiceAttributesCommand({
42836
- ServiceId: physicalId,
42837
- Attributes: removedAttrKeys
42838
- })), logicalId, { logger: this.maskedRetryLogger(maskSecrets) });
42839
- this.logger.debug(`Removed ${removedAttrKeys.length} ServiceAttribute(s) for ${logicalId}`);
43138
+ if (hasAttrUpsert || hasAttrRemove) {
43139
+ const attrWatch = startInterruptWatch(`Cloud Map service ${logicalId} attributes`);
43140
+ try {
43141
+ if (hasAttrUpsert) {
43142
+ await withRetry(() => client.send(new UpdateServiceAttributesCommand({
43143
+ ServiceId: physicalId,
43144
+ Attributes: upsertAttrs
43145
+ })), logicalId, {
43146
+ logger: this.maskedRetryLogger(maskSecrets),
43147
+ isInterrupted: attrWatch.isInterrupted,
43148
+ onInterrupted: attrWatch.onInterrupted
43149
+ });
43150
+ this.logger.debug(`Applied ${Object.keys(upsertAttrs).length} ServiceAttribute change(s) for ${logicalId}`);
43151
+ }
43152
+ if (hasAttrRemove) {
43153
+ await withRetry(() => client.send(new DeleteServiceAttributesCommand({
43154
+ ServiceId: physicalId,
43155
+ Attributes: removedAttrKeys
43156
+ })), logicalId, {
43157
+ logger: this.maskedRetryLogger(maskSecrets),
43158
+ isInterrupted: attrWatch.isInterrupted,
43159
+ onInterrupted: attrWatch.onInterrupted
43160
+ });
43161
+ this.logger.debug(`Removed ${removedAttrKeys.length} ServiceAttribute(s) for ${logicalId}`);
43162
+ }
43163
+ } finally {
43164
+ attrWatch.dispose();
43165
+ }
42840
43166
  }
42841
43167
  this.logger.debug(`Successfully updated service discovery service ${logicalId}`);
42842
43168
  return {
@@ -58198,7 +58524,7 @@ async function runDestroyForStack(stackName, state, ctx) {
58198
58524
  } catch (error) {
58199
58525
  renderer.removeTask(logicalId);
58200
58526
  const msg = error instanceof Error ? error.message : String(error);
58201
- if (!isFinalSnapshotError(error) && (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException"))) {
58527
+ if (!isInterruptedWaitError(error) && !isFinalSnapshotError(error) && (msg.includes("does not exist") || msg.includes("not found") || msg.includes("No policy found") || msg.includes("NoSuchEntity") || msg.includes("NotFoundException"))) {
58202
58528
  logger.debug(` ${logicalId} already deleted, removing from state`);
58203
58529
  result.deletedCount++;
58204
58530
  ctx.eventRecorder?.record({
@@ -58282,11 +58608,19 @@ async function runDestroyForStack(stackName, state, ctx) {
58282
58608
  logger.warn(`\n${yellow("⚠")} ${bold(`Stack ${stackName} partially destroyed`)} (${green(result.deletedCount)} deleted${retainedSuffix}${skippedSuffix}, ${red(result.errorCount)} errors). State preserved — re-run 'cdkd destroy' / 'cdkd state destroy' to clean up. If the same resource keeps failing, ${orphanHint} is the last resort: it removes the state record without deleting AWS resources.` + skippedClause);
58283
58609
  }
58284
58610
  } finally {
58285
- process.removeListener("SIGINT", sigintHandler);
58286
- renderer.stop();
58287
- await saveChain;
58288
- logger.debug("Releasing lock...");
58289
- await ctx.lockManager.releaseLock(stackName, regionForState);
58611
+ try {
58612
+ try {
58613
+ renderer.stop();
58614
+ } catch (rendererError) {
58615
+ logger.debug(`Live renderer teardown failed (continuing to release the lock): ${rendererError instanceof Error ? rendererError.message : String(rendererError)}`);
58616
+ }
58617
+ await saveChain;
58618
+ logger.debug("Releasing lock...");
58619
+ await ctx.lockManager.releaseLock(stackName, regionForState);
58620
+ } finally {
58621
+ process.removeListener("SIGINT", sigintHandler);
58622
+ }
58623
+ result.interrupted ||= draining;
58290
58624
  if (destroyAwsClients) {
58291
58625
  destroyAwsClients.destroy();
58292
58626
  process.env["AWS_REGION"] = ctx.baseRegion;
@@ -72041,26 +72375,37 @@ async function gcCommand(options) {
72041
72375
  roleArn: options.roleArn,
72042
72376
  region: namedCliRegion(options.region)
72043
72377
  });
72044
- const region = namedCliRegion(options.region) ?? "us-east-1";
72045
- const rawRegion = rawCliRegion(options.region) ?? region;
72046
- const awsClients = new AwsClients({
72047
- region,
72378
+ const effective = await resolveEffectiveRegion(options);
72379
+ const accountClients = new AwsClients({
72380
+ region: effective.region,
72048
72381
  ...options.profile && { profile: options.profile }
72049
72382
  });
72050
- setAwsClients(awsClients);
72051
- const accountId = (await awsClients.sts.send(new GetCallerIdentityCommand({}))).Account;
72383
+ const accountId = (await accountClients.sts.send(new GetCallerIdentityCommand({}))).Account;
72052
72384
  const bucketName = options.stateBucket ?? getDefaultStateBucketName(accountId);
72053
72385
  const markerS3Client = new S3Client({
72054
- region,
72386
+ region: effective.region,
72055
72387
  ...options.profile && { profile: options.profile }
72056
72388
  });
72057
72389
  const stateBackend = new S3StateBackend(markerS3Client, {
72058
72390
  bucket: bucketName,
72059
72391
  prefix: "cdkd"
72060
72392
  }, {
72393
+ region: effective.region,
72394
+ ...options.profile && { profile: options.profile }
72395
+ });
72396
+ const region = regionNeedsReconciliation(effective) ? await reconcileMarkerRegionWithLegacyDefault({
72397
+ effective,
72398
+ probe: stateBackend,
72399
+ markerKeyFor: getBootstrapMarkerKey,
72400
+ logger
72401
+ }) : effective.region;
72402
+ const rawRegion = rawCliRegion(options.region) ?? region;
72403
+ const awsClients = region === effective.region ? accountClients : new AwsClients({
72061
72404
  region,
72062
72405
  ...options.profile && { profile: options.profile }
72063
72406
  });
72407
+ if (awsClients !== accountClients) accountClients.destroy();
72408
+ setAwsClients(awsClients);
72064
72409
  const ecrClient = new ECRClient({
72065
72410
  region,
72066
72411
  ...options.profile && { profile: options.profile }
@@ -72139,7 +72484,7 @@ async function gcCommand(options) {
72139
72484
  * Create the `cdkd gc` command (upstream `cdk gc` parity naming).
72140
72485
  */
72141
72486
  function createGcCommand() {
72142
- const cmd = new Command("gc").description("Garbage-collect unreferenced objects / images from ONE region's cdkd-owned asset storage (asset bucket + container-asset ECR repo). References are collected from every cdkd state file; CDK bootstrap storage is never touched (use cdk gc for that).").option("--state-bucket <bucket>", "S3 bucket holding cdkd state (default: cdkd-state-{accountId})").addOption(new Option("--older-than <duration>", "Never delete an object / image newer than this age, even when unreferenced (protects in-flight publishes and recent rollback targets). Accepts <n>d / <n>h.").default(DEFAULT_OLDER_THAN_MS, "30d").argParser(parseOlderThan)).option("--dry-run", "Print the reclaim plan (per-item list + totals) without deleting", false).addOption(new Option("--region <region>", "Region whose cdkd asset storage to garbage-collect (defaults to AWS_REGION / AWS_DEFAULT_REGION, else the profile region; refuses if none is configured)")).action(withErrorHandling(async (options) => {
72487
+ const cmd = new Command("gc").description("Garbage-collect unreferenced objects / images from ONE region's cdkd-owned asset storage (asset bucket + container-asset ECR repo). References are collected from every cdkd state file; CDK bootstrap storage is never touched (use cdk gc for that).").option("--state-bucket <bucket>", "S3 bucket holding cdkd state (default: cdkd-state-{accountId})").addOption(new Option("--older-than <duration>", "Never delete an object / image newer than this age, even when unreferenced (protects in-flight publishes and recent rollback targets). Accepts <n>d / <n>h.").default(DEFAULT_OLDER_THAN_MS, "30d").argParser(parseOlderThan)).option("--dry-run", "Print the reclaim plan (per-item list + totals) without deleting", false).addOption(new Option("--region <region>", "Region whose cdkd asset storage to garbage-collect (defaults to AWS_REGION, then AWS_DEFAULT_REGION, then your AWS profile's region, then us-east-1)")).action(withErrorHandling(async (options) => {
72143
72488
  await gcCommand(options);
72144
72489
  }));
72145
72490
  commonOptions.forEach((opt) => cmd.addOption(opt));
@@ -83914,7 +84259,7 @@ function createMigrateCommand() {
83914
84259
  */
83915
84260
  function buildProgram() {
83916
84261
  const program = new Command();
83917
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.24");
84262
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.284.26");
83918
84263
  program.hook("preAction", (_thisCommand, actionCommand) => {
83919
84264
  const { profile } = actionCommand.optsWithGlobals();
83920
84265
  if (profile !== void 0) process.env["AWS_PROFILE"] = profile;