@intentius/chant-lexicon-aws 0.18.17 → 0.18.19

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.
Files changed (38) hide show
  1. package/dist/actions/dynamodb.d.ts +2 -0
  2. package/dist/actions/dynamodb.d.ts.map +1 -1
  3. package/dist/actions/s3.d.ts +2 -0
  4. package/dist/actions/s3.d.ts.map +1 -1
  5. package/dist/components/apply.d.ts.map +1 -1
  6. package/dist/composites/alb-shared.d.ts.map +1 -1
  7. package/dist/composites/efs-with-access-point.d.ts.map +1 -1
  8. package/dist/composites/fargate-alb.d.ts.map +1 -1
  9. package/dist/composites/fargate-service.d.ts.map +1 -1
  10. package/dist/composites/lambda-dynamodb.d.ts.map +1 -1
  11. package/dist/composites/lambda-s3.d.ts.map +1 -1
  12. package/dist/composites/microvm-app.d.ts.map +1 -1
  13. package/dist/composites/rds-instance.d.ts.map +1 -1
  14. package/dist/composites/vpc-default.d.ts.map +1 -1
  15. package/dist/generated/index.d.ts +15 -7
  16. package/dist/generated/index.d.ts.map +1 -1
  17. package/dist/integrity.json +4 -4
  18. package/dist/manifest.json +1 -1
  19. package/dist/meta.json +252 -78
  20. package/dist/types/index.d.ts +194 -81
  21. package/package.json +2 -2
  22. package/src/actions/actions.test.ts +16 -2
  23. package/src/actions/dynamodb.ts +11 -0
  24. package/src/actions/s3.ts +12 -0
  25. package/src/components/apply.test.ts +51 -0
  26. package/src/components/apply.ts +21 -3
  27. package/src/composites/alb-shared.ts +5 -7
  28. package/src/composites/efs-with-access-point.ts +6 -16
  29. package/src/composites/fargate-alb.ts +35 -41
  30. package/src/composites/fargate-service.ts +26 -35
  31. package/src/composites/lambda-dynamodb.ts +44 -55
  32. package/src/composites/lambda-s3.ts +11 -6
  33. package/src/composites/microvm-app.ts +10 -4
  34. package/src/composites/rds-instance.ts +22 -35
  35. package/src/composites/vpc-default.ts +32 -39
  36. package/src/generated/index.d.ts +194 -81
  37. package/src/generated/index.ts +15 -7
  38. package/src/generated/lexicon-aws.json +252 -78
@@ -2707,6 +2707,42 @@ export declare class Assessment {
2707
2707
  readonly CreationTime: number;
2708
2708
  }
2709
2709
 
2710
+ export declare class AssessmentFramework {
2711
+ constructor(props: {
2712
+ /** The control sets that are associated with the framework. */
2713
+ ControlSets: AssessmentFramework_ControlSet[];
2714
+ /** The name of the framework. */
2715
+ Name: string;
2716
+ /** The Amazon Resource Name (ARN) of the framework. */
2717
+ Arn?: string;
2718
+ /** The compliance type that the framework supports, such as CIS or HIPAA. */
2719
+ ComplianceType?: string;
2720
+ /** The time when the framework was created. */
2721
+ CreatedAt?: string;
2722
+ /** The user or role that created the framework. */
2723
+ CreatedBy?: string;
2724
+ /** The description of the framework. */
2725
+ Description?: string;
2726
+ /** The unique identifier for the framework. */
2727
+ FrameworkId?: string;
2728
+ /** The time when the framework was most recently updated. */
2729
+ LastUpdatedAt?: string;
2730
+ /** The user or role that most recently updated the framework. */
2731
+ LastUpdatedBy?: string;
2732
+ /** The tags associated with the framework. */
2733
+ Tags?: AssessmentFramework_Tag[];
2734
+ /** The framework type, such as a standard framework or a custom framework. */
2735
+ Type?: "Custom" | "Standard";
2736
+ }, attributes?: CFResourceAttributes);
2737
+ readonly Arn: string;
2738
+ readonly CreatedAt: string;
2739
+ readonly CreatedBy: string;
2740
+ readonly FrameworkId: string;
2741
+ readonly LastUpdatedAt: string;
2742
+ readonly LastUpdatedBy: string;
2743
+ readonly Type: "Custom" | "Standard";
2744
+ }
2745
+
2710
2746
  export declare class AssessmentTarget {
2711
2747
  constructor(props: {
2712
2748
  Arn?: string;
@@ -5684,6 +5720,20 @@ export declare class CodeDeployApplication {
5684
5720
  }, attributes?: CFResourceAttributes);
5685
5721
  }
5686
5722
 
5723
+ export declare class CodeInterpreter {
5724
+ constructor(props: {
5725
+ /** The Amazon Resource Name (ARN) of the code interpreter. */
5726
+ CodeInterpreterArn?: string;
5727
+ /** The unique identifier of the code interpreter. */
5728
+ CodeInterpreterId?: string;
5729
+ /** The current status of the code interpreter. */
5730
+ Status?: "CREATE_FAILED" | "CREATING" | "DELETED" | "DELETE_FAILED" | "DELETING" | "READY";
5731
+ }, attributes?: CFResourceAttributes);
5732
+ readonly CodeInterpreterArn: string;
5733
+ readonly CodeInterpreterId: string;
5734
+ readonly Status: "CREATE_FAILED" | "CREATING" | "DELETED" | "DELETE_FAILED" | "DELETING" | "READY";
5735
+ }
5736
+
5687
5737
  export declare class CodeInterpreterCustom {
5688
5738
  constructor(props: {
5689
5739
  /** The name of the code interpreter. */
@@ -13747,6 +13797,27 @@ export declare class EventType {
13747
13797
  readonly LastUpdatedTime: string;
13748
13798
  }
13749
13799
 
13800
+ export declare class EvidentlyExperiment {
13801
+ constructor(props: {
13802
+ MetricGoals: EvidentlyExperiment_MetricGoalObject[];
13803
+ Name: string;
13804
+ OnlineAbConfig: EvidentlyExperiment_OnlineAbConfigObject;
13805
+ Project: string;
13806
+ Treatments: EvidentlyExperiment_TreatmentObject[];
13807
+ Arn?: string;
13808
+ Description?: string;
13809
+ RandomizationSalt?: string;
13810
+ RemoveSegment?: boolean;
13811
+ /** Start Experiment. Default is False */
13812
+ RunningStatus?: EvidentlyExperiment_RunningStatusObject;
13813
+ SamplingRate?: number;
13814
+ Segment?: string;
13815
+ /** An array of key-value pairs to apply to this resource. */
13816
+ Tags?: EvidentlyExperiment_Tag[];
13817
+ }, attributes?: CFResourceAttributes);
13818
+ readonly Arn: string;
13819
+ }
13820
+
13750
13821
  export declare class EvidentlyProject {
13751
13822
  constructor(props: {
13752
13823
  Name: string;
@@ -13828,27 +13899,6 @@ export declare class ExecutionPlan {
13828
13899
  readonly Id: string;
13829
13900
  }
13830
13901
 
13831
- export declare class Experiment {
13832
- constructor(props: {
13833
- MetricGoals: Experiment_MetricGoalObject[];
13834
- Name: string;
13835
- OnlineAbConfig: Experiment_OnlineAbConfigObject;
13836
- Project: string;
13837
- Treatments: Experiment_TreatmentObject[];
13838
- Arn?: string;
13839
- Description?: string;
13840
- RandomizationSalt?: string;
13841
- RemoveSegment?: boolean;
13842
- /** Start Experiment. Default is False */
13843
- RunningStatus?: Experiment_RunningStatusObject;
13844
- SamplingRate?: number;
13845
- Segment?: string;
13846
- /** An array of key-value pairs to apply to this resource. */
13847
- Tags?: Experiment_Tag[];
13848
- }, attributes?: CFResourceAttributes);
13849
- readonly Arn: string;
13850
- }
13851
-
13852
13902
  export declare class ExperimentTemplate {
13853
13903
  constructor(props: {
13854
13904
  Description: string;
@@ -30766,6 +30816,28 @@ export declare class SageMakerEndpoint {
30766
30816
  readonly EndpointName: string;
30767
30817
  }
30768
30818
 
30819
+ export declare class SageMakerExperiment {
30820
+ constructor(props: {
30821
+ /** The name of the experiment. Must be unique in your AWS account and is not case-sensitive. */
30822
+ ExperimentName: string;
30823
+ /** The Amazon Resource Name (ARN) of the experiment. */
30824
+ Arn?: string;
30825
+ /** When the experiment was created. */
30826
+ CreationTime?: string;
30827
+ /** The description of the experiment. */
30828
+ Description?: string;
30829
+ /** The name of the experiment as displayed. The name does not need to be unique. */
30830
+ DisplayName?: string;
30831
+ /** When the experiment was last modified. */
30832
+ LastModifiedTime?: string;
30833
+ /** A list of tags to associate with the experiment. */
30834
+ Tags?: Record<string, unknown>[];
30835
+ }, attributes?: CFResourceAttributes);
30836
+ readonly Arn: string;
30837
+ readonly CreationTime: string;
30838
+ readonly LastModifiedTime: string;
30839
+ }
30840
+
30769
30841
  export declare class SageMakerHub {
30770
30842
  constructor(props: {
30771
30843
  /** A description of the hub. */
@@ -43721,6 +43793,31 @@ export declare class Assessment_Tag {
43721
43793
  });
43722
43794
  }
43723
43795
 
43796
+ export declare class AssessmentFramework_ControlSet {
43797
+ constructor(props: {
43798
+ /** The list of controls within the control set. */
43799
+ Controls: AssessmentFramework_ControlSetControl[];
43800
+ /** The name of the control set. */
43801
+ Name: string;
43802
+ });
43803
+ }
43804
+
43805
+ export declare class AssessmentFramework_ControlSetControl {
43806
+ constructor(props: {
43807
+ /** The unique identifier of the control. */
43808
+ Id: string;
43809
+ });
43810
+ }
43811
+
43812
+ export declare class AssessmentFramework_Tag {
43813
+ constructor(props: {
43814
+ /** The key name of the tag. */
43815
+ Key: string;
43816
+ /** The value for the tag. */
43817
+ Value: string;
43818
+ });
43819
+ }
43820
+
43724
43821
  export declare class AssessmentReportsDestination {
43725
43822
  constructor(props: {
43726
43823
  Destination?: string;
@@ -56334,6 +56431,22 @@ export declare class ControlPlaneScalingConfig {
56334
56431
  });
56335
56432
  }
56336
56433
 
56434
+ export declare class ControlSet {
56435
+ constructor(props: {
56436
+ /** The list of controls within the control set. */
56437
+ Controls: AssessmentFramework_ControlSetControl[];
56438
+ /** The name of the control set. */
56439
+ Name: string;
56440
+ });
56441
+ }
56442
+
56443
+ export declare class ControlSetControl {
56444
+ constructor(props: {
56445
+ /** The unique identifier of the control. */
56446
+ Id: string;
56447
+ });
56448
+ }
56449
+
56337
56450
  export declare class CookieObject {
56338
56451
  constructor(props: {
56339
56452
  /** The specific cookies to forward to your distribution's origin. */
@@ -69810,6 +69923,65 @@ export declare class EventType_Tag {
69810
69923
  });
69811
69924
  }
69812
69925
 
69926
+ export declare class EvidentlyExperiment_MetricGoalObject {
69927
+ constructor(props: {
69928
+ DesiredChange: "DECREASE" | "INCREASE";
69929
+ /** The JSON path to reference the entity id in the event. */
69930
+ EntityIdKey: string;
69931
+ MetricName: string;
69932
+ /** The JSON path to reference the numerical metric value in the event. */
69933
+ ValueKey: string;
69934
+ /** Event patterns have the same structure as the events they match. Rules use event patterns to select events. An event pattern either matches an event or it doesn't. */
69935
+ EventPattern?: string;
69936
+ UnitLabel?: string;
69937
+ });
69938
+ }
69939
+
69940
+ export declare class EvidentlyExperiment_OnlineAbConfigObject {
69941
+ constructor(props: {
69942
+ ControlTreatmentName?: string;
69943
+ TreatmentWeights?: EvidentlyExperiment_TreatmentToWeight[];
69944
+ });
69945
+ }
69946
+
69947
+ export declare class EvidentlyExperiment_RunningStatusObject {
69948
+ constructor(props: {
69949
+ /** Provide the analysis Completion time for an experiment */
69950
+ AnalysisCompleteTime?: string;
69951
+ /** Provide CANCELLED or COMPLETED desired state when stopping an experiment */
69952
+ DesiredState?: string;
69953
+ /** Reason is a required input for stopping the experiment */
69954
+ Reason?: string;
69955
+ /** Provide START or STOP action to apply on an experiment */
69956
+ Status?: string;
69957
+ });
69958
+ }
69959
+
69960
+ export declare class EvidentlyExperiment_Tag {
69961
+ constructor(props: {
69962
+ /** The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. */
69963
+ Key: string;
69964
+ /** The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. */
69965
+ Value: string;
69966
+ });
69967
+ }
69968
+
69969
+ export declare class EvidentlyExperiment_TreatmentObject {
69970
+ constructor(props: {
69971
+ Feature: string;
69972
+ TreatmentName: string;
69973
+ Variation: string;
69974
+ Description?: string;
69975
+ });
69976
+ }
69977
+
69978
+ export declare class EvidentlyExperiment_TreatmentToWeight {
69979
+ constructor(props: {
69980
+ SplitWeight: number;
69981
+ Treatment: string;
69982
+ });
69983
+ }
69984
+
69813
69985
  export declare class EvidentlyProject_AppConfigResourceObject {
69814
69986
  constructor(props: {
69815
69987
  ApplicationId: string;
@@ -69979,65 +70151,6 @@ export declare class ExecutiveSummaryOption {
69979
70151
  });
69980
70152
  }
69981
70153
 
69982
- export declare class Experiment_MetricGoalObject {
69983
- constructor(props: {
69984
- DesiredChange: "DECREASE" | "INCREASE";
69985
- /** The JSON path to reference the entity id in the event. */
69986
- EntityIdKey: string;
69987
- MetricName: string;
69988
- /** The JSON path to reference the numerical metric value in the event. */
69989
- ValueKey: string;
69990
- /** Event patterns have the same structure as the events they match. Rules use event patterns to select events. An event pattern either matches an event or it doesn't. */
69991
- EventPattern?: string;
69992
- UnitLabel?: string;
69993
- });
69994
- }
69995
-
69996
- export declare class Experiment_OnlineAbConfigObject {
69997
- constructor(props: {
69998
- ControlTreatmentName?: string;
69999
- TreatmentWeights?: Experiment_TreatmentToWeight[];
70000
- });
70001
- }
70002
-
70003
- export declare class Experiment_RunningStatusObject {
70004
- constructor(props: {
70005
- /** Provide the analysis Completion time for an experiment */
70006
- AnalysisCompleteTime?: string;
70007
- /** Provide CANCELLED or COMPLETED desired state when stopping an experiment */
70008
- DesiredState?: string;
70009
- /** Reason is a required input for stopping the experiment */
70010
- Reason?: string;
70011
- /** Provide START or STOP action to apply on an experiment */
70012
- Status?: string;
70013
- });
70014
- }
70015
-
70016
- export declare class Experiment_Tag {
70017
- constructor(props: {
70018
- /** The key name of the tag. You can specify a value that is 1 to 128 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. */
70019
- Key: string;
70020
- /** The value for the tag. You can specify a value that is 0 to 256 Unicode characters in length and cannot be prefixed with aws:. You can use any of the following characters: the set of Unicode letters, digits, whitespace, _, ., /, =, +, and -. */
70021
- Value: string;
70022
- });
70023
- }
70024
-
70025
- export declare class Experiment_TreatmentObject {
70026
- constructor(props: {
70027
- Feature: string;
70028
- TreatmentName: string;
70029
- Variation: string;
70030
- Description?: string;
70031
- });
70032
- }
70033
-
70034
- export declare class Experiment_TreatmentToWeight {
70035
- constructor(props: {
70036
- SplitWeight: number;
70037
- Treatment: string;
70038
- });
70039
- }
70040
-
70041
70154
  export declare class ExperimentConfig {
70042
70155
  constructor(props: {
70043
70156
  /** The name of an existing experiment to associate with the trial component. */
@@ -94672,7 +94785,7 @@ export declare class OnInput {
94672
94785
  export declare class OnlineAbConfigObject {
94673
94786
  constructor(props: {
94674
94787
  ControlTreatmentName?: string;
94675
- TreatmentWeights?: Experiment_TreatmentToWeight[];
94788
+ TreatmentWeights?: EvidentlyExperiment_TreatmentToWeight[];
94676
94789
  });
94677
94790
  }
94678
94791
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.18.17",
3
+ "version": "0.18.19",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.18.17",
83
+ "@intentius/chant": "^0.18.19",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -1,7 +1,7 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { S3Actions } from "./s3";
2
+ import { S3Actions, s3ActionsFor } from "./s3";
3
3
  import { LambdaActions } from "./lambda";
4
- import { DynamoDBActions } from "./dynamodb";
4
+ import { DynamoDBActions, dynamoDBActionsFor } from "./dynamodb";
5
5
  import { SQSActions } from "./sqs";
6
6
  import { SNSActions } from "./sns";
7
7
  import { IAMActions } from "./iam";
@@ -73,3 +73,17 @@ describe("Action Constants", () => {
73
73
  });
74
74
  });
75
75
  });
76
+
77
+ // The EVL003-clean lookup helpers must return the same arrays as the tables (#952).
78
+ describe("action-level lookup helpers", () => {
79
+ test("s3ActionsFor mirrors S3Actions for every access level", () => {
80
+ for (const level of Object.keys(S3Actions) as (keyof typeof S3Actions)[]) {
81
+ expect(s3ActionsFor(level)).toEqual(S3Actions[level]);
82
+ }
83
+ });
84
+ test("dynamoDBActionsFor mirrors DynamoDBActions for every access level", () => {
85
+ for (const level of Object.keys(DynamoDBActions) as (keyof typeof DynamoDBActions)[]) {
86
+ expect(dynamoDBActionsFor(level)).toEqual(DynamoDBActions[level]);
87
+ }
88
+ });
89
+ });
@@ -34,3 +34,14 @@ export const DynamoDBActions = {
34
34
  Query: ["dynamodb:Query"],
35
35
  Scan: ["dynamodb:Scan"],
36
36
  } as const;
37
+
38
+ export type DynamoDBAccessLevel = keyof typeof DynamoDBActions;
39
+
40
+ // Map-backed lookup — avoids computed element access `DynamoDBActions[access]`
41
+ // that the evaluability lint (EVL003) flags despite the statically-typed key (#952).
42
+ const DYNAMODB_ACTIONS = new Map<DynamoDBAccessLevel, readonly string[]>(
43
+ Object.entries(DynamoDBActions) as [DynamoDBAccessLevel, readonly string[]][],
44
+ );
45
+ export function dynamoDBActionsFor(access: DynamoDBAccessLevel): readonly string[] {
46
+ return DYNAMODB_ACTIONS.get(access) ?? [];
47
+ }
package/src/actions/s3.ts CHANGED
@@ -32,3 +32,15 @@ export const S3Actions = {
32
32
  DeleteObject: ["s3:DeleteObject", "s3:DeleteObjectVersion"],
33
33
  ListObjects: ["s3:ListBucket", "s3:ListBucketVersions"],
34
34
  } as const;
35
+
36
+ export type S3AccessLevel = keyof typeof S3Actions;
37
+
38
+ // A Map-backed lookup avoids computed element access (`S3Actions[access]`), which
39
+ // the evaluability lint (EVL003) flags even though the key is statically typed —
40
+ // prefer this in evaluable code such as composites (#952).
41
+ const S3_ACTIONS = new Map<S3AccessLevel, readonly string[]>(
42
+ Object.entries(S3Actions) as [S3AccessLevel, readonly string[]][],
43
+ );
44
+ export function s3ActionsFor(access: S3AccessLevel): readonly string[] {
45
+ return S3_ACTIONS.get(access) ?? [];
46
+ }
@@ -48,6 +48,57 @@ describe("cfn-deploy (#557)", () => {
48
48
  ).rejects.toThrow(/CREATE_FAILED/);
49
49
  });
50
50
 
51
+ it("throws fail-closed on an undefined stack status (Floci) instead of a TypeError (#947)", async () => {
52
+ const base = createMockCloudExecutor({ stacks: { "search-service": {} } });
53
+ const executor = {
54
+ ...base.executor,
55
+ cloudformation: {
56
+ ...base.executor.cloudformation,
57
+ waitForStack: async () => ({ stackStatus: undefined as unknown as string, outputs: {} }),
58
+ },
59
+ };
60
+ const capability = createCfnDeployCapability(executor);
61
+ await expect(
62
+ capability.run(ctx, { stack: "search-service", template: "archive:search.template.json" }),
63
+ ).rejects.toThrow(/indeterminate status/);
64
+ });
65
+
66
+ it("rollback degrades gracefully when the target lacks RollbackStack (Floci) (#947)", async () => {
67
+ const base = createMockCloudExecutor({ stacks: { "search-service": {} } });
68
+ const executor = {
69
+ ...base.executor,
70
+ cloudformation: {
71
+ ...base.executor.cloudformation,
72
+ rollbackStack: async () => {
73
+ throw new Error(
74
+ "An error occurred (UnknownAction) when calling the RollbackStack operation: Action RollbackStack is not supported.",
75
+ );
76
+ },
77
+ },
78
+ };
79
+ const capability = createCfnDeployCapability(executor);
80
+ await expect(
81
+ capability.rollback!(ctx, { stack: "search-service", template: "archive:search.template.json" }),
82
+ ).resolves.toBeUndefined();
83
+ });
84
+
85
+ it("rollback rethrows a genuine failure (not an unsupported action) (#947)", async () => {
86
+ const base = createMockCloudExecutor({ stacks: { "search-service": {} } });
87
+ const executor = {
88
+ ...base.executor,
89
+ cloudformation: {
90
+ ...base.executor.cloudformation,
91
+ rollbackStack: async () => {
92
+ throw new Error("An error occurred (AccessDenied): not authorized to RollbackStack");
93
+ },
94
+ },
95
+ };
96
+ const capability = createCfnDeployCapability(executor);
97
+ await expect(
98
+ capability.rollback!(ctx, { stack: "search-service", template: "archive:search.template.json" }),
99
+ ).rejects.toThrow(/AccessDenied/);
100
+ });
101
+
51
102
  it("passes wired inputs and imageRef through as CloudFormation parameters", async () => {
52
103
  const mock = createMockCloudExecutor({ stacks: { "search-service": {} } });
53
104
  const capability = createCfnDeployCapability(mock.executor);
@@ -153,15 +153,33 @@ export function createCfnDeployCapability(
153
153
  // failed deploy as green (and hand downstream steps an empty `outputs`),
154
154
  // so throw — the same fail-closed contract the rest of the release model
155
155
  // relies on.
156
- if (stackStatus.includes("ROLLBACK") || stackStatus.endsWith("_FAILED")) {
156
+ // A missing status is fail-closed too: Floci's describe-stacks can omit
157
+ // StackStatus mid-rollback, and `undefined.includes` would throw a TypeError
158
+ // that masks the real deploy failure (#947).
159
+ if (!stackStatus || stackStatus.includes("ROLLBACK") || stackStatus.endsWith("_FAILED")) {
157
160
  throw new Error(
158
- `cfn-deploy "${stack}": stack reached ${stackStatus} — the deploy failed and was rolled back. Inspect the stack events for the resource that failed to create/update.`,
161
+ `cfn-deploy "${stack}": stack reached ${stackStatus ?? "an indeterminate status"} — the deploy failed and was rolled back. Inspect the stack events for the resource that failed to create/update.`,
159
162
  );
160
163
  }
161
164
  return { stackStatus, outputs, ...(snapshotId ? { snapshotId } : {}) };
162
165
  },
163
166
  async rollback(ctx, input) {
164
- await executor.cloudformation.rollbackStack(input.stack ?? ctx.component);
167
+ const stack = input.stack ?? ctx.component;
168
+ try {
169
+ await executor.cloudformation.rollbackStack(stack);
170
+ } catch (err) {
171
+ const message = err instanceof Error ? err.message : String(err);
172
+ // Local emulators (Floci) don't implement RollbackStack → `UnknownAction`.
173
+ // The stack is already in a terminal failed state; degrade to a clear
174
+ // message instead of crashing the compensation phase (#947).
175
+ if (/UnknownAction|not supported/i.test(message)) {
176
+ console.error(
177
+ `cfn-deploy rollback "${stack}": the target doesn't support RollbackStack (a local emulator such as Floci) — skipping automated rollback. Original: ${message}`,
178
+ );
179
+ return;
180
+ }
181
+ throw err;
182
+ }
165
183
  },
166
184
  };
167
185
  }
@@ -103,15 +103,13 @@ export const AlbShared = Composite<AlbSharedProps>((props) => {
103
103
  Port: listenerPort,
104
104
  Protocol: protocol,
105
105
  DefaultActions: [defaultAction],
106
+ // HTTPS listeners carry a cert; a ternary keeps the `new` out of an `if` (EVL002).
107
+ Certificates:
108
+ protocol === "HTTPS" && props.certificateArn
109
+ ? [new Listener_Certificate({ CertificateArn: props.certificateArn })]
110
+ : undefined,
106
111
  };
107
112
 
108
- if (protocol === "HTTPS" && props.certificateArn) {
109
- const cert = new Listener_Certificate({
110
- CertificateArn: props.certificateArn,
111
- });
112
- listenerProps.Certificates = [cert];
113
- }
114
-
115
113
  const listener = new Listener(mergeDefaults(listenerProps, defs?.listener));
116
114
 
117
115
  return {
@@ -29,22 +29,12 @@ export interface EfsWithAccessPointProps {
29
29
  }
30
30
 
31
31
  export const EfsWithAccessPoint = Composite<EfsWithAccessPointProps>((props) => {
32
- const ingressRules: InstanceType<typeof SecurityGroup_Ingress>[] = [];
33
- if (props.sourceSecurityGroupId) {
34
- ingressRules.push(new SecurityGroup_Ingress({
35
- IpProtocol: "tcp",
36
- FromPort: 2049,
37
- ToPort: 2049,
38
- SourceSecurityGroupId: props.sourceSecurityGroupId,
39
- }));
40
- } else if (props.ingressCidr) {
41
- ingressRules.push(new SecurityGroup_Ingress({
42
- IpProtocol: "tcp",
43
- FromPort: 2049,
44
- ToPort: 2049,
45
- CidrIp: props.ingressCidr,
46
- }));
47
- }
32
+ // Ternary (not push-inside-if) keeps the `new`s out of control flow (EVL002).
33
+ const ingressRules: InstanceType<typeof SecurityGroup_Ingress>[] = props.sourceSecurityGroupId
34
+ ? [new SecurityGroup_Ingress({ IpProtocol: "tcp", FromPort: 2049, ToPort: 2049, SourceSecurityGroupId: props.sourceSecurityGroupId })]
35
+ : props.ingressCidr
36
+ ? [new SecurityGroup_Ingress({ IpProtocol: "tcp", FromPort: 2049, ToPort: 2049, CidrIp: props.ingressCidr })]
37
+ : [];
48
38
 
49
39
  const securityGroup = new SecurityGroup({
50
40
  GroupDescription: "EFS mount target SG",
@@ -143,14 +143,10 @@ export const FargateAlb = Composite<FargateAlbProps>((props) => {
143
143
  },
144
144
  });
145
145
 
146
- const environmentVars: InstanceType<typeof TaskDefinition_KeyValuePair>[] = [];
147
- if (props.environment) {
148
- for (const [name, value] of Object.entries(props.environment)) {
149
- environmentVars.push(
150
- new TaskDefinition_KeyValuePair({ Name: name, Value: value }),
151
- );
152
- }
153
- }
146
+ // `.map` keeps the `new`s out of the `for`/`if` (EVL002).
147
+ const environmentVars: InstanceType<typeof TaskDefinition_KeyValuePair>[] = props.environment
148
+ ? Object.entries(props.environment).map(([name, value]) => new TaskDefinition_KeyValuePair({ Name: name, Value: value }))
149
+ : [];
154
150
 
155
151
  const container = new TaskDefinition_ContainerDefinition({
156
152
  Name: "app",
@@ -232,40 +228,38 @@ export const FargateAlb = Composite<FargateAlbProps>((props) => {
232
228
  TargetGroupArn: targetGroup.TargetGroupArn,
233
229
  });
234
230
 
235
- let listener: InstanceType<typeof Listener>;
236
- let redirectListener: InstanceType<typeof Listener> | undefined;
237
-
238
- if (props.certificateArn) {
239
- listener = new Listener({
240
- LoadBalancerArn: alb.LoadBalancerArn,
241
- Port: 443,
242
- Protocol: "HTTPS",
243
- Certificates: [new Listener_Certificate({ CertificateArn: props.certificateArn })],
244
- DefaultActions: [forwardAction],
245
- });
246
- redirectListener = new Listener({
247
- LoadBalancerArn: alb.LoadBalancerArn,
248
- Port: 80,
249
- Protocol: "HTTP",
250
- DefaultActions: [
251
- new Listener_Action({
252
- Type: "redirect",
253
- RedirectConfig: new Listener_RedirectConfig({
254
- Protocol: "HTTPS",
255
- Port: "443",
256
- StatusCode: "HTTP_301",
231
+ // Ternaries keep the `new`s out of the `if`/`else` (EVL002).
232
+ const listener: InstanceType<typeof Listener> = props.certificateArn
233
+ ? new Listener({
234
+ LoadBalancerArn: alb.LoadBalancerArn,
235
+ Port: 443,
236
+ Protocol: "HTTPS",
237
+ Certificates: [new Listener_Certificate({ CertificateArn: props.certificateArn })],
238
+ DefaultActions: [forwardAction],
239
+ })
240
+ : new Listener({
241
+ LoadBalancerArn: alb.LoadBalancerArn,
242
+ Port: listenerPort,
243
+ Protocol: "HTTP",
244
+ DefaultActions: [forwardAction],
245
+ });
246
+ const redirectListener: InstanceType<typeof Listener> | undefined = props.certificateArn
247
+ ? new Listener({
248
+ LoadBalancerArn: alb.LoadBalancerArn,
249
+ Port: 80,
250
+ Protocol: "HTTP",
251
+ DefaultActions: [
252
+ new Listener_Action({
253
+ Type: "redirect",
254
+ RedirectConfig: new Listener_RedirectConfig({
255
+ Protocol: "HTTPS",
256
+ Port: "443",
257
+ StatusCode: "HTTP_301",
258
+ }),
257
259
  }),
258
- }),
259
- ],
260
- });
261
- } else {
262
- listener = new Listener({
263
- LoadBalancerArn: alb.LoadBalancerArn,
264
- Port: listenerPort,
265
- Protocol: "HTTP",
266
- DefaultActions: [forwardAction],
267
- });
268
- }
260
+ ],
261
+ })
262
+ : undefined;
269
263
 
270
264
  // ECS Service
271
265
  const serviceLoadBalancer = new EcsService_LoadBalancer({