1import * as Hetzner from "@/Hetzner";
2import * as Test from "@/Test/Alchemy";
3import * as servers from "@distilled.cloud/hetzner/servers";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import * as pathe from "pathe";
10import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
11import { expectUrlContains } from "../../Cloudflare/Utils/Http.ts";
12
13const { test } = Test.make({ providers: Hetzner.providers() });
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20const hasHetznerCreds = !!process.env.HCLOUD_TOKEN;
21
22const fixtureDir = pathe.resolve(
23 import.meta.dirname,
24 "../../Cloudflare/Website/staticsite-fixture",
25);
26const tempRoot = pathe.resolve(import.meta.dirname, "../../../.tmp");
27
28const waitUntilGone = (id: number) =>
29 servers.getServer({ id }).pipe(
30 Effect.as("found" as const),
31 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
32 Effect.repeat({
33 schedule: Schedule.spaced("1 second"),
34 until: (status) => status === "gone",
35 times: 10,
36 }),
37 );
38
39test.provider.skipIf(!hasHetznerCreds)(
40 "StaticSite: build command + outdir, GET index",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44
45 const cwd = yield* cloneFixture(fixtureDir, {
46 prefix: "alchemy-staticsite-hetzner-",
47 tempRoot,
48 entries: ["src", "build.sh"],
49 });
50
51 const deployed = yield* stack.deploy(
52 Effect.gen(function* () {
53 const site = yield* Hetzner.Website.StaticSite("Blog", {
54 cwd,
55 command: "bash build.sh",
56 outdir: "dist",
57 });
58 return { site };
59 }),
60 );
61
62 const url = deployed.site.url;
63 expect(url).toBeDefined();
64 expect(url).toMatch(/^http:\/\//);
65 expect(deployed.site.service).toBeDefined();
66 expect(deployed.site.server).toBeDefined();
67
68 yield* expectUrlContains(`${url!}/`, "StaticSite fixture v1", {
69 timeout: "90 seconds",
70 label: "staticsite index",
71 });
72
73 const client = yield* HttpClient.HttpClient;
74 const health = yield* client.get(`${url!}/health`).pipe(
75 Effect.flatMap((res) =>
76 res.status === 200
77 ? res.text
78 : Effect.fail(new Error(`health returned ${res.status}`)),
79 ),
80 Effect.retry({
81 schedule: Schedule.exponential("500 millis"),
82 times: 10,
83 }),
84 );
85 expect(health).toContain("ok");
86
87 const serverId = deployed.site.server!.serverId;
88 yield* stack.destroy();
89 const gone = yield* waitUntilGone(serverId);
90 expect(gone).toEqual("gone");
91 }).pipe(
92 logLevel,
93 Effect.ensuring(stack.destroy().pipe(Effect.orDie)),
94 Effect.catchTag(["PreconditionFailed", "Forbidden"], (error) =>
95 Effect.logWarning(`skipping: Hetzner quota (${error._tag})`),
96 ),
97 ),
98 {
99 tags: [
100 "provider:hetzner",
101 "provider:hetzner:service",
102 "provider:hetzner:website",
103 "live",
104 ],
105 timeout: 180_000,
106 },
107);