1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as cloudbuild from "@distilled.cloud/gcp/cloudbuild_v2";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16const remoteUri =
17 process.env.GCP_CLOUDBUILD_REMOTE_URI ??
18 "https://github.com/octocat/Hello-World.git";
19const standingConnection = process.env.GCP_CLOUDBUILD_CONNECTION;
20const runLifecycle = !!standingConnection && !process.env.FAST;
21
22const waitUntilRepositoryGone = (name: string) =>
23 cloudbuild.getProjectsLocationsConnectionsRepositories({ name }).pipe(
24 Effect.as("found" as const),
25 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
26 Effect.repeat({
27 schedule: Schedule.spaced("1 second"),
28 until: (status) => status === "gone",
29 times: 10,
30 }),
31 );
32
33const waitUntilConnectionGone = (name: string) =>
34 cloudbuild.getProjectsLocationsConnections({ name }).pipe(
35 Effect.as("found" as const),
36 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
37 Effect.repeat({
38 schedule: Schedule.spaced("1 second"),
39 until: (status) => status === "gone",
40 times: 10,
41 }),
42 );
43
44test.provider(
45 "get and binding ops on a missing repository are NotFound",
46 (stack) =>
47 Effect.gen(function* () {
48 const { project } = yield* GcpEnvironment.current;
49 const missingRepositoryName = `projects/${project}/locations/us-central1/connections/alchemy-missing-connection/repositories/alchemy-missing-repository`;
50 yield* stack.destroy();
51
52 const getError = yield* Effect.flip(
53 cloudbuild.getProjectsLocationsConnectionsRepositories({
54 name: missingRepositoryName,
55 }),
56 );
57 expect(getError._tag).toBe("NotFound");
58
59 const readError = yield* Effect.flip(
60 cloudbuild.accessReadTokenProjectsLocationsConnectionsRepositories({
61 repository: missingRepositoryName,
62 body: {},
63 }),
64 );
65 expect(readError._tag).toBe("NotFound");
66
67 const writeError = yield* Effect.flip(
68 cloudbuild.accessReadWriteTokenProjectsLocationsConnectionsRepositories(
69 {
70 repository: missingRepositoryName,
71 body: {},
72 },
73 ),
74 );
75 expect(writeError._tag).toBe("NotFound");
76
77 const refsError = yield* Effect.flip(
78 cloudbuild.fetchGitRefsProjectsLocationsConnectionsRepositories({
79 repository: missingRepositoryName,
80 refType: "BRANCH",
81 }),
82 );
83 expect(refsError._tag).toBe("NotFound");
84
85 yield* stack.destroy();
86 }).pipe(logLevel),
87 {
88 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
89 timeout: 90_000,
90 },
91);
92
93test.provider(
94 "create repository on a pending GitHub connection is BadRequest",
95 (stack) =>
96 Effect.gen(function* () {
97 yield* stack.destroy();
98
99 const connection = yield* stack.deploy(
100 Effect.gen(function* () {
101 return yield* GCP.CloudBuild.Connection("Github", {
102 location: "us-central1",
103 githubConfig: {},
104 });
105 }),
106 );
107
108 const error = yield* Effect.flip(
109 cloudbuild.createProjectsLocationsConnectionsRepositories({
110 parent: connection.name,
111 repositoryId: "alchemy-probe-source",
112 body: { remoteUri },
113 }),
114 );
115 expect(error._tag).toBe("BadRequest");
116 expect(error.message.toLowerCase()).toContain("installation");
117
118 yield* stack.destroy();
119
120 const gone = yield* waitUntilConnectionGone(connection.name);
121 expect(gone).toEqual("gone");
122 }).pipe(logLevel),
123 {
124 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
125 timeout: 90_000,
126 },
127);
128
129test.provider.skipIf(!runLifecycle)(
130 "create, replace, and delete a repository",
131 (stack) =>
132 Effect.gen(function* () {
133 yield* stack.destroy();
134
135 const created = yield* stack.deploy(
136 Effect.gen(function* () {
137 return yield* GCP.CloudBuild.Repository("Source", {
138 connection: standingConnection!,
139 remoteUri,
140 annotations: { env: "test" },
141 });
142 }),
143 );
144
145 expect(created.name).toContain("/repositories/");
146 expect(created.repositoryId).toEqual(expect.any(String));
147 expect(created.connection).toContain("/connections/");
148 expect(created.location).toEqual(expect.any(String));
149 expect(created.remoteUri).toEqual(remoteUri);
150 expect(created.annotations).toMatchObject({ env: "test" });
151
152 const fetched =
153 yield* cloudbuild.getProjectsLocationsConnectionsRepositories({
154 name: created.name,
155 });
156 expect(fetched.name).toEqual(created.name);
157 expect(fetched.remoteUri).toEqual(remoteUri);
158 expect(fetched.annotations?.env).toEqual("test");
159 expect(fetched.annotations?.["alchemy-id"]).toEqual(expect.any(String));
160
161 const listed =
162 yield* cloudbuild.listProjectsLocationsConnectionsRepositories({
163 parent: created.connection,
164 });
165 expect(
166 (listed.repositories ?? []).some(
167 (repository) => repository.name === created.name,
168 ),
169 ).toEqual(true);
170
171 const readToken =
172 yield* cloudbuild.accessReadTokenProjectsLocationsConnectionsRepositories(
173 {
174 repository: created.name,
175 body: {},
176 },
177 );
178 expect(readToken.token).toEqual(expect.any(String));
179 expect((readToken.token ?? "").length).toBeGreaterThan(0);
180
181 const writeToken =
182 yield* cloudbuild.accessReadWriteTokenProjectsLocationsConnectionsRepositories(
183 {
184 repository: created.name,
185 body: {},
186 },
187 );
188 expect(writeToken.token).toEqual(expect.any(String));
189 expect((writeToken.token ?? "").length).toBeGreaterThan(0);
190
191 const refs =
192 yield* cloudbuild.fetchGitRefsProjectsLocationsConnectionsRepositories({
193 repository: created.name,
194 refType: "BRANCH",
195 });
196 // Every repository has at least its default branch.
197 expect(refs.refNames?.length ?? 0).toBeGreaterThan(0);
198
199 const replaced = yield* stack.deploy(
200 Effect.gen(function* () {
201 return yield* GCP.CloudBuild.Repository("Source", {
202 connection: standingConnection!,
203 repositoryId: created.repositoryId,
204 remoteUri,
205 annotations: { env: "prod", role: "source" },
206 });
207 }),
208 );
209
210 expect(replaced.repositoryId).toEqual(created.repositoryId);
211 expect(replaced.connection).toEqual(created.connection);
212 expect(replaced.annotations).toMatchObject({
213 env: "prod",
214 role: "source",
215 });
216
217 const fetchedReplace =
218 yield* cloudbuild.getProjectsLocationsConnectionsRepositories({
219 name: replaced.name,
220 });
221 expect(fetchedReplace.annotations?.env).toEqual("prod");
222 expect(fetchedReplace.annotations?.role).toEqual("source");
223
224 yield* stack.destroy();
225
226 const gone = yield* waitUntilRepositoryGone(replaced.name);
227 expect(gone).toEqual("gone");
228 }).pipe(logLevel),
229 {
230 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
231 timeout: 90_000,
232 },
233);