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))) {