From 3f9f8bab64789babeaa4c8cfa75c2f98fc8e723c Mon Sep 17 00:00:00 2001
From: badcuban <108198679+badcuban@users.noreply.github.com>
Date: Wed, 12 Aug 2026 18:48:13 -0400
Subject: [PATCH] Run staged install before Electron packaging
---
scripts/build-desktop-artifact.test.ts | 31 +++++++++++++++++++++
scripts/build-desktop-artifact.ts | 38 ++++++++++++++------------
2 files changed, 52 insertions(+), 17 deletions(-)
diff --git a/scripts/build-desktop-artifact.test.ts b/scripts/build-desktop-artifact.test.ts
index b956562fa..b4ee21712 100644
--- a/scripts/build-desktop-artifact.test.ts
+++ b/scripts/build-desktop-artifact.test.ts
@@ -18,6 +18,7 @@ import {
filterPatchedDependenciesForStage,
resolveMockUpdateServerPort,
resolveMockUpdateServerUrl,
+ retryTransientElectronPackaging,
createDesktopArtifactBuildEnv,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
@@ -90,6 +91,36 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
);
});
+ it.effect("retries Electron packaging only after transient network failures", () =>
+ Effect.gen(function* () {
+ let attempts = 0;
+ const result = yield* retryTransientElectronPackaging(
+ Effect.suspend(() => {
+ attempts += 1;
+ return attempts < 3 ? Effect.fail(new Error("socket hang up")) : Effect.succeed("built");
+ }),
+ );
+
+ assert.equal(result, "built");
+ assert.equal(attempts, 3);
+ }),
+ );
+
+ it.effect("does not retry non-network packaging failures", () =>
+ Effect.gen(function* () {
+ let attempts = 0;
+ const exit = yield* retryTransientElectronPackaging(
+ Effect.suspend(() => {
+ attempts += 1;
+ return Effect.fail(new Error("production dependency not found"));
+ }),
+ ).pipe(Effect.exit);
+
+ assert.equal(exit._tag, "Failure");
+ assert.equal(attempts, 1);
+ }),
+ );
+
it("falls back to the default mock update port when the configured port is blank", () => {
assert.equal(resolveMockUpdateServerUrl(undefined), "http://localhost:3000");
assert.equal(resolveMockUpdateServerUrl(4123), "http://localhost:4123");
diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts
index a007782e6..d5f90578b 100644
--- a/scripts/build-desktop-artifact.ts
+++ b/scripts/build-desktop-artifact.ts
@@ -32,7 +32,6 @@ import * as Logger from "effect/Logger";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
-import * as Schedule from "effect/Schedule";
import * as Stream from "effect/Stream";
import { Command, Flag } from "effect/unstable/cli";
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
@@ -543,6 +542,24 @@ const runCommand = Effect.fn("runCommand")(function* (command: ChildProcess.Comm
}
});
+export function retryTransientElectronPackaging(
+ packaging: Effect.Effect,
+): Effect.Effect {
+ return packaging.pipe(
+ Effect.tapError((error) =>
+ isTransientNetworkFailure(error)
+ ? Effect.logWarning(
+ "[desktop-artifact] Electron packaging hit a transient network failure; bounded retry policy active.",
+ )
+ : Effect.void,
+ ),
+ Effect.retry({
+ times: 2,
+ while: isTransientNetworkFailure,
+ }),
+ );
+}
+
const MAC_ICON_CANVAS_SIZE = 1024;
const MAC_ICON_VISIBLE_SIZE = 824;
@@ -1180,7 +1197,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
}
yield* Effect.log("[desktop-artifact] Installing staged production dependencies...");
- const electronBuilderCommand = runCommand(
+ yield* runCommand(
ChildProcess.make({
cwd: stageAppDir,
...commandOutputOptions(options.verbose),
@@ -1229,7 +1246,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
yield* Effect.log(
`[desktop-artifact] Building ${options.platform}/${options.target} (arch=${options.arch}, version=${appVersion})...`,
);
- yield* runCommand(
+ const electronBuilderCommand = runCommand(
ChildProcess.make({
cwd: repoRoot,
env: buildEnv,
@@ -1238,20 +1255,7 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
shell: process.platform === "win32",
})`${vpBinary} exec --filter @threadlines/desktop -- electron-builder --projectDir ${stageAppDir} ${platformConfig.cliFlag} --${options.arch} --publish never`,
);
- yield* electronBuilderCommand.pipe(
- Effect.tapError((error) =>
- isTransientNetworkFailure(error)
- ? Effect.logWarning(
- "[desktop-artifact] Electron packaging hit a transient network failure; bounded retry policy active.",
- )
- : Effect.void,
- ),
- Effect.retry({
- schedule: Schedule.spaced("2 seconds"),
- times: 2,
- while: isTransientNetworkFailure,
- }),
- );
+ yield* retryTransientElectronPackaging(electronBuilderCommand);
const stageDistDir = path.join(stageAppDir, "dist");
if (!(yield* fs.exists(stageDistDir))) {