@go-to-k/cdkd 0.284.8 → 0.284.9

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.
@@ -296,6 +296,182 @@ function isThrottlingError(error) {
296
296
  return false;
297
297
  }
298
298
  /**
299
+ * HTTP status codes that indicate a TRANSIENT SERVER-side failure worth
300
+ * retrying (issue #2026).
301
+ *
302
+ * Mirrors `@smithy/service-error-classification`'s own
303
+ * `TRANSIENT_ERROR_STATUS_CODES` (`[500, 502, 503, 504]`), which is what the
304
+ * AWS SDK's default retry strategy treats as transient. Deliberately a
305
+ * SEPARATE set from {@link RETRYABLE_HTTP_STATUS_CODES} rather than an
306
+ * extension of it, because that one is consumed by {@link isThrottlingError},
307
+ * which SEVEN call sites across four files pass as a deliberately NARROW
308
+ * `isRetryable`: `describe-type.ts:67` (which states the intent outright --
309
+ * "retry ONLY throttle-shaped failures"), `dynamodb-globaltable-provider.ts`
310
+ * (x4), `export.ts:1744`, and `intrinsic-function-resolver.ts:5216`. Widening
311
+ * the shared set would have silently converted every one of them from "retry
312
+ * throttles" into "retry throttles and server errors", which none of them
313
+ * asked for.
314
+ *
315
+ * Three FURTHER sites call it as a bare classification rather than as a retry
316
+ * filter -- `drift.ts:518`, `export.ts:1755`, `dynamodb-index-busy-delete.ts:381`
317
+ * -- and they make the case stronger, not weaker: `drift.ts` would have started
318
+ * returning `undefined` (reporting "cannot compare") for a resource whose read
319
+ * merely 500'd, and the index poll would have waited a server error out as
320
+ * though it were a throttle.
321
+ *
322
+ * Measured, not inferred. `tests/integration/iam-propagation-stress` against
323
+ * real AWS (us-east-1, 2026-08-19 08:57:30Z, round 11 of 11) produced:
324
+ *
325
+ * StressQueuePolicyDC3E35C3: gave up after 5 IAM-propagation retries over
326
+ * 5.75s of propagation backoff - Failed to create SQS queue policy
327
+ * StressQueuePolicyDC3E35C3: UnknownError
328
+ * [name=InternalFailure http=500 requestId=ebf581cc-6072-5ffc-943a-e33312488615]
329
+ *
330
+ * SQS answered a `SetQueueAttributes` mid-propagation with HTTP 500
331
+ * `InternalFailure` and an empty message body -- hence the `UnknownError`
332
+ * placeholder, which matches no message pattern. With 500 absent from every
333
+ * status set, `withRetry` classified it non-retryable and threw at 5.75s of a
334
+ * 47.75s budget the sequence needed roughly 10s of.
335
+ *
336
+ * Why 502 and 504 come along rather than only the measured 500: they are the
337
+ * same class (a gateway or timeout between AWS's edge and the service), the
338
+ * SDK groups all four, and adding only the one status seen would leave the
339
+ * identical defect behind for its siblings.
340
+ *
341
+ * Note the SDK has ALREADY retried these before cdkd sees them (default
342
+ * `maxAttempts` is 3), so a 5xx reaching this classifier is one that persisted
343
+ * across the SDK's own attempts. That is an argument FOR retrying it here, not
344
+ * against: the eventual-consistency window this schedule exists to cover is
345
+ * measured in seconds, while the SDK's three attempts span well under one.
346
+ *
347
+ * ACCEPTED RISK, stated rather than discovered later: this makes a
348
+ * NON-IDEMPOTENT create retryable on a 500 that may have succeeded
349
+ * server-side. `EC2Provider.createInstance` issues `RunInstances` with no
350
+ * `ClientToken` (only four providers use one at all), and
351
+ * `IAMAccessKeyProvider` mints an unnamed key, so a replay can leave a
352
+ * resource that is absent from state and therefore from destroy. The class is
353
+ * PRE-EXISTING -- the SDK's own three attempts already reach it, and 503 was
354
+ * already retryable here -- but this widens the window from ~1s to the full
355
+ * schedule. Judged worth it because the alternative is the measured failure
356
+ * (a deploy that dies outright on a transient 500), and because the durable
357
+ * remedy is per-provider idempotency tokens rather than a blanket refusal to
358
+ * retry server errors. Tracked in issue #2039.
359
+ */
360
+ const TRANSIENT_SERVER_ERROR_STATUS_CODES = /* @__PURE__ */ new Set([
361
+ 500,
362
+ 502,
363
+ 503,
364
+ 504
365
+ ]);
366
+ /**
367
+ * Walk the error + its `.cause` chain (bounded, same depth 5 as
368
+ * {@link isThrottlingError}) looking for a transient SERVER-side HTTP status
369
+ * ({@link TRANSIENT_SERVER_ERROR_STATUS_CODES}) on `$metadata`.
370
+ *
371
+ * The walk is what makes it work in practice: providers wrap the AWS error in
372
+ * a `ProvisioningError`, so the `$metadata` carrying the status sits one link
373
+ * down, and the wrapper's interpolated message is all a message-based
374
+ * classifier can see. In the measured failure that message was the literal
375
+ * `UnknownError`, so the status was the ONLY usable evidence in the whole
376
+ * error.
377
+ */
378
+ function isTransientServerError(error) {
379
+ let current = error;
380
+ for (let depth = 0; depth < 5 && current != null; depth++) {
381
+ const status = current.$metadata?.httpStatusCode;
382
+ if (status !== void 0 && TRANSIENT_SERVER_ERROR_STATUS_CODES.has(status)) return true;
383
+ current = current.cause;
384
+ }
385
+ return false;
386
+ }
387
+ /**
388
+ * Collect {@link RetryClassificationSignals} from an error and its bounded
389
+ * `.cause` chain — the SAME walk, to the same depth 5, that
390
+ * {@link isThrottlingError} performs, so the line reports what the classifier
391
+ * genuinely saw rather than a second opinion gathered differently.
392
+ *
393
+ * The signals are taken from the first link carrying a `$metadata` object,
394
+ * because that link IS the AWS SDK error by construction: `$metadata` is
395
+ * attached by the SDK's own `deserializeMetadata`, so nothing else can carry
396
+ * it. When no link has one, the fallback is the deepest name found BELOW depth
397
+ * 0 -- which keeps the field useful for the wrapped-network-error case, where
398
+ * the name is all that survives.
399
+ *
400
+ * Excluding depth 0 from that fallback is deliberate and is what stops the
401
+ * suffix from being noise. The error `withRetry` is handed is the provider's
402
+ * own wrapper by construction (every provider catches the AWS error and
403
+ * rethrows a `ProvisioningError`), so its `name` is a cdkd class name and says
404
+ * nothing about the service. Reporting it produced the actively misleading
405
+ * ` [name=ProvisioningError no-$metadata]` on a wrapper carrying no cause at
406
+ * all -- a suffix asserting the SDK never parsed a response, about an error
407
+ * that never came from the SDK.
408
+ *
409
+ * Nothing is lost in the case this helper exists for. A degenerate
410
+ * `UnknownError` message can only be produced by `decorateServiceException`,
411
+ * i.e. by a smithy `ServiceException`, and those always carry `$metadata` --
412
+ * so that case is answered by the FIRST branch and never reaches this
413
+ * fallback.
414
+ *
415
+ * Known narrowness: the first link with a NUMERIC status wins, so an outer
416
+ * link carrying a 400 that wraps a cause carrying a 500 reports the 400 while
417
+ * `isTransientServerError` retried on the 500. Left as-is because cdkd's own
418
+ * wrappers carry no `$metadata` at all, so producing that shape takes two
419
+ * stacked SDK errors -- but it is the same "must not contradict the
420
+ * classifier" case one link further out, and is the thing to revisit if such a
421
+ * chain is ever observed. A cdkd module deliberately importing no other module, this one
422
+ * cannot ask `error instanceof CdkdError` directly: `error-handler.ts` imports
423
+ * `markNonRetryable` from here, so the dependency only runs one way.
424
+ */
425
+ function describeRetryClassificationSignals(error) {
426
+ let current = error;
427
+ let deepestName;
428
+ let sawMetadata = false;
429
+ let metadataName;
430
+ let metadataRequestId;
431
+ for (let depth = 0; depth < 5 && current != null; depth++) {
432
+ const name = current.name;
433
+ if (depth > 0 && typeof name === "string" && name !== "") deepestName = name;
434
+ const metadata = current.$metadata;
435
+ if (metadata != null && typeof metadata === "object" && !Array.isArray(metadata)) {
436
+ const { httpStatusCode, requestId } = metadata;
437
+ const linkName = typeof name === "string" && name !== "" ? name : void 0;
438
+ if (!sawMetadata) {
439
+ sawMetadata = true;
440
+ metadataName = linkName;
441
+ metadataRequestId = typeof requestId === "string" && requestId !== "" ? requestId : void 0;
442
+ }
443
+ if (typeof httpStatusCode === "number") return {
444
+ name: linkName ?? deepestName,
445
+ httpStatusCode,
446
+ requestId: (typeof requestId === "string" && requestId !== "" ? requestId : void 0) ?? metadataRequestId,
447
+ noMetadata: false
448
+ };
449
+ }
450
+ current = current.cause;
451
+ }
452
+ return {
453
+ name: metadataName ?? deepestName,
454
+ requestId: metadataRequestId,
455
+ noMetadata: !sawMetadata
456
+ };
457
+ }
458
+ /**
459
+ * Render {@link describeRetryClassificationSignals} as a compact log suffix.
460
+ *
461
+ * Returns `''` when there is nothing to say (no name and no metadata), so a
462
+ * caller can append unconditionally without emitting an empty bracket pair.
463
+ */
464
+ function formatRetryClassificationSignals(error) {
465
+ const signals = describeRetryClassificationSignals(error);
466
+ if (signals.name === void 0 && signals.httpStatusCode === void 0 && signals.requestId === void 0) return "";
467
+ const parts = [];
468
+ if (signals.name !== void 0) parts.push(`name=${signals.name}`);
469
+ if (signals.httpStatusCode !== void 0) parts.push(`http=${signals.httpStatusCode}`);
470
+ if (signals.requestId !== void 0) parts.push(`requestId=${signals.requestId}`);
471
+ if (signals.noMetadata) parts.push("no-$metadata");
472
+ return ` [${parts.join(" ")}]`;
473
+ }
474
+ /**
299
475
  * Determine whether an AWS error should be retried.
300
476
  *
301
477
  * Checks (in order):
@@ -307,11 +483,16 @@ function isThrottlingError(error) {
307
483
  * `name` or retryable HTTP status (most AWS throttles are HTTP 400, not
308
484
  * 429, so the name check carries most of the weight). See
309
485
  * {@link isThrottlingError}.
310
- * 2. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
486
+ * 2. Transient SERVER-side HTTP status (500 / 502 / 503 / 504) on the error
487
+ * or any wrapped cause — see {@link isTransientServerError}. Ahead of the
488
+ * message patterns because it is the only check that still works when the
489
+ * response carried NO message (issue #2026).
490
+ * 3. Substring match against {@link RETRYABLE_ERROR_MESSAGE_PATTERNS}
311
491
  */
312
492
  function isRetryableTransientError(error, message) {
313
493
  if (isMarkedNonRetryable(error)) return false;
314
494
  if (isThrottlingError(error)) return true;
495
+ if (isTransientServerError(error)) return true;
315
496
  return RETRYABLE_ERROR_MESSAGE_PATTERNS.some((p) => message.includes(p));
316
497
  }
317
498
  /**
@@ -8748,6 +8929,7 @@ async function withRetry(operation, logicalId, opts = {}) {
8748
8929
  let sawPropagation = false;
8749
8930
  let propagationRetries = 0;
8750
8931
  let propagationSleptMs = 0;
8932
+ let serverErrorRetries = 0;
8751
8933
  for (let attempt = 0; attempt <= attemptCeiling; attempt++) try {
8752
8934
  return await operation();
8753
8935
  } catch (error) {
@@ -8759,11 +8941,14 @@ async function withRetry(operation, logicalId, opts = {}) {
8759
8941
  if (propagation) sawPropagation = true;
8760
8942
  const attemptLimit = sawPropagation ? 26 : maxRetries;
8761
8943
  if (!retryable || attempt >= attemptLimit) {
8762
- if (propagationRetries > 0) {
8944
+ if (propagationRetries > 0 || serverErrorRetries > 0) {
8763
8945
  const budgetExhausted = sawPropagation && attempt >= attemptLimit;
8764
- const summary = `${logicalId}: gave up after ${propagationRetries} IAM-propagation ${propagationRetries === 1 ? "retry" : "retries"} over ${(propagationSleptMs / 1e3).toFixed(2)}s of propagation backoff${budgetExhausted ? " (the full propagation budget)" : ""} - ${message}`;
8946
+ const spent = [];
8947
+ if (propagationRetries > 0) spent.push(`${propagationRetries} IAM-propagation ${propagationRetries === 1 ? "retry" : "retries"} over ${(propagationSleptMs / 1e3).toFixed(2)}s of propagation backoff${budgetExhausted ? " (the full propagation budget)" : ""}`);
8948
+ if (serverErrorRetries > 0) spent.push(`${serverErrorRetries} transient server-error ${serverErrorRetries === 1 ? "retry" : "retries"} (HTTP 5xx)`);
8949
+ const summary = () => `${logicalId}: gave up after ${spent.join(" and ")} - ${message}` + formatRetryClassificationSignals(error);
8765
8950
  try {
8766
- opts.logger?.warn?.(summary);
8951
+ opts.logger?.warn?.(summary());
8767
8952
  } catch {}
8768
8953
  }
8769
8954
  throw error;
@@ -8778,7 +8963,7 @@ async function withRetry(operation, logicalId, opts = {}) {
8778
8963
  if (propagation) {
8779
8964
  propagationRetries++;
8780
8965
  propagationSleptMs = backoffThroughThisAttemptMs;
8781
- }
8966
+ } else if (opts.isRetryable === void 0 && isTransientServerError(error)) serverErrorRetries++;
8782
8967
  }
8783
8968
  throw lastError;
8784
8969
  }
@@ -15860,7 +16045,7 @@ var CloudControlProvider = class {
15860
16045
  if (context?.finalSnapshotIdentifier !== void 0) throw new ProvisioningError(`${logicalId} (${resourceType}) requires a final snapshot (DeletionPolicy: Snapshot), but the Cloud Control API delete route has no final-snapshot parameter. Re-run with --skip-final-snapshot after snapshotting manually, or retain the resource.`, resourceType, logicalId, physicalId);
15861
16046
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
15862
16047
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
15863
- const { ASGProvider } = await import("./asg-provider-jYyUCsx_.js").then((n) => n.n);
16048
+ const { ASGProvider } = await import("./asg-provider-B09ceB1r.js").then((n) => n.n);
15864
16049
  return await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
15865
16050
  }
15866
16051
  const isProtectedEc2Instance = context?.removeProtection === true && resourceType === "AWS::EC2::Instance";
@@ -23438,7 +23623,7 @@ const FLUSH_INTERVAL_MS = 2e3;
23438
23623
  const FLUSH_EVENT_THRESHOLD = 50;
23439
23624
  /** Build-time cdkd version, with a dev fallback for non-built contexts. */
23440
23625
  function getCdkdVersion() {
23441
- return "0.284.8";
23626
+ return "0.284.9";
23442
23627
  }
23443
23628
  /**
23444
23629
  * Generate a time-sortable unique run id, e.g.
@@ -25907,4 +26092,4 @@ var DeployEngine = class {
25907
26092
 
25908
26093
  //#endregion
25909
26094
  export { IntrinsicFunctionResolver as $, StackHasActiveImportsError as $n, validateContainerRepoName as $t, formatResourceLine as A, AssemblyReader as An, describeTypeWithThrottleRetry as At, isExportAliasCollision as B, ConfigError as Bn, WorkGraph as Bt, refusesFinalSnapshot as C, MIGRATE_TMP_PREFIX as Cn, s3BucketDomainName as Ct, MULTI_REGION_RECREATE_BLOCKED_TYPES as D, PARTITION_TABLE as Dn, applyRoleArnIfSet as Dt, extractDeploymentEventError as E, expectedOwnerParam as En, s3BucketWebsiteUrl as Et, red as F, getAwsClients as Fn, S3StateBackend as Ft, clearOnUpdateRemoval as G, LocalStartServiceError as Gn, escapeRegExp$1 as Gt, stateKeySecretExposure as H, DeployCancelledError as Hn, createAssetRedirectResolver as Ht, yellow as I, resetAwsClients as In, rebuildClientForBucketRegion as It, findSilentDropProperties as J, NestedStackChildDirectDestroyError as Jn, BOOTSTRAP_MARKER_PREFIX as Jt, ProviderRegistry as K, LockError as Kn, stripControlChars as Kt, collectDeclaredOutputNames as L, setAwsClients as Ln, shouldRetainResource as Lt, cyan as M, clearBucketRegionCache as Mn, DagBuilder as Mt, gray as N, resolveBucketRegion as Nn, TemplateParser as Nt, isStatefulRecreateTargetSync as O, canonicalizeRegion as On, DiffCalculator as Ot, green as P, AwsClients as Pn, LockManager as Pt, isTerminationProtectionPropagationError as Q, ResourceUpdateNotSupportedError as Qn, validateAssetBucketName as Qt, collectPublishedOutputNames as R, AssetError as Rn, AssetPublisher as Rt, isFinalSnapshotError as S, CFN_TEMPLATE_URL_LIMIT as Sn, s3BucketArn as St, makeCanonicalizePropertiesFn as T, uploadCfnTemplate as Tn, s3BucketRegionalDomainName as Tt, IAMRoleProvider as U, LocalInvokeBuildError as Un, loadPublishableAssetManifest as Ut, secretBearingStateKeyWarning as V, DependencyError as Vn, buildAssetRedirectMap as Vt, collectInlinePolicyNamesManagedBySiblings as W, LocalMigrateError as Wn, rewriteTemplateAssetReferences as Wt, slowCcOperationTimeoutMs as X, ProvisioningError as Xn, getBootstrapMarkerKey as Xt, CloudControlProvider as Y, PartialFailureError as Yn, ensureAssetStorage as Yt, disableInstanceApiTermination as Z, ResourceTimeoutError as Zn, parseBootstrapMarker as Zt, ATOMIC_FINAL_SNAPSHOT_TYPES as _, resolveStateBucketWithDefaultAndSource as _n, TEMPLATE_SOURCED_RULES as _t, DeploymentEventsStore as a, runDockerStreaming as an, normalizeAwsError as ar, resolveExplicitPhysicalId as at, ccRoutedFinalSnapshotError as b, warnDeprecatedNoPrefixCliFlag as bn, redactSecretsForState as bt, replayFailedOperations as c, Synthesizer as cn, isRetryableTransientError as cr, configBooleanRefusal as ct, updatePartialReason as d, getLegacyStateBucketName as dn, __exportAll as dr, replayWarn as dt, buildDenyExternalAccessPolicy as en, StackTerminationProtectionError as er, cfnRefValueFromPhysicalId as et, UNSPECIFIED_SKIP_REASON as f, resolveApp as fn, requireConfigArray as ft, computeImplicitDeleteEdges as g, resolveStateBucketWithDefault as gn, STATE_SOURCED_READBACK_RULES as gt, IMPLICIT_DELETE_DEPENDENCIES as h, resolveSkipPrefix as hn, STATE_SOURCED_CROSS_GENERATION_RULES as ht, DeploymentEventsReader as i, runDockerForeground as in, isCdkdError as ir, normalizeAwsTagsToCfn as it, bold as j, processStackMessages as jn, withRetry as jt, renderStatefulReason as k, derivePartitionAndUrlSuffix as kn, INTRINSIC_KEYS as kt, replayRollback as l, synthesisStatusMessage as ln, isThrottlingError as lr, configStringRefusal as lt, withResourceDeadline as m, resolveCaptureObservedState as mn, requireConfigString as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, formatDockerLoginError as nn, SynthesisError as nr, refStateLookupFromResource as nt, planFailedOps as o, AssetManifestLoader as on, withErrorHandling as or, assertRegionMatch as ot, deleteSkipReason as p, resolveAutoAssetStorage as pn, requireConfigObject as pt, findActionableSilentDrops as q, MissingCdkCliError as qn, AssetModeResolver as qt, DeployEngine as r, getDockerCmd as rn, formatError as rr, WAFv2WebACLProvider as rt, planRollback as s, getDockerImageBySourceHash as sn, isMarkedNonRetryable as sr, coerceCfnBoolean as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, buildDockerImage as tn, StateError as tr, getAccountInfo as tt, updatePartialMessage as u, getDefaultStateBucketName as un, markNonRetryable as ur, readConfigString as ut, PRE_DELETE_SNAPSHOT_TYPES as v, resolveUseCdkBootstrapAssets as vn, createSecretMasker as vt, unsupportedFinalSnapshotError as w, findLargeInlineResources as wn, s3BucketDualStackDomainName as wt, createPreDeleteFinalSnapshot as x, CFN_TEMPLATE_BODY_LIMIT as xn, scrubResourceRecord as xt, buildFinalSnapshotIdentifier as y, stateBucketExistenceConfirmed as yn, maskSecretsInText as yt, exportAliasCollisionScrubWarning as z, CdkdError as zn, stringifyValue as zt };
25910
- //# sourceMappingURL=deploy-engine-36yWG1OT.js.map
26095
+ //# sourceMappingURL=deploy-engine-CY2fx4K1.js.map