1import * as GCP from "@/GCP";
2import type { StackServices } from "@/Stack";
3import * as Test from "@/Test/Alchemy";
4import * as workflowexecutions from "@distilled.cloud/gcp/workflowexecutions_v1";
5import * as workflows from "@distilled.cloud/gcp/workflows_v1";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({
13 providers: GCP.providers() as Layer.Layer<
14 GCP.ProviderRequirements,
15 never,
16 StackServices
17 >,
18});
19
20const logLevel = Effect.provideService(
21 MinimumLogLevel,
22 process.env.DEBUG ? "Debug" : "Info",
23);
24
25const SOURCE_V1 = `main:
26 steps:
27 - done:
28 return: hello
29`;
30
31const SOURCE_V2 = `main:
32 steps:
33 - done:
34 return: world
35`;
36
37const waitUntilGone = (name: string) =>
38 workflows.getProjectsLocationsWorkflows({ name }).pipe(
39 Effect.as("found" as const),
40 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
41 Effect.repeat({
42 schedule: Schedule.spaced("1 second"),
43 until: (status) => status === "gone",
44 times: 10,
45 }),
46 );
47
48const waitUntilSucceeded = (name: string) =>
49 workflowexecutions.getProjectsLocationsWorkflowsExecutions({ name }).pipe(
50 Effect.repeat({
51 schedule: Schedule.spaced("1 second"),
52 until: (execution) =>
53 execution.state === "SUCCEEDED" ||
54 execution.state === "FAILED" ||
55 execution.state === "CANCELLED",
56 times: 10,
57 }),
58 );
59
60test.provider(
61 "create, update, replace, and delete a workflow",
62 (stack) =>
63 Effect.gen(function* () {
64 yield* stack.destroy();
65
66 const created = yield* stack.deploy(
67 Effect.gen(function* () {
68 return yield* GCP.Workflows.Workflow("Greet", {
69 location: "us-central1",
70 description: "greet",
71 labels: { env: "test" },
72 sourceContents: SOURCE_V1,
73 });
74 }),
75 );
76
77 expect(created.workflowId).toEqual(expect.any(String));
78 expect(created.name).toContain("/workflows/");
79 expect(created.location).toEqual("us-central1");
80 expect(created.description).toEqual("greet");
81 expect(created.labels).toMatchObject({ env: "test" });
82 expect(created.state).toEqual("ACTIVE");
83 expect(created.sourceContents).toContain("hello");
84 expect(created.revisionId).toEqual(expect.any(String));
85
86 const fetched = yield* workflows.getProjectsLocationsWorkflows({
87 name: created.name,
88 });
89 expect(fetched.name).toEqual(created.name);
90 expect(fetched.description).toEqual("greet");
91 expect(fetched.labels?.env).toEqual("test");
92 expect(fetched.state).toEqual("ACTIVE");
93 expect(fetched.sourceContents).toContain("hello");
94 expect(fetched.labels?.["alchemy-id"]).toEqual(expect.any(String));
95
96 const listed = yield* workflows.listProjectsLocationsWorkflows({
97 parent: `projects/${created.project}/locations/${created.location}`,
98 });
99 expect(
100 (listed.workflows ?? []).some(
101 (workflow) => workflow.name === created.name,
102 ),
103 ).toEqual(true);
104
105 const execution =
106 yield* workflowexecutions.createProjectsLocationsWorkflowsExecutions({
107 parent: created.name,
108 body: {},
109 });
110 expect(execution.name).toEqual(expect.any(String));
111 const finished = yield* waitUntilSucceeded(execution.name!);
112 expect(finished.state).toEqual("SUCCEEDED");
113 expect(finished.result).toContain("hello");
114
115 const updated = yield* stack.deploy(
116 Effect.gen(function* () {
117 return yield* GCP.Workflows.Workflow("Greet", {
118 workflowId: created.workflowId,
119 location: "us-central1",
120 description: "greet v2",
121 labels: { env: "prod", role: "greeter" },
122 callLogLevel: "LOG_ERRORS_ONLY",
123 userEnvVars: { greeting: "world" },
124 sourceContents: SOURCE_V2,
125 });
126 }),
127 );
128
129 expect(updated.name).toEqual(created.name);
130 expect(updated.workflowId).toEqual(created.workflowId);
131 expect(updated.description).toEqual("greet v2");
132 expect(updated.labels).toMatchObject({ env: "prod", role: "greeter" });
133 expect(updated.callLogLevel).toEqual("LOG_ERRORS_ONLY");
134 expect(updated.userEnvVars).toMatchObject({ greeting: "world" });
135 expect(updated.sourceContents).toContain("world");
136 expect(updated.revisionId).not.toEqual(created.revisionId);
137 expect(updated.state).toEqual("ACTIVE");
138
139 const fetchedUpdate = yield* workflows.getProjectsLocationsWorkflows({
140 name: updated.name,
141 });
142 expect(fetchedUpdate.description).toEqual("greet v2");
143 expect(fetchedUpdate.labels?.env).toEqual("prod");
144 expect(fetchedUpdate.labels?.role).toEqual("greeter");
145 expect(fetchedUpdate.callLogLevel).toEqual("LOG_ERRORS_ONLY");
146 expect(fetchedUpdate.userEnvVars?.greeting).toEqual("world");
147 expect(fetchedUpdate.sourceContents).toContain("world");
148 expect(fetchedUpdate.state).toEqual("ACTIVE");
149
150 const replaced = yield* stack.deploy(
151 Effect.gen(function* () {
152 return yield* GCP.Workflows.Workflow("Greet", {
153 workflowId: created.workflowId,
154 location: "us-east1",
155 description: "greet east",
156 labels: { env: "test" },
157 sourceContents: SOURCE_V1,
158 });
159 }),
160 );
161
162 expect(replaced.workflowId).toEqual(created.workflowId);
163 expect(replaced.location).toEqual("us-east1");
164 expect(replaced.name).toContain("/locations/us-east1/");
165 expect(replaced.name).not.toEqual(created.name);
166 expect(replaced.state).toEqual("ACTIVE");
167
168 const oldGone = yield* waitUntilGone(created.name);
169 expect(oldGone).toEqual("gone");
170
171 const fetchedReplace = yield* workflows.getProjectsLocationsWorkflows({
172 name: replaced.name,
173 });
174 expect(fetchedReplace.name).toEqual(replaced.name);
175 expect(fetchedReplace.state).toEqual("ACTIVE");
176 expect(fetchedReplace.sourceContents).toContain("hello");
177
178 yield* stack.destroy();
179
180 const gone = yield* waitUntilGone(replaced.name);
181 expect(gone).toEqual("gone");
182 }).pipe(logLevel),
183 {
184 tags: ["provider:gcp", "provider:gcp:workflows", "live"],
185 timeout: 120_000,
186 },
187);