Electron App Builds on GitHub Actions
Build Electron installers on GitHub Actions with a three-platform matrix across WarpBuild Linux, Windows, and macOS runners, with per-platform caching.
Last verified:
An Electron app that ships installers for Linux, Windows, and macOS needs a GitHub Actions matrix with one job per platform, because native modules, code signing, and installer formats are produced by platform-specific toolchains. On WarpBuild that matrix is three runs-on labels, warp-ubuntu-latest-x64-8x, warp-windows-latest-x64-8x, and warp-macos-15-arm64-6x, with the cache configured separately on the Windows leg because WarpBuild Cache is not supported on Windows runners.
This page gives the matrix workflow with per-platform cache steps, the runner sizes and per-minute rates for all three platforms, the four bottlenecks that dominate Electron pipelines, and the list-price arithmetic against GitHub-hosted runners of the same shape.
Overview
WarpBuild provides Linux x64, Linux ARM64, macOS, and Windows runners, so all three legs of an Electron release matrix run on the same account with the same billing model. Moving an existing Electron workflow across is a change to the runs-on line in each leg plus the cache step on Windows.
The reason Electron needs the full matrix comes from three separate constraints stacking up.
Native modules are compiled against the Electron ABI for one platform and architecture at a time. A package such as better-sqlite3, serialport, or sharp either downloads a prebuilt binary matching the Electron version and platform, or it compiles locally through node-gyp. Neither path produces a Windows binary from a Linux machine without a full cross toolchain.
Code signing is the harder constraint. Apple signing and notarization run through codesign and notarytool, which exist only on macOS, so the macOS leg cannot move to Linux under any configuration. Windows installer signing expects the Windows signing toolchain and a certificate the runner can reach.
Installer formats then follow from the platform: AppImage, deb, and rpm on Linux, NSIS or MSI on Windows, dmg and zip on macOS. Some of these can be produced off-platform with extra tooling, and the resulting pipeline is fragile enough that most teams stop trying and run three jobs.
Disk is the other thing Electron pipelines run into early. Every packaging target copies the unpacked application directory, and the Electron binary distributions, the packaging toolchains, and the finished installers all land on the same volume. The WarpBuild storage per platform is 150GB SSD on Linux runners, 256GB SSD on Windows runners, and 120GB SSD on the 6 vCPU macOS labels or 270GB SSD on the 12 vCPU macOS labels. The macOS runners come in multiple sizes and configurations per chip, which is where the 120GB and 270GB choice comes from.
One caching note before the workflow. The caching documentation states that WarpBuild Cache is not supported on Windows runners, and it also explains that a cache entry is versioned by the compression tool and paths, so an entry saved on a macOS runner cannot be restored on a Linux runner. Both facts push the same way: give each matrix leg its own cache step and its own key.
Configuration
The workflow below builds and packages all three platforms from one matrix. The Linux and macOS legs use WarpBuilds/cache, the Windows leg uses actions/cache, and each leg caches the npm store plus the Electron and packaging caches at the paths that platform actually uses.
name: electron-release
on:
push:
branches: [main]
pull_request:
jobs:
package:
name: package-${{ matrix.platform }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux
runner: warp-ubuntu-latest-x64-8x
build: npx electron-builder --linux --publish never
- platform: windows
runner: warp-windows-latest-x64-8x
build: npx electron-builder --win --publish never
- platform: macos
runner: warp-macos-15-arm64-6x
build: npx electron-builder --mac --publish never
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Restore caches (Linux)
if: matrix.platform == 'linux'
uses: WarpBuilds/cache@v1
with:
path: |
~/.npm
~/.cache/electron
~/.cache/electron-builder
key: linux-electron-${{ hashFiles('package-lock.json') }}
restore-keys: |
linux-electron-
- name: Restore caches (macOS)
if: matrix.platform == 'macos'
uses: WarpBuilds/cache@v1
with:
path: |
~/.npm
~/Library/Caches/electron
~/Library/Caches/electron-builder
key: macos-electron-${{ hashFiles('package-lock.json') }}
restore-keys: |
macos-electron-
- name: Restore caches (Windows)
if: matrix.platform == 'windows'
uses: actions/cache@v4
with:
path: |
~\AppData\Local\npm-cache
~\AppData\Local\electron\Cache
~\AppData\Local\electron-builder\Cache
key: windows-electron-${{ hashFiles('package-lock.json') }}
restore-keys: |
windows-electron-
- run: npm ci
- name: Build installers
run: ${{ matrix.build }}
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}-installers
path: dist/*.*
if-no-files-found: errorFour details in that file are worth stating explicitly.
fail-fast: false keeps a Windows signing failure from cancelling a macOS notarization that is already halfway through Apple's queue. Without it one flaky leg costs you the whole matrix run.
The cache key hashes package-lock.json and carries a platform prefix, so the three legs never collide. The restore-keys prefix is what makes a dependency bump cheap: on an exact miss the action restores the most recent entry with the same prefix, and the install fetches only what changed.
The signing environment variables are set on every leg and read only where they apply. The Linux packaging step ignores them, so a single env block is simpler than a conditional one.
--publish never keeps packaging separate from release upload. Upload artifacts from the matrix, then run a single release job that collects all three sets, which is far easier to re-run than a matrix leg that half-published.
Sizing
The starting point for an Electron matrix is 8 vCPU on Linux and Windows and 6 vCPU on macOS. The catalog rows and per-minute rates for all three platforms:
| Runner label | OS | vCPU | Memory | Storage | Price per minute |
|---|---|---|---|---|---|
| warp-ubuntu-latest-x64-8x | Ubuntu 24.04 | 8 | 32GB | 150GB SSD | $0.016 |
| warp-ubuntu-latest-x64-16x | Ubuntu 24.04 | 16 | 64GB | 150GB SSD | $0.032 |
| warp-windows-latest-x64-8x | Windows Server 2022 | 8 | 32GB | 256GB SSD | $0.032 |
| warp-windows-latest-x64-16x | Windows Server 2022 | 16 | 64GB | 256GB SSD | $0.064 |
| warp-macos-15-arm64-6x | macOS 15 | 6 | 22GB | 120GB SSD | $0.08 |
| warp-macos-15-arm64-12x | macOS 15 | 12 | 44GB | 270GB SSD | $0.16 |
Size each leg for what it can actually use. The renderer bundle step, whether it runs through Vite, webpack, or esbuild, is the part that scales with cores, so it rewards 16 vCPU on a large application. Native module compilation parallelizes across packages and also rewards cores. Installer compression is the phase where extra cores stop helping, because the compressor for a single target largely runs on one thread while writing a lot of bytes to disk.
Move the macOS leg to warp-macos-15-arm64-12x for disk before you move it for speed. A universal build packaged into dmg and zip targets keeps several copies of the unpacked application at once, and 120GB gets tight on a large application with many targets. The 12 vCPU label carries 270GB.
List prices against GitHub-hosted runners of the same shape
GitHub publishes per-minute list prices on the GitHub Actions minute multipliers reference and runner shapes on the GitHub-hosted runners reference. Both were checked on 2026-08-13.
| Platform | WarpBuild label and shape | WarpBuild rate | GitHub-hosted equivalent | GitHub rate |
|---|---|---|---|---|
| Linux | warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) | $0.016 | 8-core Linux larger runner (8 vCPU, 32 GB) | $0.022 |
| Linux | warp-ubuntu-latest-x64-16x (16 vCPU, 64 GB) | $0.032 | 16-core Linux larger runner (16 vCPU, 64 GB) | $0.042 |
| Windows | warp-windows-latest-x64-8x (8 vCPU, 32 GB) | $0.032 | 8-core Windows larger runner (8 vCPU, 32 GB) | $0.042 |
| Windows | warp-windows-latest-x64-16x (16 vCPU, 64 GB) | $0.064 | 16-core Windows larger runner (16 vCPU, 64 GB) | $0.082 |
| macOS | warp-macos-15-arm64-6x (6 vCPU, 22 GB) | $0.08 | Largest GitHub-hosted macOS ARM64 runner (5 vCPU, 14 GB) | $0.102 |
Stated as arithmetic, warp-ubuntu-latest-x64-8x (8 vCPU, 32 GB) costs $0.016 per minute against $0.022 per minute for the 8-core Linux larger runner (8 vCPU, 32 GB): 27 percent lower list price. warp-windows-latest-x64-8x (8 vCPU, 32 GB) costs $0.032 per minute against $0.042 per minute for the 8-core Windows larger runner: 24 percent lower list price. warp-macos-15-arm64-6x (6 vCPU, 22 GB) costs $0.08 per minute against $0.102 per minute for the largest GitHub-hosted macOS ARM64 runner (5 vCPU, 14 GB): 22 percent lower list price, with one more vCPU and 8 GB more memory. GitHub list prices checked on 2026-08-13.
Worked monthly model
Take a desktop team whose release matrix runs 300 times a month across pull requests and nightly builds. Each run takes 9 minutes on the Linux leg, 14 minutes on the Windows leg, and 12 minutes on the macOS leg, with warm caches on all three.
| Leg | Monthly minutes | WarpBuild rate | WarpBuild cost | GitHub-hosted rate | GitHub-hosted cost |
|---|---|---|---|---|---|
| Linux, 8 vCPU | 2,700 | $0.016 | $43.20 | $0.022 | $59.40 |
| Windows, 8 vCPU | 4,200 | $0.032 | $134.40 | $0.042 | $176.40 |
| macOS, 6 vCPU | 3,600 | $0.08 | $288.00 | $0.102 | $367.20 |
| Total | 10,500 | $465.60 | $603.00 |
The gap on list prices alone is $137.40 a month on this workload, and the WarpBuild side is billed per minute. Cache metering adds a small amount on top: storage is $0.20 per GB-month and each cache write or restore is $0.0001, so an 8GB Electron cache footprint with 6,000 operations a month comes to about $2.20.
Full rates for every size and platform are on the pricing page.
Bottlenecks
Four bottlenecks account for most of the wall time in an Electron pipeline on GitHub Actions.
Native module rebuilds, once per platform. Every leg of the matrix compiles or downloads its own copy of each native dependency, because the binary is specific to the platform, the architecture, and the Electron ABI. When a prebuilt binary exists for that combination the install just downloads it; when it does not, node-gyp compiles C++ and pulls down the Electron header files first. Pin the Electron version so the ABI stays stable and the prebuilt path keeps hitting, and keep the npm store in the cache so the download half is local. This is the phase that most rewards 8 vCPU or more, since packages compile in parallel.
Electron binary downloads. Packaging an Electron application means fetching the Electron distribution for each target platform and architecture, and those archives are large. Packaging tooling downloads more on top: the code signing helpers, the NSIS toolchain for Windows targets, and the app image tooling for Linux targets all land in a separate packaging cache directory. Left uncached, every matrix leg re-downloads all of it on every run. The cache steps in the workflow above cover both directories on each platform, which is the single highest-value cache in an Electron repository.
Code signing on macOS and Windows. On macOS the certificate has to be imported into a keychain the job creates, unlocked, and given a partition list, or codesign blocks waiting on an authorization prompt that never appears on a headless runner and the job hangs until the timeout fires. Notarization then submits the artifact to Apple and waits for a verdict, which is wall-clock time on someone else's queue, so a larger runner does nothing for it. On Windows the same shape appears with different tooling: the certificate must be reachable from an ephemeral machine, which rules out a physical token and pushes teams to a certificate file in secrets or a cloud signing service, and the signature needs a timestamp so it stays valid past the certificate's expiry. Keep the whole signing sequence inside a single step so the keychain or certificate state cannot drift between steps.
Installer packaging. After the application is built, each target rewrites it: asar packing, then compression into the target format, then a checksum pass. The work is dominated by disk throughput and by a compressor that mostly runs on one thread, so it responds to fewer, larger targets rather than to more cores. Two practical levers: cut the target list on pull request runs and package everything only on release runs, and watch the disk on the macOS leg, where 120GB on the 6 vCPU labels is the first ceiling a wide target matrix hits.
When a leg is slow and the cause is unclear, CI observability reports OpenTelemetry-based system metrics from the runner agent correlated with GitHub Actions job logs, which separates a CPU-bound compile from a packaging step stuck on disk. For a failure that will not reproduce on a laptop, the Action Debugger pauses the workflow and opens an SSH session on the runner, which is the shortest path to inspecting a keychain, a provisioning profile, or a half-written installer directory in place. Both are part of the WarpBuild product surface alongside snapshot runners, remote Docker builders, and the MCP server.
Proof
Every cost number on this page carries its source and a checked-on date so you can re-derive it.
Public OSS repositories running warp- labels are citable evidence, and the check is to open the workflow file and read the runs-on line yourself. The Trigger.dev end-to-end suite runs its matrix across warp-ubuntu-latest-x64-4x and warp-windows-latest-x64-8x in triggerdotdev/trigger.dev's e2e.yml, which is the same cross-platform matrix shape an Electron release uses (checked on 2026-08-13).
The full runner catalog, including every label and alias used above, is in the cloud runners documentation. Related reading: Node.js dependency caching on GitHub Actions for the install phase, Windows runners and macOS runners for the per-platform catalogs, and GitHub Actions matrix builds for the matrix mechanics.
FAQ
Do Electron builds need one runner per platform?
Yes for macOS, and in practice yes for Windows. Apple code signing and notarization only run on macOS, and Windows installer signing expects the Windows toolchain, so the reliable shape is a three-leg matrix on warp-ubuntu-latest-x64-8x, warp-windows-latest-x64-8x, and warp-macos-15-arm64-6x.
Why does the Windows leg of the matrix cache differently?
WarpBuild Cache is not supported on Windows runners, which the caching documentation states directly. Keep WarpBuilds/cache on the Linux and macOS legs and use actions/cache on the Windows leg, with separate keys because cache entries are not portable across operating systems anyway.
What runner sizes should each Electron platform leg use?
Start at warp-ubuntu-latest-x64-8x ($0.016 per minute), warp-windows-latest-x64-8x ($0.032 per minute), and warp-macos-15-arm64-6x ($0.08 per minute). Move the macOS leg to warp-macos-15-arm64-12x when a wide target matrix fills the 120GB disk, since that label carries 270GB.
How much disk does an Electron packaging job need?
More than most jobs, because the unpacked app is copied once per target. Linux runners carry 150GB SSD, Windows runners carry 256GB SSD, and macOS runners carry 120GB SSD on the 6 vCPU labels or 270GB SSD on the 12 vCPU labels.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.