Skip to content

chore: retry and cache the Electron zip download during CI builds - #34585

Open
mschile wants to merge 2 commits into
developfrom
mschile/confident-hopper-b1c094
Open

chore: retry and cache the Electron zip download during CI builds#34585
mschile wants to merge 2 commits into
developfrom
mschile/confident-hopper-b1c094

Conversation

@mschile

@mschile mschile commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator
  • Closes

Additional details

The build job's "Build packages" step has been failing intermittently across all branches (internal-pr-build, linux-arm64-build, and the develop platform builds) while downloading the Electron zip:

RequestError: socket hang up
  code: 'ECONNRESET',
  url: https://github.com/electron/electron/releases/download/v37.6.0/electron-v37.6.0-linux-x64.zip

Why a single reset kills the build. @electron/packager fetches that zip through @electron/get, whose GotDownloader downloads via got.stream(). got does not retry streams — the retry block visible in the error dump (limit: 2, with ECONNRESET in errorCodes) is present but never consulted, which is what makes this failure look so confusing. Confirmed empirically against the installed got 14.4.7, using a local server that resets every connection:

got.stream  retry.limit=3 -> server hits: 1
got(url)    retry.limit=3 -> server hits: 4

This is deliberate on got's part: on a stream failure it emits a retry event with a createRetryStream callback and expects the caller to re-create the request, since it can't know whether bytes were already piped to a destination. @electron/get never listens for that event, and exposes no retry option of its own — its full option surface is unsafelyDisableChecksums, checksums, cacheRoot, downloadOptions, mirrorOptions, downloader, tempDirectory, cacheMode, platform, arch, artifactSuffix, isGeneric. So there is no option to pass; the only supported extension point is supplying a whole custom Downloader class, which is strictly more code for the same result.

The change. @packages/electron now calls downloadArtifact() itself with backoff retries (2s/4s/8s/16s/32s), then hands the file to the packager via electronZipDir so the packager skips its own download entirely. Same library, same URL, same cache as before — the only difference is that the call is now ours, which is what makes it retryable. The retry also covers the SHASUMS256.txt request that @electron/get re-issues on every run, including cache hits (it passes cacheMode: Bypass for checksums), so caching alone could not have closed this.

Caching. The build job now also caches the download between runs. @electron/get's default cache root is platform-specific (~/.cache/electron, ~/Library/Caches/electron, %LOCALAPPDATA%), and the build job is shared by five executors (docker, Linux ARM VM, two macOS VMs, Windows VM), so a single save_cache path would not have covered them. The step pins the root via electron_config_cache — the same variable the electron npm package's own installer honors (node_modules/electron/install.js) — and @packages/electron forwards it as cacheRoot. The cache key tracks the pinned Electron version, so an Electron upgrade invalidates it with no manual coordination. On a warm cache the ~110MB transfer disappears entirely.

Note that the Lerna (powered by Nx) The task graph has a circular dependency notice appearing above these failures is unrelated and benign — it is nx's warn branch (lerna passes nxIgnoreCycles, nx calls makeAcyclic() and continues), and the same logs go on to report Successfully ran target build for 44 projects. The fatal variant reads "Could not execute command because the task graph has a circular dependency". No change was made there.


Note

Low Risk
Changes are limited to CI and Electron binary packaging; no runtime, API, or user-facing behavior is affected.

Overview
Addresses intermittent CI failures when downloading the Electron release zip (ECONNRESET / socket hang up) by owning the download instead of relying on @electron/packager@electron/get, which does not retry streamed downloads.

@packages/electron now calls downloadArtifact() with exponential backoff (2s–32s, five retries), honors electron_config_cache for the cache root, then passes electronZipDir so the packager skips its own fetch. @electron/get is added as a dev dependency (dynamic require preserved for mksnapshot).

CircleCI build-and-persist restores and saves ~/.electron-cache keyed by platform and pinned Electron version, and sets electron_config_cache during yarn build so all executors share one cache path.

Reviewed by Cursor Bugbot for commit d8b3152. Bugbot is set up for automated code reviews on this repo. Configure here.

Steps to test

CI itself is the primary test — the build job should now log Restore Electron download cache / Save Electron download cache around "Build packages", and a second run on the same Electron version should hit the cache.

Locally, from the repo root:

  1. Retry path — force every download to fail and confirm the backoff fires and then propagates:

    ELECTRON_MIRROR="http://127.0.0.1:9/" DEBUG=cypress:electron:install yarn workspace @packages/electron build-binary
    

    Expect five Electron download failed (...), retrying in Ns lines at 2/4/8/16/32s, then the error.

  2. Cache path — run twice against a scratch cache root:

    rm -rf /tmp/ci-electron-cache
    electron_config_cache=/tmp/ci-electron-cache DEBUG='@electron/get:index' yarn workspace @packages/electron build-binary
    electron_config_cache=/tmp/ci-electron-cache DEBUG='@electron/get:index' yarn workspace @packages/electron build-binary
    

    First run logs Cache miss and leaves the zip in /tmp/ci-electron-cache; the second logs Cache hit with no zip transfer. Both package the binary successfully.

  3. Normal pathyarn workspace @packages/electron build-binary with no env vars set still uses the platform-default cache root, unchanged from before.

All three were run on this branch, along with @packages/electron build (tsc for ESM + CJS), lint, and unit tests (30 passing), and yarn pack-ci --validate.

How has the user experience changed?

No change. This affects CI build tooling only — no runtime, API, or user-visible behavior is touched, so no changelog entry is included.

PR Tasks

  • [na] Is there an associated issue with maintainer approval for PR submission?
  • [na] Have tests been added/updated?
  • [na] Has a PR for user-facing changes been opened in cypress-documentation?
  • [na] Have API changes been updated in the type definitions?

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>
@mschile
mschile requested a review from a team as a code owner August 12, 2026 23:09
cypress-bot[bot]
cypress-bot Bot previously approved these changes Aug 12, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 288b5b3. Configure here.

Comment thread .circleci/src/pipeline/@pipeline.yml
The health-check job flagged @electron/get as an unused devDependency in
packages/electron. It is used, but through `require(`@${e}/get`)` — the
same computed-string form already used for @electron/packager, which keeps
the module undiscoverable by mksnapshot since it is build-only. knip cannot
resolve that statically.

Add it alongside the existing @electron/packager entry in the package's
ignoreDependencies rather than making the require a plain literal, which
would expose a build dependency to the v8 snapshot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cypress-bot
cypress-bot Bot dismissed their stale review August 12, 2026 23:26

New commits pushed (auto-approval was for 288b5b3, head is now d8b3152). Auto-approval dismissed pending Cursor Bugbot re-review of the new head SHA.

@cypress

cypress Bot commented Aug 12, 2026

Copy link
Copy Markdown

cypress    Run #73582

Run Properties:  status check passed Passed #73582  •  git commit d8b3152d55: chore: tell knip @electron/get is dynamically required
Project cypress
Branch Review mschile/confident-hopper-b1c094
Run status status check passed Passed #73582
Run duration 16m 06s
Commit git commit d8b3152d55: chore: tell knip @electron/get is dynamically required
Committer Matthew Schile
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 14
Tests that did not run due to a developer annotating a test with .skip  Pending 1135
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 27874
View all changes introduced in this branch ↗︎
UI Coverage  67.61%
  Untested elements 23  
  Tested elements 48  
Accessibility  98.99%
  Failed rules  0 critical   3 serious   1 moderate   0 minor
  Failed elements 18  

@mschile mschile self-assigned this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant