1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import {
4 consumeBusEvents,
5 type EventRecord,
6 type EventRouteProps,
7} from "../EventBridge/EventSource.ts";
8
9/**
10 * The `detail` payload CloudFormation delivers to EventBridge when a stack,
11 * resource, drift detection, or StackSet changes status. CloudFormation uses
12 * hyphenated keys; fields not shared by every event kind are optional (the
13 * schema grows over time).
14 */
15export interface StackEventDetail {
16 /** The ARN of the stack the event is about. */
17 "stack-id"?: string;
18 /** Resource events: the logical id of the resource within the stack. */
19 "logical-resource-id"?: string;
20 /** Resource events: the physical id of the resource. */
21 "physical-resource-id"?: string;
22 /** Resource events: the resource type, e.g. `AWS::SNS::Topic`. */
23 "resource-type"?: string;
24 /** Drift events: the drift detection run id. */
25 "stack-drift-detection-id"?: string;
26 /** The new status and reason. */
27 "status-details"?: {
28 /** The new status, e.g. `CREATE_COMPLETE`, `UPDATE_ROLLBACK_COMPLETE`. */
29 status?: string;
30 /** Human-readable reason accompanying the status, when present. */
31 "status-reason"?: string;
32 [key: string]: unknown;
33 };
34 /** Additional event fields (the schema grows over time). */
35 [key: string]: unknown;
36}
37
38/** A CloudFormation EventBridge event delivered to the handler. */
39export type StackEvent = EventRecord<StackEventDetail>;
40
41/** Which CloudFormation status-change events to subscribe to. */
42export type StackEventKind =
43 | "stack"
44 | "resource"
45 | "drift-detection"
46 | "stack-set"
47 | "stack-set-operation"
48 | "stack-set-stack-instance";
49
50const DETAIL_TYPES: Record<StackEventKind, string> = {
51 stack: "CloudFormation Stack Status Change",
52 resource: "CloudFormation Resource Status Change",
53 "drift-detection": "CloudFormation Drift Detection Status Change",
54 "stack-set": "CloudFormation StackSet Status Change",
55 "stack-set-operation": "CloudFormation StackSet Operation Status Change",
56 "stack-set-stack-instance":
57 "CloudFormation StackSet StackInstance Status Change",
58};
59
60export interface StackEventSourceProps extends EventRouteProps {
61 /**
62 * Logical id for the backing EventBridge rule.
63 * @default "CloudFormationEvents"
64 */
65 id?: string;
66 /**
67 * Which status-change events to subscribe to.
68 * @default all kinds
69 */
70 kinds?: readonly StackEventKind[];
71 /**
72 * Restrict to events about specific stacks (matched against the event's
73 * `stack-id`, the stack ARN).
74 */
75 stackIds?: readonly string[];
76}
77
78/**
79 * Event source connecting CloudFormation status changes to the hosting
80 * compute. CloudFormation publishes every stack, resource, drift-detection,
81 * and StackSet status change to the account's default EventBridge bus
82 * (source `aws.cloudformation`); this subscribes the host Function to those
83 * events so it can alert on failed deployments or chain post-deploy
84 * automation.
85 *
86 * CloudFormation publishes to EventBridge automatically — no additional
87 * resource is created besides the EventBridge rule targeting the host.
88 * Provide the host-specific implementation layer
89 * (e.g. `AWS.Lambda.EventSource`) on the Function effect.
90 *
91 * ### Consuming Stack Events
92 * **Example:** Alert On Failed Stack Operations
93 * ```typescript
94 * import * as AWS from "alchemy/AWS";
95 *
96 * export default AlertFunction.make(
97 * { main: import.meta.url },
98 * Effect.gen(function* () {
99 * yield* AWS.CloudFormation.consumeStackEvents(
100 * { kinds: ["stack"] },
101 * (events) =>
102 * Stream.runForEach(events, (event) =>
103 * event.detail["status-details"]?.status?.endsWith("_FAILED")
104 * ? Effect.log(`stack ${event.detail["stack-id"]} failed`)
105 * : Effect.void,
106 * ),
107 * );
108 * return {};
109 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
110 * );
111 * ```
112 */
113export const consumeStackEvents = <StreamReq = never, Req = never>(
114 props: StackEventSourceProps,
115 process: (
116 events: Stream.Stream<StackEvent, never, StreamReq>,
117 ) => Effect.Effect<void, never, Req>,
118) =>
119 consumeBusEvents(
120 props.id ?? "CloudFormationEvents",
121 {
122 source: ["aws.cloudformation"],
123 "detail-type": (
124 props.kinds ?? (Object.keys(DETAIL_TYPES) as StackEventKind[])
125 ).map((kind) => DETAIL_TYPES[kind]),
126 ...(props.stackIds !== undefined
127 ? { detail: { "stack-id": [...props.stackIds] } }
128 : {}),
129 },
130 { description: props.description, state: props.state },
131 process,
132 );