1import * as machines from "@distilled.cloud/fly-io/machines";
2import * as Fly from "@/Fly";
3import { sameStopConfig } from "@/Fly/replicas.ts";
4import * as Test from "@/Test/Alchemy";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as FileSystem from "effect/FileSystem";
8import * as Path from "effect/Path";
9import { MinimumLogLevel } from "effect/References";
10import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
11import {
12 assertAppGone,
13 assertMachineGone,
14 assertOnlyMachine,
15 getText,
16 initialText,
17 startTraffic,
18 websiteChecks,
19 websiteServices,
20} from "./fixtures/deployment.ts";
21
22const { test } = Test.make({ providers: Fly.providers() });
23const logLevel = Effect.provideService(
24 MinimumLogLevel,
25 process.env.DEBUG ? "Debug" : "Info",
26);
27
28test.provider(
29 "R08/R02 StaticSite: build v1 to v2 under un-retried traffic, forwarded policies, destroy, gone",
30 (stack) =>
31 Effect.gen(function* () {
32 yield* stack.destroy();
33 const fs = yield* FileSystem.FileSystem;
34 const path = yield* Path.Path;
35 const cwd = yield* cloneFixture(
36 path.resolve(
37 import.meta.dirname,
38 "../../Cloudflare/Website/staticsite-fixture",
39 ),
40 {
41 prefix: "alchemy-staticsite-fly-",
42 tempRoot: path.resolve(import.meta.dirname, "../../../.tmp"),
43 entries: ["src", "build.sh"],
44 },
45 );
46 const deploy = () =>
47 stack.deploy(
48 Effect.gen(function* () {
49 const site = yield* Fly.Website.StaticSite("Blog", {
50 path: cwd,
51 build: { command: "bash build.sh", output: "dist" },
52 memo: { include: ["src/**", "build.sh"] },
53 deploy: { strategy: "bluegreen", healthTimeout: "90 seconds" },
54 shutdown: { signal: "SIGTERM", timeout: "10 seconds" },
55 checks: websiteChecks("/index.html"),
56 services: websiteServices("/index.html"),
57 });
58 return { site };
59 }),
60 );
61 let appName: string | undefined;
62 yield* Effect.gen(function* () {
63 const first = yield* deploy();
64 appName = first.site.app!.appName;
65 const url = first.site.url!;
66 const oldId = first.site.service!.machineId;
67 expect(url).toBe(`https://${appName}.fly.dev`);
68 const firstBody = yield* initialText(`${url}/`);
69 expect(firstBody).toContain("StaticSite fixture v1");
70 const observed = yield* machines.getMachine({
71 app_name: appName,
72 machine_id: oldId,
73 });
74 expect(observed.config?.metadata?.["alchemy.phase"]).toBe("active");
75 expect(observed.config?.stop_config?.signal).toBe("SIGTERM");
76 expect(
77 sameStopConfig(observed.config?.stop_config, {
78 signal: "SIGTERM",
79 timeout: "10000ms",
80 }),
81 ).toBe(true);
82 expect(observed.config?.checks?.website?.path).toBe("/index.html");
83 expect(observed.config?.services?.[0]?.checks?.[0]?.path).toBe(
84 "/index.html",
85 );
86 expect(
87 observed.config?.env?.ALCHEMY_FLY_SHUTDOWN_TIMEOUT_MS,
88 ).toBeUndefined();
89 const traffic = yield* startTraffic(`${url}/`);
90 yield* traffic.waitFor((body) =>
91 body.includes("StaticSite fixture v1"),
92 );
93 const index = path.join(cwd, "src/index.html");
94 yield* fs.writeFileString(
95 index,
96 (yield* fs.readFileString(index)).replaceAll(
97 "fixture v1",
98 "fixture v2",
99 ),
100 );
101 const second = yield* deploy();
102 const newId = second.site.service!.machineId;
103 expect(
104 yield* fs.readFileString(path.join(cwd, "dist/index.html")),
105 ).toContain("StaticSite fixture v2");
106 expect(second.site.service!.code.hash).not.toBe(
107 first.site.service!.code.hash,
108 );
109 const updated = yield* machines.getMachine({
110 app_name: appName,
111 machine_id: newId,
112 });
113 expect(updated.config?.image).not.toBe(observed.config?.image);
114 expect(second.site.url).toBe(url);
115 expect(newId).not.toBe(oldId);
116 const secondBody = yield* getText(`${url}/`);
117 expect(secondBody).toContain("StaticSite fixture v2");
118 yield* traffic.waitFor((body) =>
119 body.includes("StaticSite fixture v2"),
120 );
121 const samples = yield* traffic.finish;
122 for (const body of samples)
123 expect([firstBody, secondBody]).toContain(body);
124 yield* assertMachineGone(appName, oldId);
125 yield* assertOnlyMachine(appName, newId);
126 }).pipe(
127 Effect.scoped,
128 Effect.ensuring(
129 Effect.gen(function* () {
130 yield* stack.destroy();
131 if (appName) yield* assertAppGone(appName);
132 }).pipe(Effect.orDie),
133 ),
134 );
135 }).pipe(logLevel),
136 // Two build-command/image deployments, proxy overlap, and final cloud census.
137 {
138 tags: [
139 "provider:fly",
140 "provider:fly:machine",
141 "provider:fly:website",
142 "live",
143 ],
144 timeout: 720_000,
145 },
146);