1import * as AWS from "@/AWS";
2import { Application } from "@/AWS/CodeDeploy/Application.ts";
3import { DeploymentConfig } from "@/AWS/CodeDeploy/DeploymentConfig.ts";
4import { DeploymentGroup } from "@/AWS/CodeDeploy/DeploymentGroup.ts";
5import * as Provider from "@/Provider";
6import * as Test from "@/Test/Alchemy";
7import * as codedeploy from "@distilled.cloud/aws/codedeploy";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13const applicationName = "alchemy-test-cd-app";
14const deploymentGroupName = "alchemy-test-cd-dg";
15const deploymentConfigName = "alchemy-test-cd-config";
16
17const getGroup = codedeploy
18 .getDeploymentGroup({ applicationName, deploymentGroupName })
19 .pipe(
20 Effect.map((res) => res.deploymentGroupInfo),
21 Effect.catchTag(
22 [
23 "DeploymentGroupDoesNotExistException",
24 "ApplicationDoesNotExistException",
25 ],
26 () => Effect.succeed(undefined),
27 ),
28 );
29
30// A Lambda-platform deployment group: an application, a custom canary
31// deployment configuration, a CodeDeploy service role (assumed by
32// codedeploy.amazonaws.com, granted the AWS-managed Lambda policy), and the
33// group tying a deployment config to the application.
34const makeStack = (useCustomConfig: boolean) =>
35 Effect.gen(function* () {
36 const app = yield* Application("App", {
37 applicationName,
38 computePlatform: "Lambda",
39 });
40 const config = yield* DeploymentConfig("Config", {
41 deploymentConfigName,
42 computePlatform: "Lambda",
43 trafficRoutingConfig: {
44 type: "TimeBasedCanary",
45 timeBasedCanary: { canaryPercentage: 10, canaryInterval: 1 },
46 },
47 });
48 const role = yield* AWS.IAM.Role("DeployRole", {
49 assumeRolePolicyDocument: {
50 Version: "2012-10-17",
51 Statement: [
52 {
53 Effect: "Allow",
54 Principal: { Service: "codedeploy.amazonaws.com" },
55 Action: ["sts:AssumeRole"],
56 },
57 ],
58 },
59 managedPolicyArns: [
60 "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRoleForLambda",
61 ],
62 });
63 const group = yield* DeploymentGroup("Group", {
64 applicationName: app.applicationName,
65 deploymentGroupName,
66 serviceRoleArn: role.roleArn,
67 deploymentConfigName: useCustomConfig
68 ? config.deploymentConfigName
69 : "CodeDeployDefault.LambdaAllAtOnce",
70 deploymentStyle: {
71 deploymentType: "BLUE_GREEN",
72 deploymentOption: "WITH_TRAFFIC_CONTROL",
73 },
74 autoRollbackConfiguration: {
75 enabled: true,
76 events: ["DEPLOYMENT_FAILURE"],
77 },
78 tags: { env: "test" },
79 });
80 return { app, config, group };
81 });
82
83test.provider(
84 "lifecycle: create Lambda application + deployment group, update config, destroy",
85 (stack) =>
86 Effect.gen(function* () {
87 yield* stack.destroy();
88
89 // Create.
90 const deployed = yield* stack.deploy(makeStack(false));
91 expect(deployed.app.applicationName).toBe(applicationName);
92 expect(deployed.app.applicationArn).toContain(":application:");
93 expect(deployed.app.computePlatform).toBe("Lambda");
94 expect(deployed.group.deploymentGroupName).toBe(deploymentGroupName);
95 expect(deployed.group.deploymentGroupArn).toContain(":deploymentgroup:");
96 expect(deployed.config.deploymentConfigName).toBe(deploymentConfigName);
97 expect(deployed.config.deploymentConfigArn).toContain(
98 ":deploymentconfig:",
99 );
100 expect(deployed.config.deploymentConfigId).toBeTruthy();
101 expect(deployed.config.computePlatform).toBe("Lambda");
102
103 // Out-of-band verification via distilled.
104 const created = yield* getGroup;
105 expect(created?.deploymentGroupName).toBe(deploymentGroupName);
106 expect(created?.computePlatform).toBe("Lambda");
107 expect(created?.deploymentConfigName).toBe(
108 "CodeDeployDefault.LambdaAllAtOnce",
109 );
110
111 const appCheck = yield* codedeploy.getApplication({ applicationName });
112 expect(appCheck.application?.computePlatform).toBe("Lambda");
113
114 // Out-of-band verification of the custom deployment configuration.
115 const configCheck = yield* codedeploy.getDeploymentConfig({
116 deploymentConfigName,
117 });
118 expect(configCheck.deploymentConfigInfo?.computePlatform).toBe("Lambda");
119 expect(
120 configCheck.deploymentConfigInfo?.trafficRoutingConfig?.timeBasedCanary
121 ?.canaryPercentage,
122 ).toBe(10);
123
124 // Canonical list() coverage for Application.
125 const appProvider = yield* Provider.findProvider(Application);
126 const apps = yield* appProvider.list();
127 expect(apps.some((a) => a.applicationName === applicationName)).toBe(
128 true,
129 );
130
131 // Canonical list() coverage for DeploymentConfig — custom configs are
132 // listed, AWS-managed CodeDeployDefault.* ones are filtered out.
133 const configProvider = yield* Provider.findProvider(DeploymentConfig);
134 const configs = yield* configProvider.list();
135 expect(
136 configs.some((c) => c.deploymentConfigName === deploymentConfigName),
137 ).toBe(true);
138 expect(
139 configs.every(
140 (c) => !c.deploymentConfigName.startsWith("CodeDeployDefault."),
141 ),
142 ).toBe(true);
143
144 // Update — point the group at the custom config in place (no
145 // replacement).
146 yield* stack.deploy(makeStack(true));
147 const updated = yield* getGroup;
148 expect(updated?.deploymentConfigName).toBe(deploymentConfigName);
149
150 // Destroy — verify the group, application, and config are gone
151 // out-of-band.
152 yield* stack.destroy();
153 const afterGroup = yield* getGroup;
154 expect(afterGroup).toBeUndefined();
155 const afterApp = yield* codedeploy
156 .getApplication({ applicationName })
157 .pipe(
158 Effect.map((res) => res.application),
159 Effect.catchTag("ApplicationDoesNotExistException", () =>
160 Effect.succeed(undefined),
161 ),
162 );
163 expect(afterApp).toBeUndefined();
164 const afterConfig = yield* codedeploy
165 .getDeploymentConfig({ deploymentConfigName })
166 .pipe(
167 Effect.map((res) => res.deploymentConfigInfo),
168 Effect.catchTag("DeploymentConfigDoesNotExistException", () =>
169 Effect.succeed(undefined),
170 ),
171 );
172 expect(afterConfig).toBeUndefined();
173 }),
174 {
175 tags: [
176 "provider:aws",
177 "provider:aws:codedeploy",
178 "provider:aws:iam",
179 "live",
180 ],
181 timeout: 300_000,
182 },
183);