1import type * as amplify from "@distilled.cloud/aws/amplify";
2import * as Effect from "effect/Effect";
3import type * as Stream from "effect/Stream";
4import {
5 consumeBusEvents,
6 type EventRecord,
7 type EventRouteProps,
8} from "../EventBridge/EventSource.ts";
9
10/**
11 * The `detail` payload Amplify Hosting delivers to EventBridge when a
12 * deployment (build job) changes status.
13 */
14export interface DeploymentStatusChangeDetail {
15 /**
16 * ID of the Amplify app the job belongs to.
17 */
18 appId: string;
19 /**
20 * Name of the branch the job ran on.
21 */
22 branchName: string;
23 /**
24 * ID of the build job whose status changed.
25 */
26 jobId: string;
27 /**
28 * New status of the job (`STARTED`, `SUCCEED`, or `FAILED`).
29 */
30 jobStatus: amplify.JobStatus;
31}
32
33/** A deployment-status-change EventBridge event delivered to the handler. */
34export type DeploymentStatusChangeEvent =
35 EventRecord<DeploymentStatusChangeDetail>;
36
37export interface DeploymentStatusChangeEventSourceProps extends EventRouteProps {
38 /**
39 * Logical id prefix for the backing EventBridge rule.
40 * @default "Amplify"
41 */
42 id?: string;
43 /**
44 * Only deliver events whose `jobStatus` is one of these values. Omit to
45 * receive every status transition (`STARTED`, `SUCCEED`, `FAILED`).
46 */
47 jobStatus?: amplify.JobStatus[];
48}
49
50/**
51 * Deliver Amplify Hosting deployment status changes (`aws.amplify` /
52 * `"Amplify Deployment Status Change"` EventBridge events) to the host
53 * Function — e.g. to notify a chat channel on failed builds or kick off
54 * cache invalidation when a deploy succeeds.
55 *
56 * The EventBridge pattern matches every Amplify app in the account and
57 * region (the pattern must be literal — `Output` values such as `app.appId`
58 * do not resolve inside the deployed bundle); inspect `event.detail.appId` /
59 * `event.detail.branchName` in the handler if multiple apps share the
60 * Function. Provide `AWS.Lambda.EventSource` on the Function effect to
61 * implement the subscription.
62 *
63 * ### Reacting To Deployments
64 * **Example:** Alert on Failed Builds
65 * ```typescript
66 * yield* AWS.Amplify.consumeDeploymentStatusChanges(
67 * { jobStatus: ["FAILED"] },
68 * (events) =>
69 * Stream.runForEach(events, (event) =>
70 * Effect.log(
71 * `build ${event.detail.jobId} failed on ` +
72 * `${event.detail.appId}/${event.detail.branchName}`,
73 * ),
74 * ),
75 * );
76 * ```
77 *
78 * **Example:** Register the Event Source inside a Lambda Function
79 * ```typescript
80 * import * as AWS from "alchemy/AWS";
81 *
82 * export class DeployHooks extends AWS.Lambda.Function<AWS.Lambda.Function>()(
83 * "DeployHooks",
84 * ) {}
85 *
86 * export default DeployHooks.make(
87 * { main: import.meta.url },
88 * Effect.gen(function* () {
89 * yield* AWS.Amplify.consumeDeploymentStatusChanges(
90 * { jobStatus: ["SUCCEED"] },
91 * (events) =>
92 * Stream.runForEach(events, (event) =>
93 * Effect.log(`deployed ${event.detail.branchName}`),
94 * ),
95 * );
96 * return {};
97 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
98 * );
99 * ```
100 */
101export const consumeDeploymentStatusChanges = <StreamReq = never, Req = never>(
102 props: DeploymentStatusChangeEventSourceProps,
103 process: (
104 events: Stream.Stream<DeploymentStatusChangeEvent, never, StreamReq>,
105 ) => Effect.Effect<void, never, Req>,
106) =>
107 Effect.gen(function* () {
108 const { id, jobStatus, ...routeProps } = props;
109
110 yield* consumeBusEvents(
111 `${id ?? "Amplify"}-DeploymentStatusChanges`,
112 {
113 source: ["aws.amplify"],
114 "detail-type": ["Amplify Deployment Status Change"],
115 ...(jobStatus && jobStatus.length > 0 ? { detail: { jobStatus } } : {}),
116 },
117 routeProps,
118 process,
119 );
120 });