Electron Release Pipeline
GitHub Actions CI/CD for building, signing, distributing, and auto-updating an Electron app via S3/CloudFront
Electron Release Pipeline
End-to-end pattern for building, signing, distributing, and auto-updating an Electron app using GitHub Actions, S3, and CloudFront.
Version Gating
- The app's
package.jsonversion is the single source of truth for releases. The workflow must extract the version from this file — never from tags, branches, or environment variables. - Use a two-workflow architecture to avoid wasting CI minutes on non-version changes to
package.json(e.g. dependency updates):- Tag workflow (lightweight) — triggers on pushes to
mainthat modify the app'spackage.json. Reads the version and creates av*git tag only if it doesn't already exist. This job runs in seconds and costs virtually nothing. - Build & Release workflow — triggers on
v*tag pushes (plus manual dispatch for re-runs). Performs the full build, sign, deploy pipeline. Because it only triggers on tags, it never runs unless the version actually changed.
- Tag workflow (lightweight) — triggers on pushes to
- This separation ensures that dependency bumps, script changes, or other non-version edits to
package.jsondo not trigger expensive macOS and Windows builds.
Build Jobs
- macOS and Windows builds must run in parallel on their respective runner OS types (
macos-latest,windows-latest). Never cross-compile. - Each platform job should produce both an installer (DMG/EXE) for first-time users and a ZIP for the auto-updater. These serve different purposes and both are required.
- Windows builds must use Squirrel to produce
.nupkgdelta packages and aRELEASESindex file alongside the installer and ZIP. These enable differential updates.
macOS Code Signing & Notarization
- The CI must import a P12 certificate into a temporary keychain created for the build — never the system keychain. The keychain should be cleaned up after use.
- The certificate must be stored as a base64-encoded GitHub secret and decoded at build time.
- All binaries must be signed with hardened runtime enabled and an entitlements plist that grants JIT, unsigned executable memory, and dyld environment variables (required by Electron).
- After signing, the app must be notarized using
notarytoolwith Apple ID credentials. The build should fail if notarization fails — never ship an unnotarized macOS app.
S3 Distribution Structure
- Artifacts must be uploaded to two locations in S3:
- Versioned path (
/releases/v{VERSION}/) — immutable archive of every release. Never overwrite these. - Latest path (
/latest/) — version-agnostic filenames that always point to the newest release. These get overwritten on each deploy.
- Versioned path (
- The versioned path is the source of truth for downloads. The latest path is a convenience for direct download links on the website.
Update Manifests
- The deploy step must generate YAML manifest files that
electron-updaterconsumes:latest-mac.ymlfor macOSlatest.ymlfor Windows
- Each manifest must include:
version,filesarray withurl,sha512(base64-encoded), andsize, plus top-levelpath,sha512, andreleaseDate. - The Windows manifest should additionally include an
installerUrlpointing to the.exefor users who need a fresh install rather than an update. - SHA512 hashes must be computed during the deploy step from the actual uploaded artifacts — never hardcoded or copied from a previous build.
- Manifests must be uploaded to the S3 bucket root (not inside
/releases/or/latest/) soelectron-updatercan find them at the feed URL root.
CloudFront Cache Invalidation
- After uploading to S3, the workflow must invalidate CloudFront caches for all paths that were modified:
/latest/*,/latest.yml,/latest-mac.yml, and/releases/*. - Without invalidation, users may receive stale manifests and never see the update. This step is not optional.
UpdateService (Main Process)
- Configure
electron-updaterwith a generic provider pointing to the CloudFront CDN URL. Do not use the GitHub provider — it couples releases to GitHub's availability and rate limits. - The CDN URL must come from an environment variable, not be hardcoded. This allows testing against staging feeds.
- Never auto-download updates. Check for updates automatically, but require the user to opt in before downloading. Auto-install on quit is acceptable.
- The service must gate update checks behind
app.isPackagedto prevent update prompts during development. - Provide a version override mechanism (stored in
electron-store) that allows testing the update flow in dev mode by spoofing an older version.
Windows Squirrel Lifecycle
- The Electron main process must handle Squirrel lifecycle events (
--squirrel-install,--squirrel-updated,--squirrel-uninstall,--squirrel-obsolete) before any other app initialisation. - On install/update: create desktop and start menu shortcuts via
Update.exe --createShortcut. - On uninstall: remove shortcuts via
Update.exe --removeShortcut. - On any Squirrel event: quit the app immediately after handling. The app must not show a window during these lifecycle steps.
- The UpdateService should detect whether Squirrel's
Update.exeexists and fall back to generic ZIP-based updates if it doesn't.
IPC & Renderer Integration
- Expose update operations to the renderer via dedicated IPC channels: check, download, install, get version, get available update info, and check if updates are enabled.
- Use
ipcMain.handle/ipcRenderer.invoke(promise-based) for request/response operations. UsewebContents.send/ipcRenderer.onfor push notifications (e.g., "update available"). - Wrap update state in a React context provider that listens for push events from main, exposes a hook (
useUpdate), and manages UI state (checking, available, dialog visibility). - The provider should query for already-available updates on mount — the main process may have detected an update before the renderer was ready.
GitHub Release
- After successful builds, create a GitHub Release attached to the existing tag with all platform artifacts.
- The git tag already exists at this point (created by the tag workflow). The release workflow should not create or push tags — it only consumes them.
- The GitHub Release is secondary to the S3 distribution — it serves as an archive and changelog, not as the update feed.
- The release should be created in a separate job that depends on both platform builds succeeding.
Secrets & Credentials
The pipeline requires the following secrets, stored as GitHub Actions secrets (or equivalent CI secret store):
macOS Code Signing
- P12 certificate (e.g.
CERTIFICATE_P12) — A Developer ID Application certificate exported as.p12, then base64-encoded for storage as a CI secret. To obtain: create a Certificate Signing Request in Keychain Access, submit it in the Apple Developer portal under Certificates > Developer ID Application, download the certificate, export it from Keychain Access as.p12with a password, then base64-encode the file. - Certificate password (e.g.
CERTIFICATE_PASSWORD) — The password set when exporting the.p12file.
At build time, the workflow must:
- Decode the base64 secret back to a
.p12file. - Create a temporary keychain with a random password.
- Import the
.p12into that keychain usingsecurity import. - Set the keychain's partition list to allow
codesignaccess without UI prompts. - Detect the signing identity from the keychain (e.g. via
security find-identity). - Pass the identity to the build tool (Electron Forge reads it from an environment variable like
CODESIGN_IDENTITY). - Clean up the temporary keychain after the build.
macOS Notarization
- Apple ID (e.g.
APPLE_ID) — The Apple ID email associated with the developer account. - App-specific password (e.g.
APPLE_ID_PASSWORD) — Generated at appleid.apple.com under Sign-In and Security > App-Specific Passwords. This is required because the Apple ID likely has 2FA enabled. - Apple Team ID (e.g.
APPLE_TEAM_ID) — The 10-character team identifier from the Apple Developer portal membership page.
Notarization is performed after signing using xcrun notarytool submit with --wait to block until Apple's servers complete the scan. The build must fail if notarization is rejected.
AWS (S3 Deployment)
- AWS Access Key ID (e.g.
AWS_ACCESS_KEY_ID) — IAM credentials withs3:PutObject,s3:GetObject, ands3:ListBucketpermissions on the releases bucket, pluscloudfront:CreateInvalidationon the distribution. - AWS Secret Access Key (e.g.
AWS_SECRET_ACCESS_KEY) — The corresponding secret key.
General Rules
- Never log, echo, or expose secret values in workflow output. Use the CI platform's secret masking.
- Scope secrets to the jobs that need them — code signing secrets should only be accessible to build jobs, AWS secrets only to deploy jobs.
- Store the CloudFront distribution ID and S3 bucket name as workflow-level environment variables (not secrets) since they are not sensitive.