Skip to content

Commit 288b5b3

Browse files
mschileclaude
andcommitted
chore: retry and cache the Electron zip download during CI builds
The "Build packages" step fails intermittently on every branch when the Electron zip download from GitHub's release CDN is reset mid-stream: RequestError: socket hang up ... code: 'ECONNRESET' url: .../download/v37.6.0/electron-v37.6.0-linux-x64.zip @electron/packager fetches that zip through @electron/get, which downloads via got.stream(). got does not retry streams — its retry config is present but inert there — so a single reset becomes an unhandled rejection that kills the build. @electron/get exposes no retry option of its own. Call downloadArtifact() directly so the download can be retried with backoff, then hand the file to the packager via electronZipDir so it skips its own download. The retry also covers the SHASUMS256.txt fetch, which @electron/get re-requests on every run even on a cache hit. Also cache the download across builds. @electron/get's default cache root is platform-specific, so the build job pins it via electron_config_cache (the same variable the electron package's own installer honors) to keep a single save_cache path valid on all five platform executors. The cache key tracks the pinned Electron version, so it invalidates on an upgrade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4169413 commit 288b5b3

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

.circleci/src/pipeline/@pipeline.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,11 +329,29 @@ commands:
329329
build-and-persist:
330330
description: Save entire folder as artifact for other jobs to run without reinstalling
331331
steps:
332+
- run:
333+
name: Generate Electron version key
334+
command: node -p "require('./package.json').devDependencies.electron" > electron_version
335+
- restore_cache:
336+
name: Restore Electron download cache
337+
keys:
338+
- v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key"
339+
}}-electron-download-cache-{{ checksum "electron_version" }}
332340
- run:
333341
name: Build packages
334342
command: |
335343
source ./scripts/ensure-node.sh
344+
# @packages/electron forwards this to @electron/get, whose default
345+
# cache root is platform-specific — pinning it keeps a single
346+
# save_cache path valid on every executor
347+
export electron_config_cache="$HOME/.electron-cache"
336348
yarn build
349+
- save_cache:
350+
name: Save Electron download cache
351+
key: v{{ checksum ".circleci/cache-version.txt" }}-{{ checksum "platform_key"
352+
}}-electron-download-cache-{{ checksum "electron_version" }}
353+
paths:
354+
- ~/.electron-cache
337355
- run:
338356
name: Sync Cloud Validations
339357
command: |

packages/electron/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
},
2424
"devDependencies": {
2525
"@electron/fuses": "1.8.0",
26+
"@electron/get": "^4.0.1",
2627
"@electron/packager": "18.3.6",
2728
"@vitest/coverage-v8": "^3.2.4",
2829
"eslint": "^9.31.0",

packages/electron/src/install.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,35 @@ interface PkgElectronAppOptions {
135135
overwrite: boolean
136136
electronVersion: string
137137
icon: string
138+
electronZipDir?: string
139+
}
140+
141+
// @electron/packager would fetch this through @electron/get, which never
142+
// retries and re-fetches SHASUMS256.txt even on cache hits. Calling it here
143+
// lets us retry; electronZipDir then points the packager at the result.
144+
async function downloadElectronZipWithRetries (options: { version: string, platform: string, arch: string }): Promise<string> {
145+
// required dynamically for the same mksnapshot reason as @electron/packager below
146+
const e = 'electron'
147+
const { downloadArtifact } = require(`@${e}/get`)
148+
const retryDelaysMs = [2000, 4000, 8000, 16000, 32000]
149+
150+
// same env var the `electron` package's own installer honors, so CI can point
151+
// both at one cacheable directory. Undefined falls back to the platform default.
152+
const cacheRoot = process.env.electron_config_cache
153+
154+
for (let attempt = 0; ; attempt++) {
155+
try {
156+
return await downloadArtifact({ artifactName: 'electron', cacheRoot, ...options })
157+
} catch (err) {
158+
const delayMs = retryDelaysMs[attempt]
159+
160+
if (delayMs === undefined) throw err
161+
162+
debug('electron zip download failed %o', { attempt, err })
163+
console.log(`Electron download failed (${(err as Error).message}), retrying in ${delayMs / 1000}s (attempt ${attempt + 1} of ${retryDelaysMs.length})`)
164+
await new Promise((resolve) => setTimeout(resolve, delayMs))
165+
}
166+
}
138167
}
139168

140169
async function pkgElectronApp (
@@ -180,6 +209,14 @@ async function pkgElectronApp (
180209
...options,
181210
}
182211

212+
const zipPath = await downloadElectronZipWithRetries({
213+
version: resolvedOptions.electronVersion,
214+
platform: resolvedOptions.platform,
215+
arch: resolvedOptions.arch,
216+
})
217+
218+
resolvedOptions.electronZipDir = path.dirname(zipPath)
219+
183220
debug('packager options %j', resolvedOptions)
184221
const [appPath] = await pkgr(resolvedOptions)
185222

0 commit comments

Comments
 (0)