1import * as AWS from "@/AWS";
2import { Pipeline } from "@/AWS/CodePipeline/Pipeline.ts";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as codepipeline from "@distilled.cloud/aws/codepipeline";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Output from "@/Output";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12const pipelineName = "alchemy-test-codepipeline";
13
14const getPipeline = codepipeline
15 .getPipeline({ name: pipelineName })
16 .pipe(
17 Effect.catchTag("PipelineNotFoundException", () =>
18 Effect.succeed(undefined),
19 ),
20 );
21
22// Build the stack: an S3 source bucket (must be versioned for an S3 source
23// action), an artifact bucket, a pipeline role, and the pipeline itself.
24const makeStack = (objectKey: string) =>
25 Effect.gen(function* () {
26 const source = yield* AWS.S3.Bucket("PipelineSource", {
27 versioning: "Enabled",
28 forceDestroy: true,
29 });
30 const artifacts = yield* AWS.S3.Bucket("PipelineArtifacts", {
31 forceDestroy: true,
32 });
33 const role = yield* AWS.IAM.Role("PipelineRole", {
34 assumeRolePolicyDocument: {
35 Version: "2012-10-17",
36 Statement: [
37 {
38 Effect: "Allow",
39 Principal: { Service: "codepipeline.amazonaws.com" },
40 Action: ["sts:AssumeRole"],
41 },
42 ],
43 },
44 inlinePolicies: {
45 Artifacts: {
46 Version: "2012-10-17",
47 Statement: [
48 {
49 Effect: "Allow",
50 Action: [
51 "s3:GetObject",
52 "s3:GetObjectVersion",
53 "s3:PutObject",
54 "s3:GetBucketVersioning",
55 "s3:ListBucket",
56 ],
57 Resource: [
58 source.bucketArn,
59 Output.interpolate`${source.bucketArn}/*` as any,
60 artifacts.bucketArn,
61 Output.interpolate`${artifacts.bucketArn}/*` as any,
62 ],
63 },
64 ],
65 },
66 },
67 });
68 return yield* Pipeline("LifecyclePipeline", {
69 pipelineName,
70 roleArn: role.roleArn,
71 artifactStore: { type: "S3", location: artifacts.bucketName },
72 stages: [
73 {
74 name: "Source",
75 actions: [
76 {
77 name: "S3Source",
78 category: "Source",
79 owner: "AWS",
80 provider: "S3",
81 outputArtifacts: ["SourceOutput"],
82 configuration: {
83 S3Bucket: source.bucketName,
84 S3ObjectKey: objectKey,
85 PollForSourceChanges: "false",
86 },
87 },
88 ],
89 },
90 {
91 name: "Approve",
92 actions: [
93 {
94 name: "ManualApproval",
95 category: "Approval",
96 owner: "AWS",
97 provider: "Manual",
98 },
99 ],
100 },
101 ],
102 });
103 });
104
105test.provider(
106 "lifecycle: create S3-source pipeline, update, destroy",
107 (stack) =>
108 Effect.gen(function* () {
109 yield* stack.destroy();
110
111 // Create.
112 const deployed = yield* stack.deploy(makeStack("source.zip"));
113 expect(deployed.pipelineName).toBe(pipelineName);
114 expect(deployed.pipelineArn).toContain(`:${pipelineName}`);
115
116 // Out-of-band verification via distilled.
117 const created = yield* getPipeline;
118 expect(created?.pipeline?.name).toBe(pipelineName);
119 expect(created?.pipeline?.stages?.length).toBe(2);
120 expect(created?.pipeline?.stages?.[0]?.name).toBe("Source");
121 expect(
122 created?.pipeline?.stages?.[0]?.actions?.[0]?.configuration
123 ?.S3ObjectKey,
124 ).toBe("source.zip");
125
126 // Canonical list() coverage.
127 const provider = yield* Provider.findProvider(Pipeline);
128 const all = yield* provider.list();
129 expect(all.some((p) => p.pipelineName === pipelineName)).toBe(true);
130
131 // Update — change the source object key in place (no replacement).
132 yield* stack.deploy(makeStack("release.zip"));
133 const updated = yield* getPipeline;
134 expect(
135 updated?.pipeline?.stages?.[0]?.actions?.[0]?.configuration
136 ?.S3ObjectKey,
137 ).toBe("release.zip");
138
139 // Destroy — pipeline is deleted; verify it is gone out-of-band.
140 yield* stack.destroy();
141 const after = yield* getPipeline;
142 expect(after).toBeUndefined();
143 }),
144 {
145 tags: [
146 "provider:aws",
147 "provider:aws:codepipeline",
148 "provider:aws:iam",
149 "provider:aws:s3",
150 "live",
151 ],
152 timeout: 300_000,
153 },
154);