Applies to GitHub Actions workflow files for releasing the Electron app, Electron Forge config, UpdateService, update IPC handlers, update provider components, Squirrel lifecycle handling, and entitlements plists
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. - The workflow must check whether a GitHub release for the version already exists before building. If the release exists, the workflow should skip all build/deploy steps. This makes the pipeline idempotent — re-running on the same commit is safe.
- The workflow should trigger on pushes to
mainthat modify the app’spackage.json, plus support manual dispatch for re-runs. Tags are created by the release job, not by a separate workflow.
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 git tag and a GitHub Release with all platform artifacts attached.
- The GitHub Release is secondary to the S3 distribution — it serves as an archive and changelog, not as the update feed.
- The tag and release should be created in a separate job that depends on both platform builds succeeding. Never tag a partial build.
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.