History

Fix beta distribution release flow

Rhys Sullivan committed 7 months ago

Commit ab58a3e1 parent 17ab9314 files · +209 −54Browse files

Unified · Split

4 files
  1. apps/executor/src/distribution/artifact.ts +72 −13
  2. apps/executor/src/distribution/distribution.test.ts +20 −30
  3. apps/executor/src/distribution/harness.ts +89 −0
  4. plugins/keychain-secret-store/sdk/index.ts +28 −11
apps/executor/src/distribution/artifact.ts+72−13
@@ -1,6 +1,6 @@
11 import { spawn } from "node:child_process";
22 import { existsSync } from "node:fs";
3−import { chmod, cp, mkdir, rm, writeFile } from "node:fs/promises";
3+import { chmod, cp, mkdir, rename, rm, writeFile } from "node:fs/promises";
44 import { createRequire } from "node:module";
55 import { dirname, join, resolve } from "node:path";
66
@@ -136,10 +136,77 @@ const createPackageJson = (input: { · 129 unmodified lines
136136
137137 const createLauncherSource = () => [
138138 "#!/usr/bin/env node",
139− 'import "./executor.mjs";',
139+ 'import { readFileSync, readdirSync } from "node:fs";',
140+ 'import { dirname, join } from "node:path";',
141+ 'import { fileURLToPath } from "node:url";',
142+ "",
143+ "const isMusl = () => {",
144+ ' try {',
145+ ' return readFileSync("/usr/bin/ldd", "utf8").includes("musl");',
146+ " } catch {}",
147+ "",
148+ ' if (typeof process.report?.getReport === "function") {',
149+ " const report = process.report.getReport();",
150+ " if (report?.header?.glibcVersionRuntime) {",
151+ " return false;",
152+ " }",
153+ " if (Array.isArray(report?.sharedObjects)) {",
154+ ' return report.sharedObjects.some((file) => file.includes("libc.musl-") || file.includes("ld-musl-"));',
155+ " }",
156+ " }",
157+ "",
158+ " return false;",
159+ "};",
160+ "",
161+ "const resolveBundledKeyringNativeLibraryPath = () => {",
162+ ' if (process.platform !== "linux" || process.arch !== "x64") {',
163+ " return null;",
164+ " }",
165+ "",
166+ " const binDir = dirname(fileURLToPath(import.meta.url));",
167+ ' const pattern = isMusl()',
168+ ' ? /^keyring\\.linux-x64-musl-.*\\.node$/',
169+ ' : /^keyring\\.linux-x64-gnu-.*\\.node$/;',
170+ ' const match = readdirSync(binDir).find((entry) => pattern.test(entry));',
171+ " return match ? join(binDir, match) : null;",
172+ "};",
173+ "",
174+ "const bundledKeyringNativeLibraryPath = resolveBundledKeyringNativeLibraryPath();",
175+ 'if (bundledKeyringNativeLibraryPath && !process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {',
176+ " process.env.NAPI_RS_NATIVE_LIBRARY_PATH = bundledKeyringNativeLibraryPath;",
177+ "}",
178+ "",
179+ 'await import("./executor.mjs");',
140180 "",
141181 ].join("\n");
142182
183+const buildCliBundle = async (input: {
184+ binDir: string;
185+ bundlePath: string;
186+}): Promise<void> => {
187+ const builtEntrypointPath = join(input.binDir, "main.js");
188+
189+ await runCommand({
190+ command: "bun",
191+ args: [
192+ "build",
193+ "./apps/executor/src/cli/main.ts",
194+ "--target",
195+ "node",
196+ "--outdir",
197+ input.binDir,
198+ ],
199+ cwd: repoRoot,
200+ });
201+
202+ if (!existsSync(builtEntrypointPath)) {
203+ throw new Error(`Missing bundled CLI entrypoint at ${builtEntrypointPath}`);
204+ }
205+
206+ await rm(input.bundlePath, { force: true });
207+ await rename(builtEntrypointPath, input.bundlePath);
208+};
209+
143210 export const buildDistributionPackage = async (
144211 options: BuildDistributionPackageOptions = {},
145212 ): Promise<DistributionPackageArtifact> => {
@@ -172,17 +239,9 @@ export const buildDistributionPackage = async ( · 26 unmodified lines
172239 throw new Error(`Missing built web assets at ${webDistDir}`);
173240 }
174241
175− await runCommand({
176− command: "bun",
177− args: [
178− "build",
179− "./apps/executor/src/cli/main.ts",
180− "--target",
181− "node",
182− "--outfile",
183− bundlePath,
184− ],
185− cwd: repoRoot,
242+ await buildCliBundle({
243+ binDir,
244+ bundlePath,
186245 });
187246
188247 await cp(webDistDir, webDir, { recursive: true });
apps/executor/src/distribution/distribution.test.ts+20−30
@@ -25,37 +25,17 @@ describe("distribution flow", () => { · 24 unmodified lines
2525 }
2626 `);
2727
28− const initialDoctor = yield* runCommand([
29− "doctor",
30− "--json",
31− "--base-url",
32− harness.baseUrl,
33− ]);
34− const initialDoctorJson = JSON.parse(initialDoctor.stdout) as {
35− ok: boolean;
36− checks: Record<string, { ok: boolean }>;
37− };
38− expect(initialDoctorJson.ok).toBe(false);
39− expect(initialDoctorJson.checks.webAssets?.ok).toBe(true);
40− expect(initialDoctorJson.checks.database?.ok).toBe(true);
41−
42− yield* runCommand(["up", "--base-url", harness.baseUrl]);
28+ expect(yield* harness.isReachable()).toBe(false);
4329
44− const statusResult = yield* runCommand([
45− "status",
46− "--json",
30+ const initialCall = yield* runCommand([
31+ "call",
32+ "return 1 + 1;",
4733 "--base-url",
4834 harness.baseUrl,
35+ "--no-open",
4936 ]);
50− const status = JSON.parse(statusResult.stdout) as {
51− reachable: boolean;
52− pidRunning: boolean;
53− installation: { workspaceId: string; accountId: string } | null;
54− };
55−
56− expect(status.reachable).toBe(true);
57− expect(status.pidRunning).toBe(true);
58− expect(status.installation).not.toBeNull();
37+ expect(initialCall.stdout).toBe("2");
38+ expect(yield* harness.isReachable()).toBe(true);
5939
6040 const html = yield* harness.fetchText("/");
6141 expect(html.status).toBe(200);
@@ -80,8 +60,17 @@ describe("distribution flow", () => { · 18 unmodified lines
8060 );
8161 expect(sesCall.stderr).toContain("fetch is disabled in SES executor");
8262
83− yield* runCommand(["down", "--base-url", harness.baseUrl]);
84− yield* runCommand(["up", "--base-url", harness.baseUrl]);
63+ yield* harness.stopServer();
64+ expect(yield* harness.isReachable()).toBe(false);
65+
66+ const restartedCall = yield* runCommand([
67+ "call",
68+ "return 3;",
69+ "--base-url",
70+ harness.baseUrl,
71+ "--no-open",
72+ ]);
73+ expect(restartedCall.stdout).toBe("3");
8574
8675 const installationAfterRestartResponse = yield* harness.fetchText("/v1/local/installation");
8776 expect(installationAfterRestartResponse.status).toBe(200);
@@ -95,7 +84,8 @@ describe("distribution flow", () => { · 7 unmodified lines
9584 expect(installationAfterRestart.scopeId).toBe(installation.scopeId);
9685 expect(installationAfterRestart.actorScopeId).toBe(installation.actorScopeId);
9786
98− yield* runCommand(["down", "--base-url", harness.baseUrl]);
87+ yield* harness.stopServer();
88+ expect(yield* harness.isReachable()).toBe(false);
9989 });
10090
10191 it.live("boots a staged package artifact in a fresh home", () =>
apps/executor/src/distribution/harness.ts+89−0
@@ -53,9 +53,14 @@ export class DistributionHarness extends Context.Tag( · 52 unmodified lines
5353 readonly body: string;
5454 readonly contentType: string | null;
5555 }, Error, never>;
56+ readonly isReachable: () => Effect.Effect<boolean, Error, never>;
57+ readonly stopServer: () => Effect.Effect<void, Error, never>;
5658 }
5759 >() {}
5860
61+const SERVER_WAIT_TIMEOUT_MS = 5_000;
62+const SERVER_POLL_INTERVAL_MS = 100;
63+
5964 const runCommand = (input: {
6065 readonly command: string;
6166 readonly args: ReadonlyArray<string>;
@@ -174,6 +179,36 @@ const packPackage = (packageDir: string, outputDir: string) => · 112 unmodified lines
174179 }),
175180 );
176181
182+const waitForReachability = (input: {
183+ baseUrl: string;
184+ expected: boolean;
185+}): Effect.Effect<void, Error, never> =>
186+ Effect.gen(function* () {
187+ const startedAt = Date.now();
188+
189+ while (Date.now() - startedAt < SERVER_WAIT_TIMEOUT_MS) {
190+ const reachable = yield* Effect.tryPromise({
191+ try: async () => {
192+ const response = await fetch(new URL("/", input.baseUrl));
193+ return response.ok;
194+ },
195+ catch: toError,
196+ }).pipe(Effect.catchAll(() => Effect.succeed(false)));
197+
198+ if (reachable === input.expected) {
199+ return;
200+ }
201+
202+ yield* Effect.sleep(`${SERVER_POLL_INTERVAL_MS} millis`);
203+ }
204+
205+ return yield* Effect.fail(
206+ new Error(
207+ `Timed out waiting for executor server to become ${input.expected ? "reachable" : "unreachable"} at ${input.baseUrl}`,
208+ ),
209+ );
210+ });
211+
177212 export const LocalDistributionHarnessLive = Layer.scoped(
178213 DistributionHarness,
179214 Effect.gen(function* () {
@@ -264,6 +299,58 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 84 unmodified lines
264299 catch: (cause) => cause instanceof Error ? cause : new Error(String(cause)),
265300 });
266301
302+ const isReachable = () =>
303+ Effect.tryPromise({
304+ try: async () => {
305+ const response = await fetch(new URL("/", baseUrl));
306+ return response.ok;
307+ },
308+ catch: toError,
309+ }).pipe(Effect.catchAll(() => Effect.succeed(false)));
310+
311+ const stopServer = () =>
312+ Effect.gen(function* () {
313+ const pidFile = join(executorHome, "run", "server.pid");
314+ const exists = yield* fs.exists(pidFile).pipe(Effect.mapError(toError));
315+ if (!exists) {
316+ return;
317+ }
318+
319+ const contents = yield* fs.readFileString(pidFile).pipe(
320+ Effect.mapError(toError),
321+ );
322+ const parsed = JSON.parse(contents) as { pid?: number };
323+ const pid = parsed.pid;
324+
325+ if (typeof pid === "number" && Number.isInteger(pid) && pid > 0) {
326+ yield* Effect.try({
327+ try: () => process.kill(pid, "SIGTERM"),
328+ catch: toError,
329+ }).pipe(Effect.catchAll(() => Effect.void));
330+
331+ const stopped = yield* waitForReachability({
332+ baseUrl,
333+ expected: false,
334+ }).pipe(
335+ Effect.as(true),
336+ Effect.catchAll(() => Effect.succeed(false)),
337+ );
338+ if (stopped) {
339+ return;
340+ }
341+
342+ yield* Effect.try({
343+ try: () => process.kill(pid, "SIGKILL"),
344+ catch: toError,
345+ }).pipe(Effect.catchAll(() => Effect.void));
346+ }
347+
348+ yield* waitForReachability({
349+ baseUrl,
350+ expected: false,
351+ });
352+ });
353+
267354 const writeProjectConfig = (contents: string) =>
268355 Effect.forEach(
269356 [stagedWorkspaceRoot, installedWorkspaceRoot],
@@ -291,6 +378,8 @@ export const LocalDistributionHarnessLive = Layer.scoped( · 21 unmodified lines
291378 run,
292379 runInstalled,
293380 fetchText,
381+ isReachable,
382+ stopServer,
294383 });
295384 }).pipe(Effect.provide(NodeFileSystem.layer)),
296385 );
plugins/keychain-secret-store/sdk/index.ts+28−11
@@ -1,7 +1,4 @@
11 import { randomUUID } from "node:crypto";
2−import {
3− Entry,
4−} from "@napi-rs/keyring";
52
63 import * as Effect from "effect/Effect";
74
@@ -51,12 +48,13 @@ const keychainDisplayName = () => · 43 unmodified lines
5148 ? "Windows Credential Manager"
5249 : "Desktop Keyring";
5350
54−const createKeyringEntry = (input: {
55− providerHandle: string;
56− keychainServiceName: string;
57−}) =>
58− Effect.try({
59− try: () => {
51+let keyringEntryConstructorPromise: Promise<
52+ (typeof import("@napi-rs/keyring"))["Entry"]
53+> | null = null;
54+
55+const loadKeyringEntryConstructor = () =>
56+ Effect.tryPromise({
57+ try: async () => {
6058 if (!isSupportedPlatform()) {
6159 throw runtimeEffectError(
6260 "plugin-keychain-secret-store",
@@ -64,11 +62,30 @@ const createKeyringEntry = (input: { · 1 unmodified line
6462 );
6563 }
6664
67− return new Entry(input.keychainServiceName, input.providerHandle);
65+ keyringEntryConstructorPromise ??= import("@napi-rs/keyring").then(
66+ ({ Entry }) => Entry,
67+ );
68+
69+ return await keyringEntryConstructorPromise;
6870 },
69− catch: toError,
71+ catch: (cause) =>
72+ runtimeEffectError(
73+ "plugin-keychain-secret-store",
74+ `system-keyring: failed loading native keyring support: ${toError(cause).message}`,
75+ ),
7076 });
7177
78+const createKeyringEntry = (input: {
79+ providerHandle: string;
80+ keychainServiceName: string;
81+}) =>
82+ Effect.flatMap(loadKeyringEntryConstructor(), (Entry) =>
83+ Effect.try({
84+ try: () => new Entry(input.keychainServiceName, input.providerHandle),
85+ catch: toError,
86+ })
87+ );
88+
7289 const readKeychainSecretValue = (input: {
7390 providerHandle: string;
7491 keychainServiceName: string;