@go-to-k/cdkd 0.262.2 → 0.263.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.
@@ -615,6 +615,13 @@ var AwsClients = class {
615
615
  constructor(config = {}) {
616
616
  this.config = config;
617
617
  }
618
+ get clientOptions() {
619
+ return {
620
+ ...this.config.region && { region: this.config.region },
621
+ ...this.config.profile && { profile: this.config.profile },
622
+ ...this.config.credentials && { credentials: this.config.credentials }
623
+ };
624
+ }
618
625
  /**
619
626
  * Get S3 client
620
627
  *
@@ -625,8 +632,7 @@ var AwsClients = class {
625
632
  */
626
633
  getS3Client() {
627
634
  if (!this.s3Client) this.s3Client = new S3Client({
628
- ...this.config.region && { region: this.config.region },
629
- ...this.config.credentials && { credentials: this.config.credentials },
635
+ ...this.clientOptions,
630
636
  logger: {
631
637
  debug: () => {},
632
638
  info: () => {},
@@ -645,10 +651,7 @@ var AwsClients = class {
645
651
  * 3. IAM role (if running on EC2/ECS/Lambda)
646
652
  */
647
653
  getCloudControlClient() {
648
- if (!this.cloudControlClient) this.cloudControlClient = new CloudControlClient({
649
- ...this.config.region && { region: this.config.region },
650
- ...this.config.credentials && { credentials: this.config.credentials }
651
- });
654
+ if (!this.cloudControlClient) this.cloudControlClient = new CloudControlClient({ ...this.clientOptions });
652
655
  return this.cloudControlClient;
653
656
  }
654
657
  /**
@@ -659,8 +662,8 @@ var AwsClients = class {
659
662
  */
660
663
  getIAMClient() {
661
664
  if (!this.iamClient) this.iamClient = new IAMClient({
662
- region: this.config.region || "us-east-1",
663
- ...this.config.credentials && { credentials: this.config.credentials }
665
+ ...this.clientOptions,
666
+ region: this.config.region || "us-east-1"
664
667
  });
665
668
  return this.iamClient;
666
669
  }
@@ -686,10 +689,7 @@ var AwsClients = class {
686
689
  * Get SQS client
687
690
  */
688
691
  getSQSClient() {
689
- if (!this.sqsClient) this.sqsClient = new SQSClient({
690
- ...this.config.region && { region: this.config.region },
691
- ...this.config.credentials && { credentials: this.config.credentials }
692
- });
692
+ if (!this.sqsClient) this.sqsClient = new SQSClient({ ...this.clientOptions });
693
693
  return this.sqsClient;
694
694
  }
695
695
  /**
@@ -702,10 +702,7 @@ var AwsClients = class {
702
702
  * Get SNS client
703
703
  */
704
704
  getSNSClient() {
705
- if (!this.snsClient) this.snsClient = new SNSClient({
706
- ...this.config.region && { region: this.config.region },
707
- ...this.config.credentials && { credentials: this.config.credentials }
708
- });
705
+ if (!this.snsClient) this.snsClient = new SNSClient({ ...this.clientOptions });
709
706
  return this.snsClient;
710
707
  }
711
708
  /**
@@ -718,10 +715,7 @@ var AwsClients = class {
718
715
  * Get Lambda client
719
716
  */
720
717
  getLambdaClient() {
721
- if (!this.lambdaClient) this.lambdaClient = new LambdaClient({
722
- ...this.config.region && { region: this.config.region },
723
- ...this.config.credentials && { credentials: this.config.credentials }
724
- });
718
+ if (!this.lambdaClient) this.lambdaClient = new LambdaClient({ ...this.clientOptions });
725
719
  return this.lambdaClient;
726
720
  }
727
721
  /**
@@ -734,10 +728,7 @@ var AwsClients = class {
734
728
  * Get EC2 client
735
729
  */
736
730
  getEC2Client() {
737
- if (!this.ec2Client) this.ec2Client = new EC2Client({
738
- ...this.config.region && { region: this.config.region },
739
- ...this.config.credentials && { credentials: this.config.credentials }
740
- });
731
+ if (!this.ec2Client) this.ec2Client = new EC2Client({ ...this.clientOptions });
741
732
  return this.ec2Client;
742
733
  }
743
734
  /**
@@ -750,10 +741,7 @@ var AwsClients = class {
750
741
  * Get STS client
751
742
  */
752
743
  getSTSClient() {
753
- if (!this.stsClient) this.stsClient = new STSClient({
754
- ...this.config.region && { region: this.config.region },
755
- ...this.config.credentials && { credentials: this.config.credentials }
756
- });
744
+ if (!this.stsClient) this.stsClient = new STSClient({ ...this.clientOptions });
757
745
  return this.stsClient;
758
746
  }
759
747
  /**
@@ -766,10 +754,7 @@ var AwsClients = class {
766
754
  * Get DynamoDB client
767
755
  */
768
756
  getDynamoDBClient() {
769
- if (!this.dynamoDBClient) this.dynamoDBClient = new DynamoDBClient({
770
- ...this.config.region && { region: this.config.region },
771
- ...this.config.credentials && { credentials: this.config.credentials }
772
- });
757
+ if (!this.dynamoDBClient) this.dynamoDBClient = new DynamoDBClient({ ...this.clientOptions });
773
758
  return this.dynamoDBClient;
774
759
  }
775
760
  /**
@@ -782,10 +767,7 @@ var AwsClients = class {
782
767
  * Get CloudFormation client
783
768
  */
784
769
  getCloudFormationClient() {
785
- if (!this.cloudFormationClient) this.cloudFormationClient = new CloudFormationClient({
786
- ...this.config.region && { region: this.config.region },
787
- ...this.config.credentials && { credentials: this.config.credentials }
788
- });
770
+ if (!this.cloudFormationClient) this.cloudFormationClient = new CloudFormationClient({ ...this.clientOptions });
789
771
  return this.cloudFormationClient;
790
772
  }
791
773
  /**
@@ -798,10 +780,7 @@ var AwsClients = class {
798
780
  * Get API Gateway client
799
781
  */
800
782
  getAPIGatewayClient() {
801
- if (!this.apiGatewayClient) this.apiGatewayClient = new APIGatewayClient({
802
- ...this.config.region && { region: this.config.region },
803
- ...this.config.credentials && { credentials: this.config.credentials }
804
- });
783
+ if (!this.apiGatewayClient) this.apiGatewayClient = new APIGatewayClient({ ...this.clientOptions });
805
784
  return this.apiGatewayClient;
806
785
  }
807
786
  /**
@@ -814,10 +793,7 @@ var AwsClients = class {
814
793
  * Get EventBridge client
815
794
  */
816
795
  getEventBridgeClient() {
817
- if (!this.eventBridgeClient) this.eventBridgeClient = new EventBridgeClient({
818
- ...this.config.region && { region: this.config.region },
819
- ...this.config.credentials && { credentials: this.config.credentials }
820
- });
796
+ if (!this.eventBridgeClient) this.eventBridgeClient = new EventBridgeClient({ ...this.clientOptions });
821
797
  return this.eventBridgeClient;
822
798
  }
823
799
  /**
@@ -830,10 +806,7 @@ var AwsClients = class {
830
806
  * Get Secrets Manager client
831
807
  */
832
808
  getSecretsManagerClient() {
833
- if (!this.secretsManagerClient) this.secretsManagerClient = new SecretsManagerClient({
834
- ...this.config.region && { region: this.config.region },
835
- ...this.config.credentials && { credentials: this.config.credentials }
836
- });
809
+ if (!this.secretsManagerClient) this.secretsManagerClient = new SecretsManagerClient({ ...this.clientOptions });
837
810
  return this.secretsManagerClient;
838
811
  }
839
812
  /**
@@ -846,10 +819,7 @@ var AwsClients = class {
846
819
  * Get SSM client
847
820
  */
848
821
  getSSMClient() {
849
- if (!this.ssmClient) this.ssmClient = new SSMClient({
850
- ...this.config.region && { region: this.config.region },
851
- ...this.config.credentials && { credentials: this.config.credentials }
852
- });
822
+ if (!this.ssmClient) this.ssmClient = new SSMClient({ ...this.clientOptions });
853
823
  return this.ssmClient;
854
824
  }
855
825
  /**
@@ -862,10 +832,7 @@ var AwsClients = class {
862
832
  * Get CloudFront client
863
833
  */
864
834
  getCloudFrontClient() {
865
- if (!this.cloudFrontClient) this.cloudFrontClient = new CloudFrontClient({
866
- ...this.config.region && { region: this.config.region },
867
- ...this.config.credentials && { credentials: this.config.credentials }
868
- });
835
+ if (!this.cloudFrontClient) this.cloudFrontClient = new CloudFrontClient({ ...this.clientOptions });
869
836
  return this.cloudFrontClient;
870
837
  }
871
838
  /**
@@ -882,10 +849,7 @@ var AwsClients = class {
882
849
  * users must place their certificate stack in `us-east-1`.
883
850
  */
884
851
  getACMClient() {
885
- if (!this.acmClient) this.acmClient = new ACMClient({
886
- ...this.config.region && { region: this.config.region },
887
- ...this.config.credentials && { credentials: this.config.credentials }
888
- });
852
+ if (!this.acmClient) this.acmClient = new ACMClient({ ...this.clientOptions });
889
853
  return this.acmClient;
890
854
  }
891
855
  /**
@@ -904,10 +868,7 @@ var AwsClients = class {
904
868
  * in the same region.
905
869
  */
906
870
  getLambdaMicrovmsClient() {
907
- if (!this.lambdaMicrovmsClient) this.lambdaMicrovmsClient = new LambdaMicrovmsClient({
908
- ...this.config.region && { region: this.config.region },
909
- ...this.config.credentials && { credentials: this.config.credentials }
910
- });
871
+ if (!this.lambdaMicrovmsClient) this.lambdaMicrovmsClient = new LambdaMicrovmsClient({ ...this.clientOptions });
911
872
  return this.lambdaMicrovmsClient;
912
873
  }
913
874
  /**
@@ -920,10 +881,7 @@ var AwsClients = class {
920
881
  * Get CloudWatch client
921
882
  */
922
883
  getCloudWatchClient() {
923
- if (!this.cloudWatchClient) this.cloudWatchClient = new CloudWatchClient({
924
- ...this.config.region && { region: this.config.region },
925
- ...this.config.credentials && { credentials: this.config.credentials }
926
- });
884
+ if (!this.cloudWatchClient) this.cloudWatchClient = new CloudWatchClient({ ...this.clientOptions });
927
885
  return this.cloudWatchClient;
928
886
  }
929
887
  /**
@@ -936,10 +894,7 @@ var AwsClients = class {
936
894
  * Get CloudWatch Logs client
937
895
  */
938
896
  getCloudWatchLogsClient() {
939
- if (!this.cloudWatchLogsClient) this.cloudWatchLogsClient = new CloudWatchLogsClient({
940
- ...this.config.region && { region: this.config.region },
941
- ...this.config.credentials && { credentials: this.config.credentials }
942
- });
897
+ if (!this.cloudWatchLogsClient) this.cloudWatchLogsClient = new CloudWatchLogsClient({ ...this.clientOptions });
943
898
  return this.cloudWatchLogsClient;
944
899
  }
945
900
  /**
@@ -952,10 +907,7 @@ var AwsClients = class {
952
907
  * Get BedrockAgentCoreControl client
953
908
  */
954
909
  getBedrockAgentCoreControlClient() {
955
- if (!this.bedrockAgentCoreControlClient) this.bedrockAgentCoreControlClient = new BedrockAgentCoreControlClient({
956
- ...this.config.region && { region: this.config.region },
957
- ...this.config.credentials && { credentials: this.config.credentials }
958
- });
910
+ if (!this.bedrockAgentCoreControlClient) this.bedrockAgentCoreControlClient = new BedrockAgentCoreControlClient({ ...this.clientOptions });
959
911
  return this.bedrockAgentCoreControlClient;
960
912
  }
961
913
  /**
@@ -5257,6 +5209,50 @@ function shouldRetainResource(deletionPolicy) {
5257
5209
  return deletionPolicy === "Retain" || deletionPolicy === "RetainExceptOnCreate";
5258
5210
  }
5259
5211
 
5212
+ //#endregion
5213
+ //#region src/types/rollback-journal.ts
5214
+ /**
5215
+ * Journal format version, INDEPENDENT of the state schema. An unknown value
5216
+ * on read is a hard error telling the user to upgrade cdkd (forward-compat
5217
+ * guard, mirrors state-schema handling).
5218
+ */
5219
+ const ROLLBACK_JOURNAL_VERSION = 1;
5220
+ /** Thrown when a journal's `journalVersion` is newer than this binary knows. */
5221
+ var UnknownRollbackJournalVersionError = class extends Error {
5222
+ foundVersion;
5223
+ stackName;
5224
+ constructor(foundVersion, stackName) {
5225
+ super(`Rollback journal for '${stackName}' has journalVersion ${foundVersion}, but this cdkd only understands up to ${1}. Upgrade cdkd to roll this stack back.`);
5226
+ this.name = "UnknownRollbackJournalVersionError";
5227
+ this.foundVersion = foundVersion;
5228
+ this.stackName = stackName;
5229
+ }
5230
+ };
5231
+ /**
5232
+ * Parse + validate a journal body. Throws
5233
+ * {@link UnknownRollbackJournalVersionError} on a newer version, and a plain
5234
+ * Error on a structurally-invalid body.
5235
+ */
5236
+ function parseRollbackJournal(bodyString, stackName) {
5237
+ let parsed;
5238
+ try {
5239
+ parsed = JSON.parse(bodyString);
5240
+ } catch (err) {
5241
+ throw new Error(`Rollback journal for '${stackName}' is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
5242
+ }
5243
+ if (typeof parsed !== "object" || parsed === null) throw new Error(`Rollback journal for '${stackName}' is malformed (not an object).`);
5244
+ const j = parsed;
5245
+ if (typeof j.journalVersion !== "number" || j.journalVersion < 1) throw new Error(`Rollback journal for '${stackName}' has an invalid 'journalVersion' (${String(j.journalVersion)}).`);
5246
+ if (j.journalVersion > 1) throw new UnknownRollbackJournalVersionError(j.journalVersion, stackName);
5247
+ if (!Array.isArray(j.segments)) throw new Error(`Rollback journal for '${stackName}' is missing a 'segments' array.`);
5248
+ return {
5249
+ journalVersion: j.journalVersion,
5250
+ stackName: j.stackName ?? stackName,
5251
+ region: j.region ?? "",
5252
+ segments: j.segments
5253
+ };
5254
+ }
5255
+
5260
5256
  //#endregion
5261
5257
  //#region src/utils/bucket-region-client.ts
5262
5258
  /**
@@ -5390,6 +5386,16 @@ var S3StateBackend = class {
5390
5386
  return `${this.config.prefix}/${stackName}/state.json`;
5391
5387
  }
5392
5388
  /**
5389
+ * Get the rollback-journal S3 key — a sibling of `state.json` (issue
5390
+ * #1183). Only the region-scoped layout is used: journals are new objects
5391
+ * only ever written by journal-aware binaries, and the deploy failure path
5392
+ * migrates legacy-layout state before the journal write, so a legacy-key
5393
+ * journal can never exist.
5394
+ */
5395
+ getRollbackJournalKey(stackName, region) {
5396
+ return `${this.config.prefix}/${stackName}/${region}/rollback-journal.json`;
5397
+ }
5398
+ /**
5393
5399
  * Resolve the state bucket's actual region and, if it differs from the
5394
5400
  * client's currently-configured region, replace the S3Client with one
5395
5401
  * pointed at the bucket's region.
@@ -5615,6 +5621,7 @@ var S3StateBackend = class {
5615
5621
  }));
5616
5622
  this.logger.debug(`Deleted legacy state for stack: ${stackName}`);
5617
5623
  }
5624
+ await this.deleteRollbackJournal(stackName, region);
5618
5625
  this.logger.debug(`State deleted: ${stackName} (${region})`);
5619
5626
  } catch (error) {
5620
5627
  const normalized = normalizeAwsError(error, {
@@ -5787,6 +5794,70 @@ var S3StateBackend = class {
5787
5794
  if (failures.length > 0) throw new StateError(`Failed to delete ${failures.length} object(s) from bucket '${this.config.bucket}': ${failures.join("; ")}`);
5788
5795
  }
5789
5796
  /**
5797
+ * Load the rollback journal for a stack (issue #1183). Returns `null` when
5798
+ * no journal exists (the common case — a journal only lives between a
5799
+ * failed/interrupted deploy and its `cdkd rollback`). Throws
5800
+ * {@link UnknownRollbackJournalVersionError} on a newer-version journal.
5801
+ */
5802
+ async loadRollbackJournal(stackName, region) {
5803
+ const body = await this.getRawObject(this.getRollbackJournalKey(stackName, region));
5804
+ if (body === null) return null;
5805
+ return parseRollbackJournal(body, stackName);
5806
+ }
5807
+ /**
5808
+ * Append one segment to the stack's rollback journal, creating it if
5809
+ * absent. Existing segments are preserved (never overwritten) so
5810
+ * consecutive failed deploys accumulate one segment each. Every writer
5811
+ * holds the stack lock, so no optimistic locking is needed.
5812
+ */
5813
+ async appendRollbackJournalSegment(stackName, region, segment) {
5814
+ const journal = await this.loadRollbackJournal(stackName, region) ?? {
5815
+ journalVersion: 1,
5816
+ stackName,
5817
+ region,
5818
+ segments: []
5819
+ };
5820
+ journal.segments.push(segment);
5821
+ await this.putRawObject(this.getRollbackJournalKey(stackName, region), JSON.stringify(journal, null, 2));
5822
+ }
5823
+ /**
5824
+ * Pop the newest segment off the stack's rollback journal after it has
5825
+ * been fully replayed. When the last segment is removed, the journal
5826
+ * object is deleted entirely. Returns the number of segments remaining.
5827
+ */
5828
+ async popRollbackJournalSegment(stackName, region) {
5829
+ const journal = await this.loadRollbackJournal(stackName, region);
5830
+ if (!journal || journal.segments.length === 0) {
5831
+ await this.deleteRollbackJournal(stackName, region);
5832
+ return 0;
5833
+ }
5834
+ journal.segments.pop();
5835
+ if (journal.segments.length === 0) {
5836
+ await this.deleteRollbackJournal(stackName, region);
5837
+ return 0;
5838
+ }
5839
+ await this.putRawObject(this.getRollbackJournalKey(stackName, region), JSON.stringify(journal, null, 2));
5840
+ return journal.segments.length;
5841
+ }
5842
+ /**
5843
+ * Delete the stack's rollback journal object (idempotent). Called on the
5844
+ * deploy success path, after a clean rollback, and via {@link deleteState}
5845
+ * so `cdkd destroy` / `cdkd state destroy` sweep it too.
5846
+ */
5847
+ async deleteRollbackJournal(stackName, region) {
5848
+ await this.ensureClientForBucket();
5849
+ try {
5850
+ await this.s3Client.send(new DeleteObjectCommand({
5851
+ Bucket: this.config.bucket,
5852
+ ...await this.ownerParam(),
5853
+ Key: this.getRollbackJournalKey(stackName, region)
5854
+ }));
5855
+ } catch (error) {
5856
+ if (isNoSuchKey(error) || error.name === "NotFound") return;
5857
+ this.logger.warn(`Failed to delete rollback journal for '${stackName}' (${region}): ${error instanceof Error ? error.message : String(error)}`);
5858
+ }
5859
+ }
5860
+ /**
5790
5861
  * HeadObject probe — returns true on 200, false on NotFound. Other errors
5791
5862
  * propagate so we don't accidentally swallow IAM denials.
5792
5863
  */
@@ -11169,7 +11240,7 @@ var CloudControlProvider = class {
11169
11240
  this.logger.debug(`Deleting resource ${logicalId} (${resourceType}), physical ID: ${physicalId}`);
11170
11241
  if (context?.removeProtection === true && resourceType === "AWS::AutoScaling::AutoScalingGroup") {
11171
11242
  this.logger.debug(`Delegating protected AutoScalingGroup ${logicalId} delete to the SDK ASGProvider (Cloud Control cannot force-delete a protected ASG)`);
11172
- const { ASGProvider } = await import("./asg-provider-D85MbFl6.js").then((n) => n.n);
11243
+ const { ASGProvider } = await import("./asg-provider-DF1bV_pu.js").then((n) => n.n);
11173
11244
  await new ASGProvider().delete(logicalId, physicalId, resourceType, _properties, context);
11174
11245
  return;
11175
11246
  }
@@ -16332,6 +16403,763 @@ async function withResourceDeadline(operation, opts) {
16332
16403
  });
16333
16404
  }
16334
16405
 
16406
+ //#endregion
16407
+ //#region src/deployment/rollback-executor.ts
16408
+ /**
16409
+ * True when the op recorded a replacement (old physical id differs from the
16410
+ * new one). The old physical resource is already gone / orphaned, so an
16411
+ * in-place revert is best-effort — the plan labels these explicitly.
16412
+ */
16413
+ function isReplacementOp(op) {
16414
+ return op.changeType === "UPDATE" && op.previousState?.physicalId !== void 0 && op.previousState.physicalId !== op.physicalId;
16415
+ }
16416
+ function deepEqual(a, b) {
16417
+ if (a === b) return true;
16418
+ if (a == null || b == null) return a === b;
16419
+ if (typeof a !== typeof b) return false;
16420
+ if (typeof a !== "object") return false;
16421
+ if (Array.isArray(a) || Array.isArray(b)) {
16422
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
16423
+ return a.every((v, i) => deepEqual(v, b[i]));
16424
+ }
16425
+ const ao = a;
16426
+ const bo = b;
16427
+ const ak = Object.keys(ao);
16428
+ if (ak.length !== Object.keys(bo).length) return false;
16429
+ for (const k of ak) {
16430
+ if (!Object.prototype.hasOwnProperty.call(bo, k)) return false;
16431
+ if (!deepEqual(ao[k], bo[k])) return false;
16432
+ }
16433
+ return true;
16434
+ }
16435
+ /**
16436
+ * Classify what a single op WILL do against the current state, without
16437
+ * touching AWS. Pure — used both by the command's plan preview and by the
16438
+ * replayer (which re-derives the action to stay in lock-step with the
16439
+ * plan). `orphanLogicalIds` mirrors `cdk rollback --orphan`.
16440
+ */
16441
+ function classifyRollbackOp(op, stateResources, orphanLogicalIds) {
16442
+ const replacement = isReplacementOp(op);
16443
+ if (op.changeType === "DELETE") return "unrecoverable-delete";
16444
+ if (orphanLogicalIds.has(op.logicalId)) return "orphan-flag";
16445
+ if (op.changeType === "CREATE") {
16446
+ const current = stateResources[op.logicalId];
16447
+ if (!current) return "skip-already-done";
16448
+ if (op.physicalId !== void 0 && current.physicalId !== op.physicalId) return "skip-mismatch";
16449
+ const policy = current.deletionPolicy;
16450
+ if (policy === "Retain" || policy === "Snapshot") return "orphan-retain";
16451
+ return "delete";
16452
+ }
16453
+ const current = stateResources[op.logicalId];
16454
+ if (!current) return "skip-absent";
16455
+ if (op.previousState && deepEqual(current.properties, op.previousState.properties)) {
16456
+ if (!replacement) return "skip-already-done";
16457
+ }
16458
+ return "revert";
16459
+ }
16460
+ /**
16461
+ * Build the full ordered plan for a list of ops (one segment). Mirrors the
16462
+ * replay order: UPDATE/DELETE first (reverse completion order), then CREATE
16463
+ * deletions in dependency-aware order.
16464
+ */
16465
+ function planRollback(operations, stateResources, orphanLogicalIds = /* @__PURE__ */ new Set()) {
16466
+ const { createOps, otherOps } = partitionOps(operations);
16467
+ return [...[...otherOps].reverse(), ...sortRollbackCreates(createOps, stateResources)].map((op) => ({
16468
+ op,
16469
+ action: classifyRollbackOp(op, stateResources, orphanLogicalIds),
16470
+ replacement: isReplacementOp(op)
16471
+ }));
16472
+ }
16473
+ function partitionOps(operations) {
16474
+ const createOps = [];
16475
+ const otherOps = [];
16476
+ for (const op of operations) if (op.changeType === "CREATE") createOps.push(op);
16477
+ else otherOps.push(op);
16478
+ return {
16479
+ createOps,
16480
+ otherOps
16481
+ };
16482
+ }
16483
+ /**
16484
+ * Replay a list of completed operations against `stateResources` (mutated in
16485
+ * place), reverting each. Best-effort: a provider failure is caught, warned,
16486
+ * and counted; replay continues.
16487
+ *
16488
+ * - UPDATE / DELETE first (reverse completion order), then CREATE deletions
16489
+ * in reverse dependency order (dependents deleted before dependencies).
16490
+ * - `afterOp` is invoked after each op that MUTATED state (so the command can
16491
+ * persist state incrementally, mirroring `saveStateAfterResource`). The
16492
+ * in-process engine passes no `afterOp` and saves state once at the end.
16493
+ * - `isInterrupted` is polled between ops; when it flips true, replay stops
16494
+ * (the pending op is left for a re-run).
16495
+ */
16496
+ async function replayRollback(operations, stateResources, stackName, ctx, options = {}) {
16497
+ const orphanLogicalIds = options.orphanLogicalIds ?? /* @__PURE__ */ new Set();
16498
+ const result = {
16499
+ failures: 0,
16500
+ warnings: 0,
16501
+ interrupted: false
16502
+ };
16503
+ if (operations.length === 0) {
16504
+ ctx.logger.info("No completed operations to roll back.");
16505
+ return result;
16506
+ }
16507
+ ctx.logger.info(`Rolling back ${operations.length} completed operation(s)...`);
16508
+ ctx.recordEvent?.({
16509
+ eventType: "ROLLBACK_STARTED",
16510
+ stackName
16511
+ });
16512
+ const { createOps, otherOps } = partitionOps(operations);
16513
+ for (let i = otherOps.length - 1; i >= 0; i--) {
16514
+ if (options.isInterrupted?.()) {
16515
+ result.interrupted = true;
16516
+ break;
16517
+ }
16518
+ await replaySingle(otherOps[i], stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp);
16519
+ }
16520
+ if (!result.interrupted && createOps.length > 0) {
16521
+ const sorted = sortRollbackCreates(createOps, stateResources);
16522
+ for (const op of sorted) {
16523
+ if (options.isInterrupted?.()) {
16524
+ result.interrupted = true;
16525
+ break;
16526
+ }
16527
+ await replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, options.afterOp);
16528
+ }
16529
+ }
16530
+ ctx.logger.info("Rollback completed. Some resources may remain if deletion failed.");
16531
+ ctx.recordEvent?.({
16532
+ eventType: "ROLLBACK_FINISHED",
16533
+ stackName
16534
+ });
16535
+ return result;
16536
+ }
16537
+ async function replaySingle(op, stateResources, stackName, ctx, orphanLogicalIds, result, afterOp) {
16538
+ const action = classifyRollbackOp(op, stateResources, orphanLogicalIds);
16539
+ const { logger } = ctx;
16540
+ try {
16541
+ switch (action) {
16542
+ case "unrecoverable-delete":
16543
+ logger.warn(` Rollback: Cannot restore deleted resource ${op.logicalId} (${op.resourceType}) — resource has already been deleted`);
16544
+ result.warnings++;
16545
+ return;
16546
+ case "skip-already-done":
16547
+ logger.debug(` Rollback: ${op.logicalId} already reverted, skipping`);
16548
+ return;
16549
+ case "skip-mismatch":
16550
+ logger.warn(` Rollback: Skipping ${op.logicalId} — its physical id changed since the failed deploy (replaced by a later attempt); manual attention may be required`);
16551
+ result.warnings++;
16552
+ return;
16553
+ case "skip-absent":
16554
+ logger.warn(` Rollback: Cannot restore ${op.logicalId} — resource no longer in state, skipping`);
16555
+ result.warnings++;
16556
+ return;
16557
+ case "orphan-flag":
16558
+ if (op.changeType === "CREATE") {
16559
+ delete stateResources[op.logicalId];
16560
+ logger.info(` Rollback: Orphaning created resource ${op.logicalId} (--orphan)`);
16561
+ await afterOp?.(op.logicalId);
16562
+ } else logger.info(` Rollback: Leaving ${op.logicalId} at its new state (--orphan)`);
16563
+ return;
16564
+ case "orphan-retain":
16565
+ delete stateResources[op.logicalId];
16566
+ logger.info(` Rollback: Leaving ${op.logicalId} (${op.resourceType}) in AWS (DeletionPolicy: ${stateResourcesPolicyLabel(op, stateResources)}) — removed from state`);
16567
+ await afterOp?.(op.logicalId);
16568
+ ctx.recordEvent?.({
16569
+ eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
16570
+ stackName,
16571
+ operation: "CREATE",
16572
+ logicalId: op.logicalId,
16573
+ resourceType: op.resourceType,
16574
+ ...op.provisionedBy && { provisionedBy: op.provisionedBy }
16575
+ });
16576
+ return;
16577
+ case "delete": {
16578
+ if (!op.physicalId) {
16579
+ logger.warn(` Rollback: Cannot delete ${op.logicalId} — no physical ID recorded`);
16580
+ result.warnings++;
16581
+ return;
16582
+ }
16583
+ logger.info(` Rollback: Deleting created resource ${op.logicalId} (${op.resourceType})`);
16584
+ const { provider } = ctx.providerRegistry.getProviderFor({
16585
+ resourceType: op.resourceType,
16586
+ provisionedBy: op.provisionedBy
16587
+ });
16588
+ await provider.delete(op.logicalId, op.physicalId, op.resourceType, op.properties, { expectedRegion: ctx.region });
16589
+ delete stateResources[op.logicalId];
16590
+ logger.info(` Rollback: ${op.logicalId} deleted successfully`);
16591
+ await afterOp?.(op.logicalId);
16592
+ ctx.recordEvent?.({
16593
+ eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
16594
+ stackName,
16595
+ operation: "CREATE",
16596
+ logicalId: op.logicalId,
16597
+ resourceType: op.resourceType,
16598
+ ...op.provisionedBy && { provisionedBy: op.provisionedBy }
16599
+ });
16600
+ return;
16601
+ }
16602
+ case "revert": {
16603
+ if (!op.previousState) {
16604
+ logger.warn(` Rollback: Cannot restore ${op.logicalId} — no previous state available`);
16605
+ result.warnings++;
16606
+ return;
16607
+ }
16608
+ const current = stateResources[op.logicalId];
16609
+ if (!current) {
16610
+ logger.warn(` Rollback: Cannot restore ${op.logicalId} — resource not found in current state`);
16611
+ result.warnings++;
16612
+ return;
16613
+ }
16614
+ logger.info(` Rollback: Restoring ${op.logicalId} (${op.resourceType}) to previous state`);
16615
+ const { provider } = ctx.providerRegistry.getProviderFor({
16616
+ resourceType: op.resourceType,
16617
+ provisionedBy: op.provisionedBy
16618
+ });
16619
+ await provider.update(op.logicalId, current.physicalId, op.resourceType, op.previousState.properties, current.properties);
16620
+ stateResources[op.logicalId] = op.previousState;
16621
+ logger.info(` Rollback: ${op.logicalId} restored successfully`);
16622
+ await afterOp?.(op.logicalId);
16623
+ ctx.recordEvent?.({
16624
+ eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
16625
+ stackName,
16626
+ operation: "UPDATE",
16627
+ logicalId: op.logicalId,
16628
+ resourceType: op.resourceType,
16629
+ ...op.provisionedBy && { provisionedBy: op.provisionedBy }
16630
+ });
16631
+ return;
16632
+ }
16633
+ }
16634
+ } catch (rollbackError) {
16635
+ logger.warn(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
16636
+ logger.warn(" Continuing with remaining rollback operations...");
16637
+ result.failures++;
16638
+ ctx.recordEvent?.({
16639
+ eventType: "ROLLBACK_RESOURCE_FAILED",
16640
+ stackName,
16641
+ operation: op.changeType,
16642
+ logicalId: op.logicalId,
16643
+ resourceType: op.resourceType,
16644
+ ...op.provisionedBy && { provisionedBy: op.provisionedBy },
16645
+ error: extractDeploymentEventError(rollbackError)
16646
+ });
16647
+ }
16648
+ }
16649
+ function stateResourcesPolicyLabel(op, stateResources) {
16650
+ return stateResources[op.logicalId]?.deletionPolicy ?? "Retain";
16651
+ }
16652
+ /**
16653
+ * Sort CREATE rollback operations so that resources depending on others are
16654
+ * deleted first (reverse dependency order), using state dependencies. Same
16655
+ * algorithm as the pre-extraction `DeployEngine.sortRollbackCreates`.
16656
+ */
16657
+ function sortRollbackCreates(createOps, stateResources, logger) {
16658
+ const opMap = /* @__PURE__ */ new Map();
16659
+ const deleteIds = /* @__PURE__ */ new Set();
16660
+ for (const op of createOps) {
16661
+ opMap.set(op.logicalId, op);
16662
+ deleteIds.add(op.logicalId);
16663
+ }
16664
+ const dependedBy = /* @__PURE__ */ new Map();
16665
+ for (const id of deleteIds) if (!dependedBy.has(id)) dependedBy.set(id, /* @__PURE__ */ new Set());
16666
+ for (const id of deleteIds) {
16667
+ const resource = stateResources[id];
16668
+ if (!resource?.dependencies) continue;
16669
+ for (const dep of resource.dependencies) {
16670
+ if (!deleteIds.has(dep)) continue;
16671
+ if (!dependedBy.has(dep)) dependedBy.set(dep, /* @__PURE__ */ new Set());
16672
+ dependedBy.get(dep).add(id);
16673
+ }
16674
+ }
16675
+ const sorted = [];
16676
+ let remaining = new Set(deleteIds);
16677
+ while (remaining.size > 0) {
16678
+ const level = [];
16679
+ for (const id of remaining) {
16680
+ const dependents = dependedBy.get(id);
16681
+ if (!(dependents ? [...dependents].some((d) => remaining.has(d)) : false)) level.push(id);
16682
+ }
16683
+ if (level.length === 0) {
16684
+ logger?.warn(`Circular dependency detected in rollback order, processing remaining ${remaining.size} resources`);
16685
+ for (const id of remaining) {
16686
+ const op = opMap.get(id);
16687
+ if (op) sorted.push(op);
16688
+ }
16689
+ break;
16690
+ }
16691
+ for (const id of level) {
16692
+ const op = opMap.get(id);
16693
+ if (op) sorted.push(op);
16694
+ }
16695
+ remaining = new Set([...remaining].filter((id) => !level.includes(id)));
16696
+ }
16697
+ logger?.debug(`Rollback CREATE deletion order: ${sorted.map((op) => op.logicalId).join(" → ")}`);
16698
+ return sorted;
16699
+ }
16700
+
16701
+ //#endregion
16702
+ //#region src/state/deployment-events-store.ts
16703
+ /**
16704
+ * Deployment-events store (issue #808) — persists the structured
16705
+ * deployment events emitted by the deploy engine / destroy runner as
16706
+ * JSONL to the state bucket, plus a small per-stack run index:
16707
+ *
16708
+ * - Events: `{prefix}/{stackName}/{region}/deployments/{runId}.jsonl`
16709
+ * - Index: `{prefix}/{stackName}/{region}/deployments/index.json`
16710
+ *
16711
+ * Design constraints (all load-bearing):
16712
+ *
16713
+ * - **Best-effort, never blocking**: `record()` is synchronous and only
16714
+ * buffers in memory; flushes run asynchronously (debounced timer +
16715
+ * size threshold) and are serialized on a write chain. A failed S3
16716
+ * write warns once and degrades to debug-level afterwards — it can
16717
+ * NEVER fail or block the deploy/destroy itself.
16718
+ * - **No locking**: each run writes to its own unique `{runId}.jsonl`
16719
+ * key (no concurrent writer by construction). `index.json` is
16720
+ * last-writer-wins — acceptable for a derived view; the `.jsonl`
16721
+ * files are the source of truth and `cdkd events` can read a run
16722
+ * directly by id even if the index lost the race.
16723
+ * - **No resource properties** in events — errors + metadata only
16724
+ * (properties may contain secrets and already live in state.json).
16725
+ * - **Separate keys from state.json** — no state schema bump; event
16726
+ * files survive `cdkd destroy` (state deletion does not touch
16727
+ * `deployments/`), preserving post-mortem context.
16728
+ * - **Bounded growth** (issue #885): `finalize()` prunes `{runId}.jsonl`
16729
+ * streams that fell out of the index window so the `deployments/`
16730
+ * prefix stays bounded; `cdkd events prune` (via
16731
+ * {@link DeploymentEventsReader.pruneRuns}) is the explicit purge.
16732
+ */
16733
+ /** Max runs retained in `deployments/index.json` (newest first). */
16734
+ const DEPLOYMENT_EVENTS_MAX_INDEX_RUNS = 20;
16735
+ /** Debounce window between buffered events and the async S3 flush. */
16736
+ const FLUSH_INTERVAL_MS = 2e3;
16737
+ /** Flush immediately once this many events are buffered. */
16738
+ const FLUSH_EVENT_THRESHOLD = 50;
16739
+ /** Build-time cdkd version, with a dev fallback for non-built contexts. */
16740
+ function getCdkdVersion() {
16741
+ return "0.263.0";
16742
+ }
16743
+ /**
16744
+ * Generate a time-sortable unique run id, e.g.
16745
+ * `20260613T012345678Z-1a2b3c4d`. The timestamp prefix keeps S3 listings
16746
+ * and `cdkd events` output chronologically meaningful; the random suffix
16747
+ * guarantees uniqueness across concurrent runs.
16748
+ */
16749
+ function newDeploymentRunId(now = /* @__PURE__ */ new Date()) {
16750
+ return `${now.toISOString().replace(/[-:.]/g, "")}-${randomUUID().slice(0, 8)}`;
16751
+ }
16752
+ /** S3 key of a run's JSONL event stream. */
16753
+ function deploymentEventsKey(prefix, stackName, region, runId) {
16754
+ return `${prefix}/${stackName}/${region}/deployments/${runId}.jsonl`;
16755
+ }
16756
+ /** S3 key of a stack's run index. */
16757
+ function deploymentEventsIndexKey(prefix, stackName, region) {
16758
+ return `${prefix}/${stackName}/${region}/deployments/index.json`;
16759
+ }
16760
+ /** S3 key prefix under which a stack's `deployments/` artifacts live. */
16761
+ function deploymentsDirPrefix(prefix, stackName, region) {
16762
+ return `${prefix}/${stackName}/${region}/deployments/`;
16763
+ }
16764
+ /**
16765
+ * Parse the wall-clock time encoded in a run id's leading timestamp
16766
+ * (e.g. `20260613T012345678Z-1a2b3c4d`) back to epoch milliseconds.
16767
+ * Returns `null` for any id that does not start with the canonical
16768
+ * compact-ISO prefix — the age-based pruner treats an unparseable id as
16769
+ * "do not delete on age grounds", which is the safe direction.
16770
+ */
16771
+ function runIdTimestampMs(runId) {
16772
+ const m = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(\d{3})Z/.exec(runId);
16773
+ if (!m) return null;
16774
+ const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}.${m[7]}Z`;
16775
+ const ms = Date.parse(iso);
16776
+ return Number.isNaN(ms) ? null : ms;
16777
+ }
16778
+ /** Extract the `{runId}` from a `.../{runId}.jsonl` event-stream key. */
16779
+ function runIdFromJsonlKey(key, dirPrefix) {
16780
+ if (!key.startsWith(dirPrefix) || !key.endsWith(".jsonl")) return null;
16781
+ return key.slice(dirPrefix.length, -6);
16782
+ }
16783
+ /**
16784
+ * Buffering JSONL writer for one deployment run. Implements the
16785
+ * {@link DeploymentEventRecorder} seam the deploy engine / destroy
16786
+ * runner emit through.
16787
+ */
16788
+ var DeploymentEventsStore = class {
16789
+ logger = getLogger().child("DeploymentEvents");
16790
+ backend;
16791
+ stackName;
16792
+ region;
16793
+ command;
16794
+ runId;
16795
+ cdkdVersion;
16796
+ startedAt;
16797
+ /** All events recorded so far (the full JSONL body is re-PUT per flush —
16798
+ * S3 has no append, and metadata-only events are small). */
16799
+ events = [];
16800
+ /** Number of events already persisted by the last successful flush. */
16801
+ persistedCount = 0;
16802
+ flushTimer;
16803
+ /** Serializes S3 writes so flushes never interleave. */
16804
+ writeChain = Promise.resolve();
16805
+ warnedOnce = false;
16806
+ finalized = false;
16807
+ constructor(backend, options) {
16808
+ this.backend = backend;
16809
+ this.stackName = options.stackName;
16810
+ this.region = options.region;
16811
+ this.command = options.command;
16812
+ this.runId = options.runId ?? newDeploymentRunId();
16813
+ this.cdkdVersion = options.cdkdVersion ?? getCdkdVersion();
16814
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
16815
+ }
16816
+ /**
16817
+ * Buffer one event (synchronous, never throws). The timestamp is
16818
+ * stamped here so emitters don't need to.
16819
+ */
16820
+ record(event) {
16821
+ try {
16822
+ if (this.finalized) return;
16823
+ this.events.push({
16824
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
16825
+ ...event
16826
+ });
16827
+ if (this.events.length - this.persistedCount >= FLUSH_EVENT_THRESHOLD) this.scheduleFlush(0);
16828
+ else this.scheduleFlush(FLUSH_INTERVAL_MS);
16829
+ } catch {}
16830
+ }
16831
+ /**
16832
+ * Final flush + index update. Called by the owner (deploy CLI per-stack
16833
+ * finally / destroy runner finally) after the run reaches a terminal
16834
+ * state. Best-effort: never throws.
16835
+ */
16836
+ async finalize(result) {
16837
+ if (this.finalized) return;
16838
+ this.finalized = true;
16839
+ if (this.flushTimer) {
16840
+ clearTimeout(this.flushTimer);
16841
+ this.flushTimer = void 0;
16842
+ }
16843
+ if (this.events.length === 0) return;
16844
+ await this.enqueueWrite(async () => {
16845
+ await this.doFlush();
16846
+ const keptRunIds = await this.updateIndex(result);
16847
+ await this.pruneSupersededRunFiles(keptRunIds);
16848
+ });
16849
+ }
16850
+ /** Await any in-flight async flushes (used by tests). */
16851
+ async drain() {
16852
+ await this.writeChain;
16853
+ }
16854
+ scheduleFlush(delayMs) {
16855
+ if (this.flushTimer) {
16856
+ if (delayMs > 0) return;
16857
+ clearTimeout(this.flushTimer);
16858
+ }
16859
+ this.flushTimer = setTimeout(() => {
16860
+ this.flushTimer = void 0;
16861
+ this.enqueueWrite(() => this.doFlush());
16862
+ }, delayMs);
16863
+ this.flushTimer.unref?.();
16864
+ }
16865
+ enqueueWrite(op) {
16866
+ const next = this.writeChain.then(op).catch((err) => {
16867
+ this.warnOnce(`Failed to persist deployment events for run ${this.runId}: ${err instanceof Error ? err.message : String(err)}`);
16868
+ });
16869
+ this.writeChain = next;
16870
+ return next;
16871
+ }
16872
+ async doFlush() {
16873
+ if (this.events.length === 0 || this.events.length === this.persistedCount) return;
16874
+ const snapshotCount = this.events.length;
16875
+ const body = this.events.slice(0, snapshotCount).map((e) => JSON.stringify(e)).join("\n");
16876
+ await this.backend.putRawObject(deploymentEventsKey(this.backend.prefix, this.stackName, this.region, this.runId), body + "\n", "application/x-ndjson");
16877
+ this.persistedCount = snapshotCount;
16878
+ }
16879
+ /**
16880
+ * Prepend this run's summary to `deployments/index.json`, truncated to
16881
+ * the last {@link DEPLOYMENT_EVENTS_MAX_INDEX_RUNS} runs. Read-modify-
16882
+ * write WITHOUT optimistic locking — last-writer-wins (documented
16883
+ * trade-off; the per-run `.jsonl` files are the source of truth).
16884
+ *
16885
+ * Returns the run ids retained in the index (newest-first), which the
16886
+ * caller feeds to {@link pruneSupersededRunFiles} so the `.jsonl` files
16887
+ * stay bounded to the same window as the index.
16888
+ */
16889
+ async updateIndex(result) {
16890
+ const key = deploymentEventsIndexKey(this.backend.prefix, this.stackName, this.region);
16891
+ let existingRuns = [];
16892
+ try {
16893
+ const raw = await this.backend.getRawObject(key);
16894
+ if (raw !== null) {
16895
+ const parsed = JSON.parse(raw);
16896
+ if (Array.isArray(parsed.runs)) existingRuns = parsed.runs;
16897
+ }
16898
+ } catch (err) {
16899
+ this.logger.debug(`Deployment-events index unreadable, rewriting: ${err instanceof Error ? err.message : String(err)}`);
16900
+ }
16901
+ const runs = [{
16902
+ runId: this.runId,
16903
+ command: this.command,
16904
+ cdkdVersion: this.cdkdVersion,
16905
+ startedAt: this.startedAt,
16906
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
16907
+ result,
16908
+ eventCount: this.persistedCount
16909
+ }, ...existingRuns.filter((r) => r.runId !== this.runId)].slice(0, 20);
16910
+ const file = {
16911
+ indexVersion: 1,
16912
+ stackName: this.stackName,
16913
+ region: this.region,
16914
+ runs,
16915
+ lastModified: Date.now()
16916
+ };
16917
+ await this.backend.putRawObject(key, JSON.stringify(file, null, 2));
16918
+ return runs.map((r) => r.runId);
16919
+ }
16920
+ /**
16921
+ * Self-bounding prune (issue #885): delete `{runId}.jsonl` streams that
16922
+ * have fallen out of the index window so the `deployments/` prefix does
16923
+ * not grow without bound. Best-effort — runs inside the same write-chain
16924
+ * link as the flush + index write, so a failure here is caught + warned
16925
+ * once by {@link enqueueWrite} and never blocks the deploy / destroy.
16926
+ *
16927
+ * Concurrency-tolerant by construction: the cutoff is the OLDEST retained
16928
+ * run id, and only `.jsonl` files strictly below it are deleted. Run ids
16929
+ * are time-sortable, so a concurrent NEWER run's id sorts ABOVE every
16930
+ * retained id and is never touched. A concurrent run that STARTED earlier
16931
+ * but is still writing could in theory fall below the cutoff if 20+ newer
16932
+ * runs finalized while it ran — an extreme edge that self-heals anyway,
16933
+ * since the whole JSONL body is re-PUT on that run's next flush / finalize
16934
+ * (S3 has no append; each flush rewrites the full stream).
16935
+ */
16936
+ async pruneSupersededRunFiles(keptRunIds) {
16937
+ if (keptRunIds.length < 20) return;
16938
+ const cutoff = keptRunIds.reduce((min, id) => id < min ? id : min, keptRunIds[0]);
16939
+ const dirPrefix = deploymentsDirPrefix(this.backend.prefix, this.stackName, this.region);
16940
+ const stale = (await this.backend.listRawKeys(dirPrefix)).filter((k) => {
16941
+ const runId = runIdFromJsonlKey(k, dirPrefix);
16942
+ return runId !== null && runId < cutoff;
16943
+ });
16944
+ if (stale.length === 0) return;
16945
+ await this.backend.deleteRawObjects(stale);
16946
+ this.logger.debug(`Pruned ${stale.length} superseded deployment-event stream(s) for ${this.stackName} (${this.region})`);
16947
+ }
16948
+ warnOnce(message) {
16949
+ if (this.warnedOnce) {
16950
+ this.logger.debug(message);
16951
+ return;
16952
+ }
16953
+ this.warnedOnce = true;
16954
+ this.logger.warn(`${message} — continuing; deployment events are best-effort and never block the run.`);
16955
+ }
16956
+ };
16957
+ /**
16958
+ * Read side for `cdkd events`: discovers regions / runs / event streams
16959
+ * under `{prefix}/{stackName}/{region}/deployments/`. Region discovery
16960
+ * deliberately does NOT rely on state.json — event files survive
16961
+ * `cdkd destroy`, so a destroyed stack's runs stay readable.
16962
+ */
16963
+ var DeploymentEventsReader = class {
16964
+ backend;
16965
+ constructor(backend) {
16966
+ this.backend = backend;
16967
+ }
16968
+ /**
16969
+ * Regions that have a `deployments/` index or event stream for the
16970
+ * stack. Derived from the raw key listing under `{prefix}/{stackName}/`.
16971
+ */
16972
+ async listRegions(stackName) {
16973
+ const prefix = `${this.backend.prefix}/${stackName}/`;
16974
+ const keys = await this.backend.listRawKeys(prefix);
16975
+ const regions = /* @__PURE__ */ new Set();
16976
+ for (const key of keys) {
16977
+ const segments = key.slice(prefix.length).split("/");
16978
+ if (segments.length === 3 && segments[1] === "deployments" && segments[0]) regions.add(segments[0]);
16979
+ }
16980
+ return [...regions].sort();
16981
+ }
16982
+ /**
16983
+ * Run summaries for `(stackName, region)`, newest first. Returns the
16984
+ * index file's `runs` (already newest-first); when the index is missing
16985
+ * or unreadable, falls back to enumerating `{runId}.jsonl` keys (sorted
16986
+ * descending — runIds are time-prefixed).
16987
+ *
16988
+ * In the fallback path the run's terminal result / command / version are
16989
+ * NOT known from the key alone, so each run's JSONL is read and its last
16990
+ * `RUN_FINISHED` (+ first `RUN_STARTED`) event mined for the true result,
16991
+ * command, version, and timestamps. A run whose JSONL has no terminal
16992
+ * `RUN_FINISHED` (interrupted run, or one whose index write lost the
16993
+ * race) is reported as `'UNKNOWN'` — it is NEVER fabricated as `'FAILED'`,
16994
+ * which would mislabel a successful run that merely failed to update the
16995
+ * derived index.
16996
+ */
16997
+ async listRuns(stackName, region) {
16998
+ const key = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
16999
+ try {
17000
+ const raw = await this.backend.getRawObject(key);
17001
+ if (raw !== null) {
17002
+ const parsed = JSON.parse(raw);
17003
+ if (Array.isArray(parsed.runs)) return parsed.runs;
17004
+ }
17005
+ } catch {}
17006
+ const dirPrefix = `${this.backend.prefix}/${stackName}/${region}/deployments/`;
17007
+ const runIds = (await this.backend.listRawKeys(dirPrefix)).filter((k) => k.endsWith(".jsonl")).map((k) => k.slice(dirPrefix.length, -6)).sort().reverse();
17008
+ return Promise.all(runIds.map((runId) => this.summarizeRunFromJsonl(stackName, region, runId)));
17009
+ }
17010
+ /**
17011
+ * Reconstruct a {@link DeploymentRunSummary} for the index-fallback path
17012
+ * by reading the run's JSONL. Derives the true result from the last
17013
+ * `RUN_FINISHED` event; absent that, returns `'UNKNOWN'` rather than
17014
+ * fabricating a definitive failure.
17015
+ */
17016
+ async summarizeRunFromJsonl(stackName, region, runId) {
17017
+ const fallback = {
17018
+ runId,
17019
+ command: "deploy",
17020
+ cdkdVersion: "unknown",
17021
+ startedAt: "",
17022
+ finishedAt: "",
17023
+ result: "UNKNOWN",
17024
+ eventCount: 0
17025
+ };
17026
+ let events = null;
17027
+ try {
17028
+ events = await this.readRunEvents(stackName, region, runId);
17029
+ } catch {
17030
+ return fallback;
17031
+ }
17032
+ if (events === null || events.length === 0) return fallback;
17033
+ const started = events.find((e) => e.eventType === "RUN_STARTED");
17034
+ let finished;
17035
+ for (const e of events) if (e.eventType === "RUN_FINISHED") finished = e;
17036
+ return {
17037
+ runId,
17038
+ command: started?.command ?? fallback.command,
17039
+ cdkdVersion: started?.cdkdVersion ?? fallback.cdkdVersion,
17040
+ startedAt: started?.timestamp ?? fallback.startedAt,
17041
+ finishedAt: finished?.timestamp ?? fallback.finishedAt,
17042
+ result: finished?.result ?? "UNKNOWN",
17043
+ eventCount: events.length
17044
+ };
17045
+ }
17046
+ /**
17047
+ * Parse one run's JSONL event stream. Returns `null` when the run file
17048
+ * does not exist. Malformed lines are skipped (a torn final line from
17049
+ * an interrupted flush must not hide the rest of the stream).
17050
+ */
17051
+ async readRunEvents(stackName, region, runId) {
17052
+ const key = deploymentEventsKey(this.backend.prefix, stackName, region, runId);
17053
+ const raw = await this.backend.getRawObject(key);
17054
+ if (raw === null) return null;
17055
+ const events = [];
17056
+ for (const line of raw.split("\n")) {
17057
+ const trimmed = line.trim();
17058
+ if (!trimmed) continue;
17059
+ try {
17060
+ events.push(JSON.parse(trimmed));
17061
+ } catch {}
17062
+ }
17063
+ return events;
17064
+ }
17065
+ /**
17066
+ * Prune a stack's recorded deployment-event runs (issue #885 — `cdkd
17067
+ * events prune`). Deletes `{runId}.jsonl` streams beyond a retention
17068
+ * window and rewrites (or removes) `index.json` to match.
17069
+ *
17070
+ * Retention semantics (see {@link DeploymentEventsPruneOptions}):
17071
+ * - `all` — delete every run + the index (full purge).
17072
+ * - `keep N` — retain the newest N runs, delete the rest.
17073
+ * - `olderThanMs`— delete runs whose run-id timestamp is older than the
17074
+ * cutoff; a run id with no parseable timestamp is kept.
17075
+ * - `keep` + `olderThanMs` — a run is deleted only when it is BOTH
17076
+ * beyond the newest-N window AND older than the cutoff
17077
+ * (the most conservative combination).
17078
+ * - none of the above — defaults to `keep DEPLOYMENT_EVENTS_MAX_INDEX_RUNS`
17079
+ * (matches the index window the writer self-bounds to).
17080
+ *
17081
+ * Unlike the writer's best-effort auto-prune, this is user-initiated and
17082
+ * surfaces errors to the caller (the command reports / exits non-zero).
17083
+ */
17084
+ async pruneRuns(stackName, region, opts = {}) {
17085
+ const dirPrefix = deploymentsDirPrefix(this.backend.prefix, stackName, region);
17086
+ const runIdsDesc = (await this.backend.listRawKeys(dirPrefix)).map((k) => runIdFromJsonlKey(k, dirPrefix)).filter((id) => id !== null).sort().reverse();
17087
+ const indexKey = deploymentEventsIndexKey(this.backend.prefix, stackName, region);
17088
+ if (opts.all === true) {
17089
+ const toDelete = runIdsDesc.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
17090
+ await this.backend.deleteRawObjects([...toDelete, indexKey]);
17091
+ return {
17092
+ deletedRunIds: runIdsDesc,
17093
+ remainingRunIds: [],
17094
+ indexDeleted: true
17095
+ };
17096
+ }
17097
+ const noGuards = opts.keep === void 0 && opts.olderThanMs === void 0;
17098
+ const keep = opts.keep ?? (noGuards ? 20 : void 0);
17099
+ const protectedByCount = keep === void 0 ? /* @__PURE__ */ new Set() : new Set(runIdsDesc.slice(0, keep));
17100
+ let candidates = runIdsDesc.filter((id) => !protectedByCount.has(id));
17101
+ if (opts.olderThanMs !== void 0) {
17102
+ const cutoff = (opts.now ?? /* @__PURE__ */ new Date()).getTime() - opts.olderThanMs;
17103
+ candidates = candidates.filter((id) => {
17104
+ const t = runIdTimestampMs(id);
17105
+ return t !== null && t < cutoff;
17106
+ });
17107
+ }
17108
+ if (candidates.length === 0) return {
17109
+ deletedRunIds: [],
17110
+ remainingRunIds: runIdsDesc,
17111
+ indexDeleted: false
17112
+ };
17113
+ const deletedSet = new Set(candidates);
17114
+ const deleteKeys = candidates.map((id) => deploymentEventsKey(this.backend.prefix, stackName, region, id));
17115
+ const remainingRunIds = runIdsDesc.filter((id) => !deletedSet.has(id));
17116
+ const indexDeleted = await this.rewriteIndexAfterPrune(indexKey, stackName, region, deletedSet, remainingRunIds.length === 0);
17117
+ await this.backend.deleteRawObjects(deleteKeys);
17118
+ return {
17119
+ deletedRunIds: candidates,
17120
+ remainingRunIds,
17121
+ indexDeleted
17122
+ };
17123
+ }
17124
+ /**
17125
+ * Drop the pruned run ids from `index.json`, or delete the index entirely
17126
+ * when no `.jsonl` streams remain. A corrupt / unreadable index is left
17127
+ * untouched (the `.jsonl` files are the source of truth; `cdkd events`
17128
+ * falls back to key enumeration). Returns whether the index was deleted.
17129
+ */
17130
+ async rewriteIndexAfterPrune(indexKey, stackName, region, deletedRunIds, noRunsRemain) {
17131
+ if (noRunsRemain) {
17132
+ await this.backend.deleteRawObjects([indexKey]);
17133
+ return true;
17134
+ }
17135
+ let raw;
17136
+ try {
17137
+ raw = await this.backend.getRawObject(indexKey);
17138
+ } catch {
17139
+ return false;
17140
+ }
17141
+ if (raw === null) return false;
17142
+ let parsed;
17143
+ try {
17144
+ parsed = JSON.parse(raw);
17145
+ } catch {
17146
+ return false;
17147
+ }
17148
+ if (!Array.isArray(parsed.runs)) return false;
17149
+ const remaining = parsed.runs.filter((r) => !deletedRunIds.has(r.runId));
17150
+ if (remaining.length === parsed.runs.length) return false;
17151
+ const file = {
17152
+ indexVersion: 1,
17153
+ stackName,
17154
+ region,
17155
+ runs: remaining,
17156
+ lastModified: Date.now()
17157
+ };
17158
+ await this.backend.putRawObject(indexKey, JSON.stringify(file, null, 2));
17159
+ return false;
17160
+ }
17161
+ };
17162
+
16335
17163
  //#endregion
16336
17164
  //#region src/deployment/deploy-engine.ts
16337
17165
  /**
@@ -16738,6 +17566,10 @@ var DeployEngine = class {
16738
17566
  const currentEtag = currentStateData?.etag;
16739
17567
  const migrationPending = currentStateData?.migrationPending ?? false;
16740
17568
  this.logger.debug(`Loaded current state: ${Object.keys(currentState.resources).length} resources`);
17569
+ try {
17570
+ const journal = await this.stateBackend.loadRollbackJournal(stackName, this.stackRegion);
17571
+ if (journal && journal.segments.length > 0) this.logger.info(`A previous deploy of '${stackName}' failed or was interrupted. Run 'cdkd rollback ${stackName}' to revert it, or continue deploying to fix forward.`);
17572
+ } catch {}
16741
17573
  this.kickOffAutoRefreshObservedProperties(currentState.resources);
16742
17574
  this.logger.debug(`Template has ${Object.keys(template.Resources || {}).length} resources`);
16743
17575
  const parameterValues = await this.resolver.resolveParameters(template, this.options.parameters);
@@ -16843,6 +17675,7 @@ var DeployEngine = class {
16843
17675
  await this.drainObservedCaptures(newState.resources);
16844
17676
  const newEtag = await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(newState));
16845
17677
  this.logger.debug(`State saved (ETag: ${newEtag})`);
17678
+ await this.deleteRollbackJournalBestEffort(stackName);
16846
17679
  if (this.exportIndexStore) await this.exportIndexStore.updateForStack(stackName, this.stackRegion, newState.outputs ?? {});
16847
17680
  const durationMs = Date.now() - startTime;
16848
17681
  const unchangedCount = this.diffCalculator.filterByType(changes, "NO_CHANGE").length + actualCounts.skipped;
@@ -17003,6 +17836,7 @@ var DeployEngine = class {
17003
17836
  if (this.interrupted && this.hasPending(deleteExecutor)) throw new InterruptedError(this.interruptCause ?? "user");
17004
17837
  }
17005
17838
  } catch (error) {
17839
+ const initialDeploy = currentEtag === void 0;
17006
17840
  try {
17007
17841
  const preRollbackState = {
17008
17842
  version: 8,
@@ -17025,14 +17859,20 @@ var DeployEngine = class {
17025
17859
  } catch (saveError) {
17026
17860
  this.logger.warn(`Failed to save partial state before rollback: ${saveError instanceof Error ? saveError.message : String(saveError)}`);
17027
17861
  }
17862
+ let autoRollbackClean = false;
17028
17863
  if (error instanceof InterruptedError) {
17029
- this.logger.info(`Partial state saved (${Object.keys(newResources).length} resources). Run deploy again to resume, or destroy to clean up.`);
17864
+ await this.writeRollbackJournalSegment(stackName, completedOperations, "interrupted", initialDeploy);
17865
+ this.logger.info(`Partial state saved (${Object.keys(newResources).length} resources). Run deploy again to resume, 'cdkd rollback' to revert, or destroy to clean up.`);
17030
17866
  throw error;
17031
17867
  }
17032
17868
  if (this.options.noRollback) {
17869
+ await this.writeRollbackJournalSegment(stackName, completedOperations, "no-rollback-failure", initialDeploy);
17033
17870
  this.logger.warn("Deployment failed. --no-rollback is set, skipping rollback.");
17034
- this.logger.warn("Partial state has been saved. Manual cleanup may be required.");
17035
- } else await this.performRollback(completedOperations, newResources, stackName);
17871
+ this.logger.warn("Partial state has been saved. Run 'cdkd deploy' to resume, 'cdkd rollback' to revert, or destroy to clean up.");
17872
+ } else {
17873
+ await this.writeRollbackJournalSegment(stackName, completedOperations, "auto-rollback-started", initialDeploy);
17874
+ autoRollbackClean = (await this.performRollback(completedOperations, newResources, stackName)).failures === 0;
17875
+ }
17036
17876
  try {
17037
17877
  const postRollbackState = {
17038
17878
  version: 8,
@@ -17046,6 +17886,7 @@ var DeployEngine = class {
17046
17886
  };
17047
17887
  await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...currentEtag !== void 0 && { expectedEtag: currentEtag } });
17048
17888
  this.logger.debug("State saved after deployment failure");
17889
+ if (autoRollbackClean) await this.deleteRollbackJournalBestEffort(stackName);
17049
17890
  } catch (saveError) {
17050
17891
  this.logger.debug(`Retrying state save after rollback (ETag mismatch): ${saveError instanceof Error ? saveError.message : String(saveError)}`);
17051
17892
  try {
@@ -17062,6 +17903,7 @@ var DeployEngine = class {
17062
17903
  };
17063
17904
  await this.stateBackend.saveState(stackName, this.stackRegion, this.withParentInfo(postRollbackState), { ...freshEtag !== void 0 && { expectedEtag: freshEtag } });
17064
17905
  this.logger.debug("State saved after deployment failure (retry succeeded)");
17906
+ if (autoRollbackClean) await this.deleteRollbackJournalBestEffort(stackName);
17065
17907
  } catch (retryError) {
17066
17908
  this.logger.warn(`Failed to save state after rollback: ${retryError instanceof Error ? retryError.message : String(retryError)}`);
17067
17909
  }
@@ -17073,6 +17915,7 @@ var DeployEngine = class {
17073
17915
  outputs = await this.resolveOutputs(template, newResources, stackName, parameterValues, conditions);
17074
17916
  } catch (outputError) {
17075
17917
  await this.persistStateAfterOutputFailure(stackName, currentState, newResources, currentEtag, pendingMigration);
17918
+ await this.writeRollbackJournalSegment(stackName, completedOperations, "no-rollback-failure", currentEtag === void 0);
17076
17919
  throw outputError;
17077
17920
  }
17078
17921
  return {
@@ -17142,169 +17985,62 @@ var DeployEngine = class {
17142
17985
  }
17143
17986
  }
17144
17987
  /**
17145
- * Perform best-effort rollback of completed operations respecting dependencies
17146
- *
17147
- * - CREATE delete the newly created resource (in reverse dependency order)
17148
- * - UPDATE update back to previous properties
17149
- * - DELETE → cannot rollback (resource already deleted), log warning
17150
- *
17151
- * Resources completed concurrently in the dispatcher may have dependencies
17152
- * between them (e.g., IAM Policy depends on IAM Role). When rolling back
17153
- * CREATEs (deleting), dependent resources must be deleted before their
17154
- * dependencies. This method sorts CREATE rollback operations using dependency
17155
- * information from state, then processes UPDATE/DELETE rollbacks, and finally
17156
- * processes sorted CREATE rollback deletions.
17988
+ * Perform best-effort rollback of completed operations (issue #1183:
17989
+ * extracted into `rollback-executor.ts` so the standalone `cdkd rollback`
17990
+ * command drives identical semantics). Thin wrapper that builds the
17991
+ * executor context from the engine's collaborators and delegates.
17157
17992
  */
17158
17993
  async performRollback(completedOperations, stateResources, stackName) {
17159
- if (completedOperations.length === 0) {
17160
- this.logger.info("No completed operations to roll back.");
17161
- return;
17162
- }
17163
- this.logger.info(`Rolling back ${completedOperations.length} completed operation(s)...`);
17164
- this.recordEvent({
17165
- eventType: "ROLLBACK_STARTED",
17166
- stackName
17167
- });
17168
- const createOps = [];
17169
- const otherOps = [];
17170
- for (const op of completedOperations) if (op.changeType === "CREATE") createOps.push(op);
17171
- else otherOps.push(op);
17172
- for (let i = otherOps.length - 1; i >= 0; i--) {
17173
- const op = otherOps[i];
17174
- await this.performSingleRollback(op, stateResources, stackName);
17175
- }
17176
- if (createOps.length > 0) {
17177
- const sortedCreateOps = this.sortRollbackCreates(createOps, stateResources);
17178
- for (const op of sortedCreateOps) await this.performSingleRollback(op, stateResources, stackName);
17179
- }
17180
- this.logger.info("Rollback completed. Some resources may remain if deletion failed.");
17181
- this.recordEvent({
17182
- eventType: "ROLLBACK_FINISHED",
17183
- stackName
17184
- });
17994
+ const result = await replayRollback(completedOperations, stateResources, stackName, this.rollbackExecutorContext());
17995
+ return {
17996
+ failures: result.failures,
17997
+ warnings: result.warnings
17998
+ };
17185
17999
  }
17186
18000
  /**
17187
- * Sort CREATE rollback operations so that resources depending on others
17188
- * are deleted first (reverse dependency order).
17189
- *
17190
- * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
18001
+ * Best-effort rollback-journal deletion (issue #1183) used on the deploy
18002
+ * success path and after a clean automatic rollback. Never throws — a
18003
+ * failed delete only warns (the journal is advisory; the worst case is a
18004
+ * spurious "previous deploy failed" note on the next deploy).
17191
18005
  */
17192
- sortRollbackCreates(createOps, stateResources) {
17193
- const opMap = /* @__PURE__ */ new Map();
17194
- const deleteIds = /* @__PURE__ */ new Set();
17195
- for (const op of createOps) {
17196
- opMap.set(op.logicalId, op);
17197
- deleteIds.add(op.logicalId);
17198
- }
17199
- const dependedBy = /* @__PURE__ */ new Map();
17200
- for (const id of deleteIds) if (!dependedBy.has(id)) dependedBy.set(id, /* @__PURE__ */ new Set());
17201
- for (const id of deleteIds) {
17202
- const resource = stateResources[id];
17203
- if (!resource?.dependencies) continue;
17204
- for (const dep of resource.dependencies) {
17205
- if (!deleteIds.has(dep)) continue;
17206
- if (!dependedBy.has(dep)) dependedBy.set(dep, /* @__PURE__ */ new Set());
17207
- dependedBy.get(dep).add(id);
17208
- }
17209
- }
17210
- const sorted = [];
17211
- let remaining = new Set(deleteIds);
17212
- while (remaining.size > 0) {
17213
- const level = [];
17214
- for (const id of remaining) {
17215
- const dependents = dependedBy.get(id);
17216
- if (!(dependents ? [...dependents].some((d) => remaining.has(d)) : false)) level.push(id);
17217
- }
17218
- if (level.length === 0) {
17219
- this.logger.warn(`Circular dependency detected in rollback order, processing remaining ${remaining.size} resources`);
17220
- for (const id of remaining) {
17221
- const op = opMap.get(id);
17222
- if (op) sorted.push(op);
17223
- }
17224
- break;
17225
- }
17226
- for (const id of level) {
17227
- const op = opMap.get(id);
17228
- if (op) sorted.push(op);
17229
- }
17230
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
18006
+ async deleteRollbackJournalBestEffort(stackName) {
18007
+ try {
18008
+ await this.stateBackend.deleteRollbackJournal(stackName, this.stackRegion);
18009
+ } catch (err) {
18010
+ this.logger.debug(`Failed to delete rollback journal for ${stackName}: ${err instanceof Error ? err.message : String(err)}`);
17231
18011
  }
17232
- this.logger.debug(`Rollback CREATE deletion order: ${sorted.map((op) => op.logicalId).join(" → ")}`);
17233
- return sorted;
18012
+ }
18013
+ /** Build the {@link RollbackExecutorContext} from the engine's fields. */
18014
+ rollbackExecutorContext() {
18015
+ return {
18016
+ providerRegistry: this.providerRegistry,
18017
+ region: this.stackRegion,
18018
+ logger: this.logger,
18019
+ recordEvent: (event) => this.recordEvent(event)
18020
+ };
17234
18021
  }
17235
18022
  /**
17236
- * Perform a single rollback operation (extracted for reuse)
18023
+ * Record one rollback-journal segment (issue #1183) so the failed /
18024
+ * interrupted / about-to-auto-rollback deploy can be reverted later by
18025
+ * `cdkd rollback`. Best-effort like the partial-state save, but warns
18026
+ * LOUDLY on failure — the user just lost the ability to `cdkd rollback`.
17237
18027
  */
17238
- async performSingleRollback(op, stateResources, stackName) {
18028
+ async writeRollbackJournalSegment(stackName, completedOperations, reason, initialDeploy) {
18029
+ if (completedOperations.length === 0) return;
17239
18030
  try {
17240
- switch (op.changeType) {
17241
- case "CREATE": {
17242
- if (!op.physicalId) {
17243
- this.logger.warn(` Rollback: Cannot delete ${op.logicalId} — no physical ID recorded`);
17244
- break;
17245
- }
17246
- this.logger.info(` Rollback: Deleting created resource ${op.logicalId} (${op.resourceType})`);
17247
- const { provider } = this.providerRegistry.getProviderFor({
17248
- resourceType: op.resourceType,
17249
- provisionedBy: op.provisionedBy
17250
- });
17251
- await provider.delete(op.logicalId, op.physicalId, op.resourceType, op.properties, { expectedRegion: this.stackRegion });
17252
- delete stateResources[op.logicalId];
17253
- this.logger.info(` Rollback: ${op.logicalId} deleted successfully`);
17254
- this.recordEvent({
17255
- eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
17256
- stackName,
17257
- operation: "CREATE",
17258
- logicalId: op.logicalId,
17259
- resourceType: op.resourceType,
17260
- ...op.provisionedBy && { provisionedBy: op.provisionedBy }
17261
- });
17262
- break;
17263
- }
17264
- case "UPDATE": {
17265
- if (!op.previousState) {
17266
- this.logger.warn(` Rollback: Cannot restore ${op.logicalId} — no previous state available`);
17267
- break;
17268
- }
17269
- this.logger.info(` Rollback: Restoring ${op.logicalId} (${op.resourceType}) to previous state`);
17270
- const { provider } = this.providerRegistry.getProviderFor({
17271
- resourceType: op.resourceType,
17272
- provisionedBy: op.provisionedBy
17273
- });
17274
- const currentResource = stateResources[op.logicalId];
17275
- if (!currentResource) {
17276
- this.logger.warn(` Rollback: Cannot restore ${op.logicalId} — resource not found in current state`);
17277
- break;
17278
- }
17279
- await provider.update(op.logicalId, currentResource.physicalId, op.resourceType, op.previousState.properties, currentResource.properties);
17280
- stateResources[op.logicalId] = op.previousState;
17281
- this.logger.info(` Rollback: ${op.logicalId} restored successfully`);
17282
- this.recordEvent({
17283
- eventType: "ROLLBACK_RESOURCE_SUCCEEDED",
17284
- stackName,
17285
- operation: "UPDATE",
17286
- logicalId: op.logicalId,
17287
- resourceType: op.resourceType,
17288
- ...op.provisionedBy && { provisionedBy: op.provisionedBy }
17289
- });
17290
- break;
17291
- }
17292
- case "DELETE":
17293
- this.logger.warn(` Rollback: Cannot restore deleted resource ${op.logicalId} (${op.resourceType}) — resource has already been deleted`);
17294
- break;
17295
- }
17296
- } catch (rollbackError) {
17297
- this.logger.warn(` Rollback failed for ${op.logicalId} (${op.changeType}): ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
17298
- this.logger.warn(" Continuing with remaining rollback operations...");
17299
- this.recordEvent({
17300
- eventType: "ROLLBACK_RESOURCE_FAILED",
17301
- stackName,
17302
- operation: op.changeType,
17303
- logicalId: op.logicalId,
17304
- resourceType: op.resourceType,
17305
- ...op.provisionedBy && { provisionedBy: op.provisionedBy },
17306
- error: extractDeploymentEventError(rollbackError)
17307
- });
18031
+ const segment = {
18032
+ ...this.options.eventRecorder?.runId !== void 0 && { runId: this.options.eventRecorder.runId },
18033
+ timestamp: Date.now(),
18034
+ reason,
18035
+ initialDeploy,
18036
+ ...this.options.roleArn && { roleArn: this.options.roleArn },
18037
+ cdkdVersion: getCdkdVersion(),
18038
+ operations: completedOperations
18039
+ };
18040
+ await this.stateBackend.appendRollbackJournalSegment(stackName, this.stackRegion, segment);
18041
+ this.logger.debug(`Rollback journal segment written (${reason})`);
18042
+ } catch (journalError) {
18043
+ this.logger.warn(`Failed to write rollback journal: ${journalError instanceof Error ? journalError.message : String(journalError)}. 'cdkd rollback' will NOT be able to revert this deploy — use 'cdkd deploy' to resume or 'cdkd destroy' to clean up.`);
17308
18044
  }
17309
18045
  }
17310
18046
  /**
@@ -17909,5 +18645,5 @@ var DeployEngine = class {
17909
18645
  };
17910
18646
 
17911
18647
  //#endregion
17912
- export { getBootstrapMarkerKey as $, formatError as $t, refStateLookupFromResource as A, resolveBucketRegion as At, S3StateBackend as B, LocalMigrateError as Bt, findActionableSilentDrops as C, CFN_TEMPLATE_URL_LIMIT as Ct, isTerminationProtectionPropagationError as D, expectedOwnerParam as Dt, disableInstanceApiTermination as E, uploadCfnTemplate as Et, applyRoleArnIfSet as F, AssetError as Ft, WorkGraph as G, PartialFailureError as Gt, shouldRetainResource as H, LockError as Ht, DiffCalculator as I, CdkdError as It, loadPublishableAssetManifest as J, ResourceUpdateNotSupportedError as Jt, buildAssetRedirectMap as K, ProvisioningError as Kt, DagBuilder as L, ConfigError as Lt, normalizeAwsTagsToCfn as M, getAwsClients as Mt, resolveExplicitPhysicalId as N, resetAwsClients as Nt, IntrinsicFunctionResolver as O, AssemblyReader as Ot, assertRegionMatch as P, setAwsClients as Pt, ensureAssetStorage as Q, SynthesisError as Qt, TemplateParser as R, DependencyError as Rt, ProviderRegistry as S, CFN_TEMPLATE_BODY_LIMIT as St, slowCcOperationTimeoutMs as T, findLargeInlineResources as Tt, AssetPublisher as U, MissingCdkCliError as Ut, rebuildClientForBucketRegion as V, LocalStartServiceError as Vt, stringifyValue as W, NestedStackChildDirectDestroyError as Wt, AssetModeResolver as X, StackTerminationProtectionError as Xt, rewriteTemplateAssetReferences as Y, StackHasActiveImportsError as Yt, BOOTSTRAP_MARKER_PREFIX as Z, StateError as Zt, green as _, resolveSkipPrefix as _t, withRetry as a, getDockerCmd as at, IAMRoleProvider as b, resolveUseCdkBootstrapAssets as bt, computeImplicitDeleteEdges as c, AssetManifestLoader as ct, isStatefulRecreateTargetSync as d, synthesisStatusMessage as dt, isCdkdError as en, parseBootstrapMarker as et, renderStatefulReason as f, getDefaultStateBucketName as ft, gray as g, resolveCaptureObservedState as gt, cyan as h, resolveAutoAssetStorage as ht, withResourceDeadline as i, formatDockerLoginError as it, WAFv2WebACLProvider as j, AwsClients as jt, cfnRefValueFromPhysicalId as k, clearBucketRegionCache as kt, extractDeploymentEventError as l, getDockerImageBySourceHash as lt, bold as m, resolveApp as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, withErrorHandling as nn, validateContainerRepoName as nt, isRetryableTransientError as o, runDockerForeground as ot, formatResourceLine as p, getLegacyStateBucketName as pt, createAssetRedirectResolver as q, ResourceTimeoutError as qt, DeployEngine as r, __exportAll as rn, buildDockerImage as rt, IMPLICIT_DELETE_DEPENDENCIES as s, runDockerStreaming as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, normalizeAwsError as tn, validateAssetBucketName as tt, MULTI_REGION_RECREATE_BLOCKED_TYPES as u, Synthesizer as ut, red as v, resolveStateBucketWithDefault as vt, CloudControlProvider as w, MIGRATE_TMP_PREFIX as wt, collectInlinePolicyNamesManagedBySiblings as x, warnDeprecatedNoPrefixCliFlag as xt, yellow as y, resolveStateBucketWithDefaultAndSource as yt, LockManager as z, LocalInvokeBuildError as zt };
17913
- //# sourceMappingURL=deploy-engine-DbhK2y6L.js.map
18648
+ export { rewriteTemplateAssetReferences as $, StackHasActiveImportsError as $t, disableInstanceApiTermination as A, uploadCfnTemplate as At, DiffCalculator as B, CdkdError as Bt, yellow as C, resolveStateBucketWithDefaultAndSource as Ct, findActionableSilentDrops as D, CFN_TEMPLATE_URL_LIMIT as Dt, ProviderRegistry as E, CFN_TEMPLATE_BODY_LIMIT as Et, WAFv2WebACLProvider as F, AwsClients as Ft, rebuildClientForBucketRegion as G, LocalStartServiceError as Gt, TemplateParser as H, DependencyError as Ht, normalizeAwsTagsToCfn as I, getAwsClients as It, stringifyValue as J, NestedStackChildDirectDestroyError as Jt, shouldRetainResource as K, LockError as Kt, resolveExplicitPhysicalId as L, resetAwsClients as Lt, IntrinsicFunctionResolver as M, AssemblyReader as Mt, cfnRefValueFromPhysicalId as N, clearBucketRegionCache as Nt, CloudControlProvider as O, MIGRATE_TMP_PREFIX as Ot, refStateLookupFromResource as P, resolveBucketRegion as Pt, loadPublishableAssetManifest as Q, ResourceUpdateNotSupportedError as Qt, assertRegionMatch as R, setAwsClients as Rt, red as S, resolveStateBucketWithDefault as St, collectInlinePolicyNamesManagedBySiblings as T, warnDeprecatedNoPrefixCliFlag as Tt, LockManager as U, LocalInvokeBuildError as Ut, DagBuilder as V, ConfigError as Vt, S3StateBackend as W, LocalMigrateError as Wt, buildAssetRedirectMap as X, ProvisioningError as Xt, WorkGraph as Y, PartialFailureError as Yt, createAssetRedirectResolver as Z, ResourceTimeoutError as Zt, formatResourceLine as _, getLegacyStateBucketName as _t, DeploymentEventsStore as a, normalizeAwsError as an, validateAssetBucketName as at, gray as b, resolveCaptureObservedState as bt, withResourceDeadline as c, formatDockerLoginError as ct, IMPLICIT_DELETE_DEPENDENCIES as d, runDockerStreaming as dt, StackTerminationProtectionError as en, AssetModeResolver as et, computeImplicitDeleteEdges as f, AssetManifestLoader as ft, renderStatefulReason as g, getDefaultStateBucketName as gt, isStatefulRecreateTargetSync as h, synthesisStatusMessage as ht, DeploymentEventsReader as i, isCdkdError as in, parseBootstrapMarker as it, isTerminationProtectionPropagationError as j, expectedOwnerParam as jt, slowCcOperationTimeoutMs as k, findLargeInlineResources as kt, withRetry as l, getDockerCmd as lt, MULTI_REGION_RECREATE_BLOCKED_TYPES as m, Synthesizer as mt, DEFAULT_RESOURCE_WARN_AFTER_MS as n, SynthesisError as nn, ensureAssetStorage as nt, planRollback as o, withErrorHandling as on, validateContainerRepoName as ot, extractDeploymentEventError as p, getDockerImageBySourceHash as pt, AssetPublisher as q, MissingCdkCliError as qt, DeployEngine as r, formatError as rn, getBootstrapMarkerKey as rt, replayRollback as s, __exportAll as sn, buildDockerImage as st, DEFAULT_RESOURCE_TIMEOUT_MS as t, StateError as tn, BOOTSTRAP_MARKER_PREFIX as tt, isRetryableTransientError as u, runDockerForeground as ut, bold as v, resolveApp as vt, IAMRoleProvider as w, resolveUseCdkBootstrapAssets as wt, green as x, resolveSkipPrefix as xt, cyan as y, resolveAutoAssetStorage as yt, applyRoleArnIfSet as z, AssetError as zt };
18649
+ //# sourceMappingURL=deploy-engine-BbNhlr7X.js.map