v27 Breaking Changes
v27 moves the entire electron-builder ecosystem to native ES modules, raises the minimum runtime to Node.js 22.12.0, and hard-deletes the deprecated APIs that accumulated since v22. Every breaking change in the release is catalogued on this page.
Most projects need only a Node.js bump plus one command. Run the automated migrator and follow the step-by-step migration walkthrough →.
This page is the canonical reference for what changed in v27. For the how — the ordered upgrade steps, the automated migrator, and the checklist — see the v26 → v27 migration walkthrough.
- The
build()API is unchanged; configuration keys that were renamed or restructured are rewritten automatically bymigrate-schema(see each entry below) and keep their v26 behavior, and exported types that survive keep their shape. - CJS
require()continues to work without code changes on supported Node.js versions. - Every config-level breaking change below that has a mechanical v27 equivalent is rewritten automatically by
electron-builder migrate-schema(look for the Auto ✓ marker). The config keys it cannot rewrite,squirrelWindows.customSquirrelVendorDirand a custom NSIS bundle innsis.customNsisBinary/customNsisResources, are reported with a warning. Runtime, CLI, env-var, and behavior changes require manual action — see whatmigrate-schemadoes and does not do.
electron-builder migrate-schema # apply changes in place
electron-builder migrate-schema --dry-run # preview only
This handles every change marked Auto ✓ below. See the walkthrough for flags and serialization caveats.
In v27 every toolsets.* property defaults to "latest" — an unset property and the literal "latest" both resolve to the newest published bundle for that toolset (previously each property defaulted to a fixed pinned version; null is no longer accepted). No config change is required, but the effective defaults moved:
wine→"system"— the host-installedwineonPATH, on macOS as well as Linux (v26 downloaded a Wine 4.0.1 bundle on macOS). Building Windows targets on macOS now requires Wine on the host (brew install --cask wine-stable). The"1.0.1"Wine 11.0 bundle still downloads when pinned explicitly, but the published bundle ships no PE builtins, so it cannot run Windows tools on its own.winCodeSign→1.3.0— Windows Kits 10.0.26100.0,osslsigncode2.11 (native arm64), and the Azure Trusted Signingdlib+ .NET 8 payload.appimage→1.1.0— static FUSE3-compatible runtime; addsunsquashfssupport.icons→1.2.3— newerwasm-vips/@resvg/resvg-wasmbundle that writes 16/32 px ICNS entries as ARGB (was1.1.0).linuxToolsMac→1.0.1(runs on macOS 15+) andsevenZip→1.0.1(correct per-arch Windows binaries).nsis(1.2.1) andfpm(2.2.1) are unchanged; the newsquirreltoolset resolves to1.1.1.
To stay on a legacy bundle, pin the toolset to "0.0.0". Because winCodeSign now defaults to 1.3.0, Azure Trusted Signing uses the faster signtool /dlib path out of the box. Full breakdown in Toolsets & environment variables.
Breaking changes at a glance
Rows marked Auto ✓ are rewritten for you. For the shortlist of changes the migrator can not apply — the new defaults and runtime behaviors you may still trip over — see What's New in v27 → new defaults & behavior changes.
| Change | Auto | Action required |
|---|---|---|
| Node.js >=22.12.0 required | — | Update your runtime and CI Node version |
| All packages are native ESM | — | None — require() still works on Node >=22.12 |
electron-forge-maker-* are now ESM | — | None — same API, same export shape |
electronCompile removed | ✓ | Remove from config; migrate off electron-compile |
disableDefaultIgnoredFiles removed | ✓ | Removed automatically; re-include specific files via a files glob (e.g. **/*.obj) |
framework / nodeVersion / launchUiVersion removed | ✓ | Removed automatically (Electron is the only framework) |
| ProtonFramework & LibUiFramework removed | — | Migrate to an Electron-based build setup |
appImage.systemIntegration removed | ✓ | Removed automatically |
npmSkipBuildFromSource removed | ✓ | Replaced by nativeModules.buildDependenciesFromSource |
Native-module options grouped under nativeModules | ✓ | nativeRebuilder → rebuildMode; the "legacy" rebuilder is removed (dropped with a warning) |
ASAR options consolidated under asar | ✓ | asarUnpack → asar.unpack, etc., including platform-level mac.asarUnpack → mac.asar.unpack (asar: true is still valid, just redundant) |
macOS signing consolidated under mac.sign | ✓ | identity/entitlements/hardenedRuntime/… → mac.sign.*; signIgnore → sign.ignore |
mac.universal options consolidated | ✓ | mergeASARs/singleArchFiles/x64ArchFiles → mac.universal.* |
Windows signing unified under win.sign | ✓ | Discriminated union type: "signtool" | "hsm" | "pkcs11" | "azure" |
win.signExecutable / win.signAndEditExecutable removed | ✓ | false → win.sign: false; resource editing always runs |
electronDownload → electronGet | ✓ | Reshaped to @electron/get options; some legacy fields dropped (warned) |
snap config key removed | ✓ | Restructured to snapcraft with an explicit base |
Root-level directories removed | ✓ | Move under build.directories |
build.helper-bundle-id removed | ✓ | Moved to mac.helperBundleId |
squirrelWindows.noMsi removed | ✓ | Replaced by msi (inverted) |
squirrelWindows.customSquirrelVendorDir removed | — | Supply a custom Squirrel bundle via toolsets.squirrel (a ToolsetCustom object); migrate-schema warns but cannot rewrite it (different bundle layout) |
nsis.customNsisBinary / customNsisResources removed | partial | debugLogging → nsis.installerDebugLogging (auto); supply a custom NSIS bundle via toolsets.nsis (the v26 checksum keeps working) with the plugins inside (migrate-schema warns, cannot convert it). installerDebugLogging needs a log-enabled custom bundle |
GithubOptions.vPrefixedTagName removed | ✓ | Use tagNamePrefix; an empty tagNamePrefix: "" is now honored (v26 ignored it) |
GitlabOptions.vPrefixedTagName retained | — | None — still functional; the migrator leaves GitLab entries untouched |
devMetadata / extraMetadata in PackagerOptions removed | — | Use config / config.extraMetadata |
Implicit --publish removed | — | Pass --publish explicitly |
--em.build / --em.directories CLI flags removed | — | Use -c / -c.directories |
linux.syncDesktopName removed | ✓ | Removed automatically; behaviour is always on. If it was false, set desktopName to control the filename |
| Linux maintainer-script EJS syntax removed | — | Use ${var} instead of <%= var %> |
| NSIS file-association ProgID format changed | — | Update custom NSIS scripts that hard-code the old ProgID |
NSIS customInstallMode macro guard casing corrected | — | Rename a customInstallmode (lowercase m) macro definition in custom NSIS scripts to the documented customInstallMode |
Toolset defaults resolve to "latest" | — | No action; pin to "0.0.0" to restore a legacy bundle. Wine is now the host install — building Windows targets on macOS needs Wine installed |
toolsets.*: null rejected; appimage: "1.0.2" retired | ✓ | null entries are removed (unset = "latest"); "1.0.2" becomes "1.0.3" |
| Toolset env-var overrides removed | — | Replace APPIMAGE_TOOLS_PATH, ELECTRON_BUILDER_NSIS_DIR, USE_SYSTEM_WINE, … with toolsets.X: { url, checksum } — setting a removed variable now fails the build |
CI_BUILD_TAG env var removed | — | Use CI_COMMIT_TAG |
Azure Trusted Signing /dlib is the default | — | Pin winCodeSign below 1.3.0 only to force the legacy PowerShell path |
New: win.target: "msix" (beta) | — | Optional, additive — the default winCodeSign works (only the legacy 0.0.0 bundle is rejected) |
PlatformPackager.info & platformSpecificBuildOptions now protected | — | Plugin authors: hard break — .info no longer compiles externally; use the new pass-through getters |
Linux .desktop Exec now runs a generated *-launcher script | — | Update custom .desktop/AppArmor/MIME tooling that hard-codes the Exec command |
node_modules arch/os-filtered on every build | — | Awareness — packages whose cpu/os mismatch the target are now excluded |
arch: "all" now expands to x64 + arm64; 32-bit fails fast on Electron 44+ | — | arch: "all" drops ia32 — request ia32 explicitly; ia32/armv7l require electronVersion <= 43.x |
macOS productName/executableName validated, not silently sanitized | — | A name needing filename sanitization now throws — pick a name that needs none |
| macOS default entitlements tightened | — | The bundled default now grants only allow-jit; add allow-unsigned-executable-memory / disable-library-validation back in build/entitlements.mac.plist (and .inherit.plist) only if your app needs them |
| Bitbucket Cloud publishing: token without username → Bearer auth | — | Set BITBUCKET_USERNAME if your token is an app password / API token |
Redundant production dependencies excluded, not rejected | — | electron/electron-builder are excluded from the copied node_modules (was a hard error); tune the set via ignoredProductionDependencies. If you set ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY (removed) to bundle electron-builder, override the list instead; electron-prebuilt/electron-rebuild no longer error and now ship if declared — remove them from dependencies |
allowMissingDependencies now fails the build | — | A missing production dependency is a hard error; set allowMissingDependencies: true to restore v26 warn-only behavior |
| Native binaries are verified against the target | — | A .node addon built for another platform/arch now fails the build; set nativeModules.verifyNativeBinaries: "warn" (or false) to package anyway |
extraFiles / extraResources to is validated | — | An absolute to, or one escaping the output dir, now throws |
Windows publisherName validated against the certificate | — | Update publisherName after a certificate rotation, or drop it. Custom win.sign.sign hooks (even with a certificate configured) and certificates without a CN now require it when app-update.yml is written (or set win.verifyUpdateCodeSignature: false if your updates are not Authenticode-signed) |
Custom Windows signing hook moved to win.sign.sign | ✓ | win.signtoolOptions.sign moves with the rest of signtoolOptions to win.sign.sign |
| macOS names are no longer NFD-normalized | — | Update tooling that matches the normalized on-disk bundle filename |
Linux executableArgs field codes are now literal | — | Remove %F/%U from executableArgs; use linux.desktop.entry.Exec |
http-parser dropped from default pacman depends | — | Add it back with "depends": ["default", "http-parser"] only if your app really needs it |
| New: Cloudflare R2 publish provider | — | None — additive; needs accountId and an https publicUrl |
New command: migrate-schema | — | None — run it to apply every Auto ✓ change above |
DMG filesystem defaults to APFS | — | Set dmg.filesystem: "HFS+" only if you need pre-10.13 macOS compatibility |
disableWebInstaller defaults to true (electron-updater) | — | Web-installer updates are rejected (ERR_UPDATER_WEB_INSTALLER_DISABLED) unless disableWebInstaller is false; v27+ nsis-web installs opt in automatically; older installs, and apps switching from nsis to nsis-web, need disableWebInstaller: false |
nsis-web installer verifies and installs its own copy of the app package | — | A --package-file package or a publish-URL download that does not match, or a --package-file package that cannot be copied, aborts the install (exit code 2); the local package file is now left in place. Only if you run one web installer with a --package-file from another build: set nsisWeb.allowUnverifiedAppPackage: true |
NSIS: per-machine builds set isAdminRightsRequired in the update info | — | None for most apps. Their updates are started with elevate.exe; with autoInstallEvent: "onNextLaunch", per-machine apps call installPendingUpdateIfAvailable() to install a pending update |
| Update credentials are only sent to the feed's origin (electron-updater) | — | Only if latest*.yml points downloads at another origin (another host, or another port or scheme) that needs your requestHeaders / addAuthHeader credentials or a token in the feed URL's query: serve the files from the feed origin or use pre-signed URLs. A custom provider used with credential headers must override Provider.feedBaseUrl (else ERR_UPDATER_FEED_BASE_URL_NOT_DECLARED) |
| Suffixed channels expand to lower channels | — | Only with generateUpdatesFilesForAllChannels: a beta-*/latest-* channel now writes 2–3 yml files instead of 1 |
| Signed update manifests are required | — | Generate a key with electron-builder create-update-key and set ELECTRON_BUILDER_UPDATE_SIGN_KEY, or set updateManifest: false to keep publishing unsigned manifests |
app-update.yml feed follows the update-writing targets | — | Only with target-level publish (e.g. nsis.publish), targets of one app with different feeds, or a first provider with publishAutoUpdate: false: check which feed installs will poll |
latest*.yml drops legacy top-level path/sha512 | — | None for electron-updater >=2.16 (all modern clients); set electronUpdaterCompatibility to a legacy-inclusive range only if you still ship apps embedding electron-updater 1.x–2.15 |
quitAndInstall takes an options object (electron-updater) | — | Replace positional args: quitAndInstall(true, false) → quitAndInstall({ isSilent: true, isForceRunAfter: false }) |
autoInstallOnAppQuit replaced by autoInstallEvent enum (electron-updater) | — | autoInstallOnAppQuit = false → autoInstallEvent = "manual"; default "onQuit" preserves behavior |
downloadUpdate() resolves with an object, not an array (electron-updater) | — | const [installer] = await downloadUpdate() → const { updateFile } = await downloadUpdate(); same for checkForUpdates() → downloadPromise |
verifyUpdateCodeSignature renamed and returns a result object (electron-updater) | — | NsisUpdater.verifyUpdateCodeSignature → verifyUpdateFileAuthenticodeSignature; return { response: "success" } / { response: "failure", message } instead of null / string |
Renamed type exports (ElectronDownloadOptions, WindowsAzureSigningConfiguration, …) | — | Import the new names — no compat aliases |
SnapOptions, ProtonFramework, LibUiFramework exports removed | — | Use the snapcraft config shape / Electron framework |
Runtime & ESM
Node.js >=22.12.0 required
v27 requires Node.js 22.12.0 or later. This is the version where Node's require(esm) support was stabilized (no flags needed), which allows both CJS and ESM consumers to use these packages without any code changes.
{
"engines": {
"node": ">=22.12.0"
}
}
See the walkthrough for updating your runtime, CI, and Docker images.
Native ESM output
electron-builder packages now ship as native ES modules. On Node >=22.12.0 both styles continue to work:
// CJS require() — still works
const { build, Platform } = require("electron-builder")
// ESM import — now the preferred style
import { build, Platform } from "electron-builder"
If your project uses "type": "module" or ESM imports, no changes are needed. If it uses CJS on Node >=22.12, require() continues to work as before. The build() function signature, all configuration options, and all exported types are otherwise unchanged.
moduleResolution — v27 packages include exports maps and full TypeScript declarations. "node" (legacy), "node16"/"nodenext", and "bundler" (recommended) all work.
electron-forge-maker-* plugins
The four Forge maker plugins (electron-forge-maker-appimage, electron-forge-maker-nsis, electron-forge-maker-nsis-web, electron-forge-maker-snap) are now native ES modules. The public API is identical — Electron Forge loads them via dynamic import() internally, so no config changes are required.
Removed configuration options
electronCompile
The electronCompile configuration option has been removed. electron-compile is an unmaintained library with no releases since 2019.
{ "build": { "electronCompile": true } } // ← delete this line
If your project relies on electron-compile for source compilation, migrate to a modern build tool before upgrading:
- electron-vite — recommended; fast, first-class ESM and Electron integration
- esbuild — extremely fast, minimal configuration
- webpack — mature, widely used
framework / nodeVersion / launchUiVersion
These three fields are removed. Only Electron is supported as a target framework — proton/proton-native and libui support has been removed (see Removed exports).
{
"build": {
"framework": "electron", // ← delete; "electron" was and remains the default
"nodeVersion": "current", // ← delete; no effect
"launchUiVersion": "0.1.0" // ← delete; no effect
}
}
disableDefaultIgnoredFiles
The disableDefaultIgnoredFiles option has been removed. To include a file that is excluded by default (for example a Wavefront .obj 3D model, or any other default-excluded extension/name), add an explicit files glob that targets it — an explicit include now overrides the matching default exclusion:
{
"build": {
"disableDefaultIgnoredFiles": true, // ← delete this line
"files": [
"**/*",
"**/*.obj" // ← add a glob for the default-excluded files you want to keep
]
}
}
Broad patterns such as **/* still honor the defaults; only a pattern that names the extension or directory concretely opts it back in.
appImage.systemIntegration
Removed. Desktop integration is handled automatically by AppImageLauncher.
npmSkipBuildFromSource
Removed. Use buildDependenciesFromSource (now under nativeModules). It is the logical inverse:
// v26
{ "npmSkipBuildFromSource": true }
// v27 equivalent
{ "nativeModules": { "buildDependenciesFromSource": false } }
asar: true (still valid — no action needed)
asar: true remains valid in v27. The type is AsarOptions | boolean | null, the schema accepts a boolean, and the runtime maps true to {} — so an existing asar: true keeps working exactly as before. It is simply redundant, because ASAR is enabled by default:
{ "build": { "asar": true } } // Still works — but redundant
{ "build": { "asar": {} } } // Equivalent; or omit the key entirely
migrate-schema drops the redundant root-level true (folding any other ASAR keys into an asar object instead). The full ASAR restructuring is documented under ASAR options → asar.
Root-level directories in package.json
Specifying directories at the root of package.json is removed. Move it under the build key.
{ "name": "my-app", "directories": { "output": "dist" } } // Before
{ "name": "my-app", "build": { "directories": { "output": "dist" } } } // After
build.helper-bundle-id
The hyphenated root-level helper-bundle-id is removed. Use mac.helperBundleId.
{ "build": { "helper-bundle-id": "com.example.helper" } } // Before
{ "build": { "mac": { "helperBundleId": "com.example.helper" } } } // After
squirrelWindows.noMsi
The noMsi boolean is removed in favor of its inverse, msi.
{ "build": { "squirrelWindows": { "noMsi": true } } } // Before
{ "build": { "squirrelWindows": { "msi": false } } } // After
squirrelWindows.customSquirrelVendorDir
Removed. v27 inlines the Squirrel.Windows installer logic and drops the electron-winstaller npm dependency (and its vendored binaries); the Squirrel vendor toolset is now fetched from the maintained squirrel.windows electron-builder-binaries bundle. To pin a version or supply a custom/local bundle — for example an air-gapped mirror — use the standard toolsets.squirrel setting instead, the same way you would pin toolsets.nsis or toolsets.winCodeSign.
The shape and behaviour differ, so this is not a 1-to-1 rename:
- The old
customSquirrelVendorDirpointed at a directory whose contents were the vendor files (Squirrel.exe,nuget.exe, …) and was copied verbatim, bypassing all provisioning. toolsets.squirrelaccepts a version pin ("1.1.1"/"latest") or aToolsetCustomobject whoseurlpoints at a bundle that contains anelectron-winstaller/vendor/subtree. The bundle still goes through normal provisioning: on every platformrcedit.exeis supplied from thewinCodeSigntoolset (it runs under Wine on non-Windows hosts), and formsi: truethe shared WiX toolset is merged in.
// Before (removed):
{ "squirrelWindows": { "customSquirrelVendorDir": "./my-vendor" } }
// After: a custom toolset bundle whose electron-winstaller/vendor/ holds the binaries.
// A bare file:// directory is used as-is (no checksum); a remote/archive url needs a checksum.
{ "toolsets": { "squirrel": { "url": "file:///abs/path/to/squirrel-toolset" } } }
migrate-schema cannot rewrite this key (the bundle layout differs), so it warns and leaves it in place; the next build then fails with a message pointing at toolsets.squirrel. Remove the key to use the default bundle, or point toolsets.squirrel at a bundle with the layout above.
For a fully offline build, seed the squirrel.windows@<version> archive (plus winCodeSign and, for MSI, the WiX bundle) in the toolset cache or point toolsets.squirrel at a local bundle — see Offline / Air-Gapped Builds. Nothing is downloaded outside the checksummed toolset bundles.
nsis.customNsisBinary / customNsisResources removed → toolsets.nsis
Removed, on nsis, nsisWeb and portable, together with the exported CustomNsisBinary type. The NSIS compiler and its plugins now come only from the toolsets.nsis bundle, and NSIS logging has its own option. A config that still sets either key fails the build with a message naming the replacement.
- A custom NSIS build (for example an NSISbi fork for installers over 2 GB, or a log-enabled
makensis) moves totoolsets.nsis. customNsisBinary.debugLoggingmoves tonsis.installerDebugLogging(ornsisWeb.installerDebugLogging). It never had an effect onportable, which has no equivalent.customNsisResourceshas no separate replacement: a customtoolsets.nsisbundle carries its own plugins.
// Before (v26):
{ "nsis": { "customNsisBinary": { "url": "https://example.com/my-nsis.7z", "checksum": "<checksum>", "version": "3.10", "debugLogging": true } } }
// After:
{
"toolsets": { "nsis": { "url": "https://example.com/my-nsis.7z", "checksum": "<checksum>", "version": "3.10" } },
"nsis": { "installerDebugLogging": true }
}
Moving a custom bundle is not a pure rename:
- The checksum carries over. The
urlandchecksumwork as before, including for an NSISBI drop-in: v26 used base64-encoded SHA-512 for all toolset checksums, andtoolsets.nsisaccepts such a value unchanged, as well as a SHA-256 hex value. SHA-256 hex is the v27 default because toolset downloads moved to the official@electron/getpackage, whose checksum verification (sumchecker) only supports SHA-256 hex (see Custom toolset checksums). Only an archive you repack, for example to add the plugins, needs a new checksum. - The bundle must carry the plugins. v26 read the NSIS plugins from a separate resources bundle (
customNsisResources, or the default one), while a customtoolsets.nsisbundle must contain them itself, in aplugins/orwindows/Plugins/directory (elevate.exeis still read from the bundle root, as in v26). A bundle with the v26 layout (Bin/makensis.exeorlinux/makensis) is still found. See Custom NSIS bundle layout for the exact paths, the plugins the bundle must contain, and how to repack an NSIS or NSISBI release.
installerDebugLogging needs a log-enabled NSIS, that is makensis and its stubs compiled with NSIS_CONFIG_LOG=yes; any other makensis rejects the LogSet it emits. The bundled toolsets.nsis versions are not log-enabled, so the build fails with a configuration error when installerDebugLogging is set without a custom toolsets.nsis bundle. This is unchanged from v26, where the option also only worked with your own customNsisBinary. When building on Linux or macOS, the bundle's makensis for that host must be log-enabled too.
migrate-schema moves debugLogging to installerDebugLogging (dropping it, with a warning, for portable) and removes a customNsisBinary that sets nothing else. It cannot convert a custom bundle, so it leaves a customNsisBinary with a url / checksum / version, and any customNsisResources, in place and warns; the build then fails with the message above until you move them.
GithubOptions / GitlabOptions vPrefixedTagName
The vPrefixedTagName boolean on GithubOptions is removed. Use tagNamePrefix to control the tag prefix.
{ "publish": { "provider": "github", "vPrefixedTagName": false } } // Before
{ "publish": { "provider": "github", "tagNamePrefix": "" } } // After (empty string = no prefix)
To keep the default v prefix, simply remove vPrefixedTagName — tagNamePrefix defaults to "v".
An empty tagNamePrefix is now honored. v26 ignored tagNamePrefix: "" and still tagged v1.2.3 (unless vPrefixedTagName: false); v27 uses it as-is and tags 1.2.3. migrate-schema rewrites a GitHub tagNamePrefix: "" without vPrefixedTagName: false to "v" — keeping your existing tag names — and warns so you can set it back if unprefixed tags are what you wanted. A non-empty tagNamePrefix always won in v26 and is kept. The migrator applies to GitHub publish entries anywhere in the config (root, mac/win/linux, and target sections such as nsis.publish).
Only the GitHub field was removed. On GitLab, vPrefixedTagName is unchanged in v27 — it still exists in the type, the schema, and the runtime, and continues to control the tag prefix (vPrefixedTagName: false → 1.2.3; omit it → v1.2.3). It has no tagNamePrefix equivalent, so migrate-schema leaves GitLab publish entries untouched. No action is required.
linux.syncDesktopName (always synced)
The linux.syncDesktopName flag is removed. The behaviour it gated is now always on: the installed .desktop filename is always derived from the desktopName field in package.json — installed as ${desktopName}.desktop, with any trailing .desktop in the value stripped first — falling back to executableName only when desktopName is absent.
// Before (v26) — opt-in
{ "build": { "linux": { "syncDesktopName": true } } } // ← delete this line
migrate-schema removes the flag automatically. No replacement is needed. If you had syncDesktopName: false (opting out of syncing in v26), the migrator warns you: the installed filename is now always derived from desktopName, so set (or omit) desktopName to control it.
Why this changed: Electron derives its app_id / WM_CLASS from desktopName, and desktop environments match a running window to its launcher entry by comparing WM_CLASS against the installed .desktop filename. When the two diverged — which is exactly what happened with syncDesktopName: false (the v26 default) whenever a desktopName was set — GNOME, KDE, and others failed to associate the window with its launcher, breaking taskbar grouping, dock icons, and launcher highlighting (see #9103). Removing the flag eliminates a footgun rather than removing functionality. Path-traversal/NUL validation on the resulting filename is unchanged.
Linux maintainer-script EJS template syntax
The legacy <%= varName %> EJS interpolation in Linux maintainer scripts (e.g. FPM after-install/after-remove) is removed. Use shell-style ${varName} instead.
devMetadata / extraMetadata (programmatic PackagerOptions)
These fields in the programmatic PackagerOptions API are removed (they have thrown InvalidConfigurationError since v22).
// Before
await build({ targets: Platform.MAC.createTarget(), devMetadata: { … }, extraMetadata: { … } })
// After
await build({ targets: Platform.MAC.createTarget(), config: { extraMetadata: { … } } })
Restructured configuration
Native-module options → nativeModules
Four root-level configuration properties are moved into a new nativeModules sub-key, and nativeRebuilder is renamed to rebuildMode.
// Before (v26)
{ "build": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "nativeRebuilder": "parallel" } }
// After (v27)
{ "build": { "nativeModules": { "buildDependenciesFromSource": true, "nodeGypRebuild": false, "npmRebuild": true, "rebuildMode": "parallel" } } }
npmArgs is not affected — it controls the package-manager install phase and remains at the root level. npmSkipBuildFromSource (deprecated in v26) is removed; migrate-schema converts it to its inverse, nativeModules.buildDependenciesFromSource.
The "legacy" value of nativeRebuilder (the old app-builder binary rebuilder) is removed — rebuildMode only accepts "sequential" or "parallel", and native modules are always rebuilt with @electron/rebuild. migrate-schema drops a "legacy" value with a warning, so the default "sequential" mode applies.
ASAR options → asar
All ASAR-related configuration is now nested under a single asar key. Flat root-level properties are removed. The asar type is now AsarOptions | false | null.
| Removed key | Replacement |
|---|---|
asar-unpack | asar.unpack |
asar-unpack-dir | asar.unpack |
asar.unpackDir | asar.unpack |
asarUnpack | asar.unpack |
disableSanityCheckAsar | asar.disableSanityCheck |
disableAsarIntegrity | asar.disableIntegrity |
asar: true | (still valid — redundant, since absence means enabled) |
mac.asarUnpack / win.asarUnpack / linux.asarUnpack / mas.asarUnpack / masDev.asarUnpack | <platform>.asar.unpack |
// Before
{ "build": { "asarUnpack": ["**/*.node"], "disableSanityCheckAsar": true, "disableAsarIntegrity": true } }
// After
{
"build": {
"asar": {
"unpack": ["**/*.node"],
"disableSanityCheck": true,
"disableIntegrity": true
}
}
}
When asar: false, the root-level sub-options have no effect and the migrator removes them.
Platform-level asar now replaces the root one. In v26 the root asarUnpack, disableSanityCheckAsar, and disableAsarIntegrity applied to every platform, and a platform's own asarUnpack was added to the root patterns. In v27 all three live inside asar, and a platform-level asar (e.g. mac.asar) replaces the root asar object entirely. migrate-schema keeps the v26 result: a platform asarUnpack becomes <platform>.asar.unpack merged with the root patterns and options, and a platform that already overrides asar gets the former root-level values copied in. (For programmatic JS/TS configs, the codemod only rewrites the cases where a plain move is equivalent and warns about the rest.)
// Before (v26)
{ "asarUnpack": ["**/*.node"], "mac": { "asarUnpack": ["Resources/**"] } }
// After (v27)
{ "asar": { "unpack": ["**/*.node"] }, "mac": { "asar": { "unpack": ["**/*.node", "Resources/**"] } } }
macOS signing → mac.sign
All macOS code-signing options now live inside a single sign object on mac (and mas / masDev), mirroring the Windows win.sign grouping. mac.sign is typed as CustomMacSign | ElectronSignOptions | string | null, where ElectronSignOptions is a typed pass-through to @electron/osx-sign.
Removed (mac.*) | Replacement (mac.sign.*) |
|---|---|
identity | sign.identity |
entitlements | sign.entitlements |
entitlementsInherit | sign.entitlementsInherit |
entitlementsLoginHelper | sign.entitlementsLoginHelper |
provisioningProfile | sign.provisioningProfile |
type | sign.type (defaults to a value derived from the target: mas-dev builds default to development, everything else to distribution; an explicit value wins) |
binaries | sign.binaries |
requirements | sign.requirements |
hardenedRuntime | sign.hardenedRuntime |
gatekeeperAssess | (removed — @electron/osx-sign 2.x dropped the spctl --assess step; there is no sign.gatekeeperAssess) |
strictVerify | sign.strictVerify |
preAutoEntitlements | sign.preAutoEntitlements |
timestamp | sign.timestamp |
additionalArguments | sign.additionalArguments |
signIgnore | sign.ignore (renamed to the @electron/osx-sign canonical name) |
// Before
{ "mac": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "signIgnore": ["**/*.txt"] } }
// After
{ "mac": { "sign": { "identity": "Developer ID Application: My Company (TEAMID)", "hardenedRuntime": true, "ignore": ["**/*.txt"] } } }
To skip signing, use mac.sign.identity: null (or mac.sign: null). The same structure applies to mas and masDev.
A few of these accepted null in v26 as "unset" but do not in v27 (type, provisioningProfile, binaries, signIgnore, singleArchFiles, x64ArchFiles); migrate-schema drops those nulls instead of moving them. identity: null is kept — it still means "skip signing".
If you used mac.sign as a custom signing function or module path (sign: "./customSign.js") together with sibling fields like identity, the two can no longer coexist — sign is now a single union. migrate-schema leaves your custom signer untouched and prints a warning so you can decide whether to keep the custom function or switch to an ElectronSignOptions object.
macOS default entitlements tightened
The bundled default entitlements file (used when neither mac.sign.entitlements nor build/entitlements.mac.plist is present) now grants only com.apple.security.cs.allow-jit. com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation are no longer granted by default — modern Electron does not need either in the main process, and both materially weaken the Hardened Runtime.
- Nested binaries (helpers, frameworks,
.nodemodules, executables inapp.asar.unpacked) are no longer signed with one blanket plist. They now receive@electron/osx-sign's per-file defaults — renderer/GPU helpers:allow-jit; plugin helper: Chromium's looser set; everything else:default.darwin.plist— unless you supplybuild/entitlements.mac.inherit.plist/mac.sign.entitlementsInherit. - MAS builds without a
build/entitlements.mas.plistfall back to@electron/osx-sign's sandboxeddefault.mas.plist(previously they received the non-sandboxedmactemplate). - Ad-hoc builds (
mac.sign.identity: "-") keep working: a built-in ad-hoc entitlements file that includesdisable-library-validationis applied to the app and its nested binaries. - New post-sign warning: after signing, electron-builder lists Mach-O libraries and bundles under
Contents/Resources/app.asar.unpackedandContents/PlugIns(which electron-builder never re-signs) whose signature carries a different Team ID (or none) whiledisable-library-validationis not granted — these would fail library validation when loaded. Executables are spawned rather than loaded, so they are not subject to library validation and are not flagged.
Action is required only if your app loads frameworks, plugins, or native modules signed by another Team ID (or unsigned), or still relies on allow-unsigned-executable-memory: add the keys back in build/entitlements.mac.plist (and in build/entitlements.mac.inherit.plist when a helper process or a sidecar executable does the loading — library validation is enforced by the loading process based on its own entitlements, not by what the nested file was signed with). See Loading third-party or unsigned binaries.
This is a runtime default, not a config-key rename, so migrate-schema does not change it.
mac.universal
The universal-build options move from mac root into a universal object (typed as ElectronUniversalOptions, a pass-through to @electron/universal). They have no effect unless the target arch is universal.
Removed (mac.*) | Replacement (mac.universal.*) |
|---|---|
mergeASARs | universal.mergeASARs |
singleArchFiles | universal.singleArchFiles |
x64ArchFiles | universal.x64ArchFiles |
{ "mac": { "mergeASARs": true, "singleArchFiles": "*.node" } } // Before
{ "mac": { "universal": { "mergeASARs": true, "singleArchFiles": "*.node" } } } // After
Windows signing → win.sign
In v26, Windows signing used two separate root-level keys (signtoolOptions and azureSignOptions). In v27, all Windows signing modes are expressed through a single win.sign key typed as a discriminated union:
win.sign: { type: "signtool" | "hsm" | "pkcs11" | "azure", … } | false | null
false / null disables signing entirely. Unset means electron-builder discovers credentials from the environment (e.g. WIN_CSC_LINK).
win.signtoolOptions → win.sign: { type: "signtool", … } — all fields move verbatim; add type: "signtool".
{ "win": { "signtoolOptions": { "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // Before
{ "win": { "sign": { "type": "signtool", "certificateFile": "cert.pfx", "publisherName": "CN=ACME Inc" } } } // After
win.azureSignOptions → win.sign: { type: "azure", … } — fields move into win.sign with type: "azure". The old index signature ([k: string]: string) that allowed arbitrary extra keys is replaced by an explicit additionalMetadata object. migrate-schema both restructures the key and moves any unrecognized extra keys into additionalMetadata.
// Before
{ "win": { "azureSignOptions": { "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "ExcludeCredentials": "ManagedIdentityCredential" } } }
// After
{ "win": { "sign": { "type": "azure", "endpoint": "https://weu.codesigning.azure.net/", "certificateProfileName": "my-profile", "additionalMetadata": { "ExcludeCredentials": "ManagedIdentityCredential" } } } }
New signing modes (beta): hsm and pkcs11
v27 introduces two new signing modes in win.sign:
type: "hsm"— Hardware Security Module via signtool/csp /kc(Windows-only; requirestoolsets.winCodeSign: "1.x").type: "pkcs11"— PKCS#11 token via osslsigncode (cross-platform; runs on macOS/Linux CI without a Windows VM).
{
"win": {
"sign": {
"type": "pkcs11",
"pkcs11Module": "/usr/lib/x86_64-linux-gnu/opensc-pkcs11.so",
"pkcs11KeyUri": "pkcs11:token=MyToken;object=MyKey;type=private",
"certificateFile": "cert.pem"
}
},
"toolsets": { "winCodeSign": "1.3.0" }
}
Both HSM and PKCS#11 are beta — the interfaces are stable but real-hardware test coverage is limited.
Custom Windows signing hooks move to win.sign.sign
In v26 the custom signer hook lived at win.signtoolOptions.sign. v27 reuses the win.sign key for the discriminated union, and the hook lives inside the signtool-family config, so it moves together with the rest of signtoolOptions:
{ "win": { "signtoolOptions": { "sign": "./customSign.js" } } } // Before (v26)
{ "win": { "sign": { "type": "signtool", "sign": "./customSign.js" } } } // After (v27)
migrate-schema performs this move. (A top-level win.sign: "./customSign.js" is a pre-v26 shape that v26 already rejected; move it under { "type": "signtool", "sign": … } by hand.)
A hook now also needs win.sign.publisherName, even with a certificate configured (the publisher name of a hook is never derived from a certificate) — see Windows publisherName.
win.signExecutable / win.signAndEditExecutable removed
| Removed | Replacement |
|---|---|
win.signExecutable: false | win.sign: false (disables signing; resource editing still runs) |
win.signExecutable: true | (deleted — signing is enabled by default when credentials are available) |
win.signAndEditExecutable: true | (deleted — resource editing always runs) |
win.signAndEditExecutable: false | win.sign: false for the signing half; no equivalent for skipping resource editing — see note below |
win.signAndEditExecutable: false formerly skipped both resource editing (icon, metadata) and signing. In v27 resource editing always runs. migrate-schema maps the signing half to win.sign: false and warns about the rest. If you need to skip resource editing for a specific artifact, apply resources manually after building.
Both flags set to false skipped all signing in v26, so any win.signtoolOptions / win.azureSignOptions next to them never applied. migrate-schema removes those blocks (naming them in a warning, without printing their values) rather than turning signing on; to sign in v27, replace win.sign: false with a win.sign: { type: … } object.
electronDownload → electronGet
The electronDownload configuration key is renamed to electronGet and reshaped to match @electron/get's options directly (v27 upgrades to @electron/get v5, which downloads via fetch).
Old electronDownload.* | New electronGet.* |
|---|---|
mirror | mirrorOptions.mirror |
isVerifyChecksum: false | unsafelyDisableChecksums: true |
cache | (not a config key — set the ELECTRON_BUILDER_CACHE environment variable) |
customDir, customFilename, strictSSL, force, platform, arch, version | (no equivalent — dropped by migrate-schema with a warning) |
{ "electronDownload": { "mirror": "https://my-mirror/" } } // Before
{ "electronGet": { "mirrorOptions": { "mirror": "https://my-mirror/" } } } // After
snap → snapcraft
The top-level snap configuration key is removed. Use snapcraft with an explicit base field and per-base options nested under a sub-key named after the base.
// Before
{ "build": { "snap": { "confinement": "strict", "stagePackages": ["libfoo"], "base": "core22" } } }
// After
{ "build": { "snapcraft": { "base": "core22", "core22": { "confinement": "strict", "stagePackages": ["libfoo"] } } } }
Supported base values: "core18", "core20", "core22", "core24", and "custom". migrate-schema performs this restructuring; when the old config has no base, it assumes "core20" and warns so you can confirm. A base: "custom" config (inline/path snapcraft.yaml) is moved verbatim. The SnapOptions export is also removed — see Removed exports.
CLI changes
Implicit --publish removed
v27 no longer auto-publishes based on the presence of CI tag environment variables, git tags, or npm lifecycle events. Pass --publish <always|onTag|onTagOrDraft|never> explicitly in your release scripts, or set the publish option in your configuration.
Why: unexpected auto-publishing could accidentally expose secrets or publish unfinished work. Making publishing explicit closes that hole.
Removed flags: --em.build, --em.directories
These flags are removed (they have thrown since v22).
| Removed flag | Replacement |
|---|---|
--em.build | -c (pass build config inline) |
--em.directories | -c.directories |
New command: migrate-schema
v27 adds electron-builder migrate-schema, which rewrites your config to v27 form in place and auto-migrates static (json/json5/yaml/package.json) and programmatic (.js/.ts/.cjs/.mjs) configs. TOML configs are parsed and the required changes printed, but not written. See the walkthrough for flags and the before/after of every rewrite.
It rewrites every entry marked Auto ✓ on this page: removed keys (electronCompile, framework/nodeVersion/launchUiVersion, disableDefaultIgnoredFiles, appImage.systemIntegration, linux.syncDesktopName, mac.gatekeeperAssess), the nativeModules, asar (root and platform-level), mac.sign, mac.universal, win.sign, electronGet, and snapcraft restructurings, GitHub vPrefixedTagName → tagNamePrefix, helper-bundle-id, squirrelWindows.noMsi, root-level directories in package.json, and toolsets.* values v27 rejects (null, the retired appimage: "1.0.2"). Values with no v27 equivalent (for example nativeRebuilder: "legacy" or electronDownload.strictSSL) are dropped with a warning.
It warns but leaves in place what cannot be rewritten mechanically: squirrelWindows.customSquirrelVendorDir, a custom NSIS bundle in customNsisBinary / customNsisResources (→ toolsets.nsis; it does move customNsisBinary.debugLogging → installerDebugLogging), a custom mac.sign signer combined with sibling signing options, and (in JS/TS configs) platform-level asarUnpack next to root-level ASAR options.
It prints an advisory (without changing the config) for runtime defaults and behavior changes you should check: an nsis-web target (disableWebInstaller), nsis.perMachine or nsisWeb.perMachine set to true (per-machine NSIS updates), a custom win.sign.sign hook without win.sign.publisherName, unless win.verifyUpdateCodeSignature is false (publisher name), a generic publish configuration whose url has a query string (update credentials), and a mac/mas/masDev section that names no entitlements file (tightened default entitlements).
It does not touch anything outside the config: CLI flags, environment variables, custom NSIS scripts, Linux maintainer scripts, app code using the electron-updater API, or plugins using PlatformPackager. Every other row marked — in the table above is a manual step.
Publishing (electron-publish)
Bitbucket Cloud publishing: token without username uses Bearer auth
The Bitbucket publisher now selects its authentication scheme by whether a username is present:
- Username present (
bitbucket.username, or theBITBUCKET_USERNAMEenv var) → HTTP Basic auth, as before. Use this for an app password or an Atlassian API token (the username is your Bitbucket username or Atlassian account email). - No username → the token is sent as
Authorization: Bearer <token>(a repository / project / workspace access token). This is the new behavior; previously a token was always sent as Basic auth using the repository owner as the username.
Action is required only if your CI sets BITBUCKET_TOKEN (or bitbucket.token) to an app password / API token without a username — those requests now go out as Bearer and will fail authentication. Set BITBUCKET_USERNAME (or bitbucket.username) so Basic auth is used. Genuine access tokens need no username.
New: Cloudflare R2 publish provider (additive)
v27 adds an opt-in provider: "r2" publish target (Cloudflare R2, an S3-compatible object store) with matching electron-updater support. Credentials come from CF_R2_ACCESS_KEY_ID / CF_R2_SECRET_ACCESS_KEY; the config requires a Cloudflare accountId, and an https publicUrl (custom domain or pub-<hash>.r2.dev) is needed for auto-update downloads. This is purely additive — no migration action is required.
Toolsets & environment variables
Toolset defaults resolve to "latest" (newest bundle)
In v27 every toolsets.* property defaults to "latest" — an unset property or the literal "latest" resolves to the newest published bundle for that toolset. (Earlier v27 prereleases pinned a fixed default per toolset; those fixed defaults are gone.) The null value is no longer part of the ToolsetConfig type, and it is rejected by schema validation — each toolsets.* property is typed as a ToolsetCustom object or a version string, with no null branch. Switch null → "latest" or omit the key entirely; both resolve to the newest bundle. migrate-schema removes null entries. (In v26 an unset or null toolset resolved to the legacy "0.0.0" bundle, so pin "0.0.0" if you need to stay on it.) The appimage "1.0.2" pin is no longer offered; migrate-schema rewrites it to "1.0.3".
| Toolset | v26 default | v27 "latest" resolves to | What the upgrade entails |
|---|---|---|---|
wine | 0.0.0 (Wine 4.0.1, macOS only) | system | Host-installed wine on PATH, on macOS and Linux. Requires a host Wine on macOS — see the note below. The "1.0.1" Wine 11.0 bundle still downloads when pinned, but ships no PE builtins and cannot run Windows tools on its own |
winCodeSign | 0.0.0 (winCodeSign 2.6.0) | 1.3.0 | Windows Kits 10.0.26100.0; osslsigncode 2.11 + native arm64; bundles the Azure Trusted Signing dlib + .NET 8 runtime |
appimage | 0.0.0 (FUSE2 runtime) | 1.1.0 | Static FUSE3-compatible runtime (runs without a host FUSE install); adds unsquashfs support |
nsis | 0.0.0 (NSIS 3.0.4.1, split bundle) | 1.2.1 | NSIS 3.12; unified single-archive bundle; entrypoint scripts auto-set NSISDIR |
fpm | 2.2.1 | 2.2.1 | Unchanged — FPM 1.17.0 / Ruby 3.4.3 |
icons | 1.1.0 | 1.2.3 | Newer bundle — wasm-vips + @resvg/resvg-wasm; 16/32 px ICNS entries written as ARGB |
linuxToolsMac | 1.0.0 | 1.0.1 | Same tools rebuilt to run on macOS 15+ (1.0.0 required macOS 26) |
sevenZip | 1.0.0 | 1.0.1 | Correct per-arch Windows 7za.exe (x64, ia32, arm64) |
squirrel | (electron-winstaller npm package) | 1.1.1 | New toolset — Squirrel.Windows vendor bundle, replaces the electron-winstaller dependency |
No action required for most projects — the new bundles are drop-in replacements and produce identical output. wine is the exception: its default is no longer a bundle at all, so a macOS host that builds Windows targets now needs Wine installed (brew install --cask wine-stable). If you hit a regression introduced by a newer bundle, pin back by setting the toolset version to "0.0.0":
{ "build": { "toolsets": { "winCodeSign": "0.0.0", "nsis": "0.0.0", "appimage": "0.0.0", "wine": "0.0.0" } } }
This escape hatch is intended as a short-term workaround. The "0.0.0" alias may be removed in a future major release.
Toolset env-var overrides removed
This is a breaking change if you used env-var toolset overrides. The following environment variables are removed — replace each with a ToolsetCustom object on the relevant toolsets key:
| Removed env var | Toolset it controlled |
|---|---|
APPIMAGE_TOOLS_PATH | AppImage build tools (mksquashfs, runtime) |
LINUX_TOOLS_MAC_PATH | Linux-tools-mac bundle (ar, lzip, gtar) |
CUSTOM_FPM_PATH | FPM executable |
ELECTRON_BUILDER_NSIS_DIR | NSIS compiler bundle directory |
ELECTRON_BUILDER_NSIS_RESOURCES_DIR | NSIS resources/plugins directory |
CUSTOM_NSIS_RESOURCES | Alternate NSIS resources bundle |
ELECTRON_BUILDER_WINE_TOOLSET_DIR | Wine bundle directory |
USE_SYSTEM_WINE | Forced the host-installed Wine instead of the downloaded bundle (now the default — toolsets.wine: "system") |
USE_SYSTEM_SIGNCODE | Forced the host signtool/signcode instead of the bundled winCodeSign toolset |
USE_SYSTEM_OSSLSIGNCODE | Forced the host osslsigncode instead of the bundled one |
USE_SYSTEM_FPM | Forced the host-installed fpm instead of the bundled FPM |
SIGNTOOL_PATH | Path to signtool.exe |
ELECTRON_BUILDER_7ZIP_PATH | The 7-Zip executable |
ELECTRON_BUILDER_ICONS_TOOLSET_DIR | Icons toolset bundle directory |
Setting any of these now fails the build with a message naming the toolsets replacement, rather than being silently ignored. The two signing USE_SYSTEM_* variables (USE_SYSTEM_SIGNCODE, USE_SYSTEM_OSSLSIGNCODE) have no env-var replacement — configure signing through win.sign and the winCodeSign toolset instead. USE_SYSTEM_WINE is replaced by toolsets.wine: "system", which is already the default. USE_SYSTEM_FPM is now also removed (it was still functional in earlier v27 prereleases): supply a custom FPM via toolsets.fpm: { url: "file:///path/to/dir" }. On Windows there is no bundled FPM, so an FPM-based target now requires an explicit custom toolsets.fpm and otherwise throws a clear configuration error (previously it silently fell back to a host fpm on PATH).
The url accepts an https:// URL (downloaded and cached automatically) or a file:// path (used as-is). The bundle must mirror the directory layout of the corresponding built-in bundle (see electron-builder-binaries/packages).
// Remote bundle (URL)
{ "build": { "toolsets": { "nsis": { "url": "https://example.com/my-nsis-bundle-1.0.tar.gz", "checksum": "<SHA-256 hex or base64 SHA-512 of the archive>", "version": "my-custom-1.0" } } } }
// Local directory (no checksum required)
{ "build": { "toolsets": { "appimage": { "url": "file:///path/to/my-appimage-tools-dir" } } } }
Wine note: macOS and Linux both use the host-installed
wineonPATHby default ("latest"resolves to"system"), soUSE_SYSTEM_WINEcan simply be dropped. To point at a custom Wine build instead, supply aToolsetCustomobject ontoolsets.wine; note that such a directory must contain a prebuiltwine-homeprefix alongsidebin/andlib/, so the host install is the simpler option for a stock Wine installation.
Supported archive formats: .zip, .7z, .tar.gz, .tar.xz. Exception for sevenZip: because 7-Zip is used to extract .7z and .tar.xz archives, a custom sevenZip bundle can only be supplied as a .tar.gz, .zip, or bare file:// directory.
pacman default dependencies changed
http-parser was dropped from the default pacman depends list. Additionally, depends for deb/rpm/pacman now accepts the "default" keyword, which expands to the built-in list so you can extend it instead of replacing it:
{ "build": { "pacman": { "depends": ["default", "my-extra-package"] } } }
ElectronGetOptions.force removed
The force field has no equivalent in @electron/get v5 and was dropped from ElectronGetOptions. Clear the cache directory instead (or set ELECTRON_BUILDER_CACHE to a fresh path) to force a re-download. migrate-schema drops it with a warning.
CI_BUILD_TAG environment variable
Removed. Use CI_COMMIT_TAG (the standard GitLab CI variable) to provide the release tag.
Azure Trusted Signing signtool /dlib is the default
Azure Trusted Signing (win.sign: { type: "azure" }) uses the faster signtool /dlib path automatically: the default winCodeSign ("latest" → 1.3.0) ships the ATS dlib + .NET 8 payload, so no pin is required. To force the legacy PowerShell Invoke-TrustedSigning path instead (which requires the TrustedSigning PS module in the signing environment), pin winCodeSign below 1.3.0:
{ "build": { "toolsets": { "winCodeSign": "1.2.1" } } } // or "0.0.0" — forces the legacy PowerShell signing path
The signer resolves the path from the winCodeSign value: unset / null / "latest" and any explicit version >= 1.3.0 use signtool /dlib; a version below 1.3.0 (no dlib in the bundle) uses PowerShell. A ToolsetCustom object uses dlib from your supplied bundle.
New: MSIX target (beta)
v27 adds a beta msix Windows target alongside appx, producing .msix, .msixbundle (multi-arch), and .msixupload (Store) artifacts with modern manifest features that AppX cannot express — Package Integrity (uap10) and Windows Services (desktop6). It is purely additive: no migration is required. It works with the default winCodeSign toolset (only the legacy 0.0.0 bundle, which lacks the MSIX-capable Windows SDK, is rejected). MSIX builds on Windows 10+ natively or on macOS via Parallels Desktop (Linux is unsupported), and does not auto-update via electron-updater (updates come from the Microsoft Store or App Installer). See the MSIX target documentation for the full configuration surface.
win:
target: msix
# no toolsets pin needed — the default winCodeSign is MSIX-capable
NSIS & behavior changes
NSIS file-association ProgID format changed
NSIS installers that declare fileAssociations now register each association under a unique, Microsoft-recommendation-compliant ProgID instead of using the association name (or extension) verbatim. The previous value could collide with unrelated applications — most easily when forking a project without changing fileAssociations[].name.
The generated ProgID has the form <program>.<component>: <program> is derived from your productName (so it stays readable in the registry) and <component> mixes a short readable prefix with a value derived from the app GUID. It is at most 39 characters, contains only letters, digits, and a single period, and never starts with a digit.
No config change is required, and there is nothing to auto-migrate. fileAssociations and its name / ext / description fields are unchanged; the new ProgID is produced automatically. The installer and uninstaller derive the same ProgID, so registration and removal stay in sync.
Action is required only if you ship a custom NSIS script (nsis.include / nsis.script) or external tooling that hard-codes the old ProgID — the association name or extension — for example to add extra shell verbs or registry entries under that key. Update those references to the new generated value.
On upgrade, an installer built with v27 registers the new ProgID. One-click installers run the previous version's uninstaller during the upgrade, which removes the old-format entry; with assisted installers that do not uninstall the prior version first, the old ProgID may remain in the registry until that version is removed.
NSIS customInstallMode macro guard casing corrected
The guard around the customInstallMode macro in multiUserUi.nsh previously checked !ifmacrodef customInstallmode (lowercase m), while the documentation and the adjacent !insertmacro customInstallMode call use customInstallMode. In v27 the guard checks the documented name, customInstallMode.
No action is required if your custom NSIS script (build/installer.nsh or nsis.include) defines the macro with the documented casing — it is now reliably applied on the install-mode page of assisted multi-user installers (e.g. to set $isForceMachineInstall).
Action is required only if you defined the macro under the exact lowercase name customInstallmode, which relied on the undocumented casing of the old guard — rename the definition to customInstallMode:
!macro customInstallMode
# set $isForceCurrentInstall or $isForceMachineInstall here
!macroend
Linux launcher entrypoint
Every Linux target (deb/rpm, AppImage, snap, flatpak) now launches through a generated <executableName>-launcher shell script rather than invoking the executable directly. Two things change in the built artifact:
- The package ships a new file —
<executableName>-launcher(e.g./opt/<app>/MyApp-launcher). - The generated
.desktopExeckey points at the launcher instead of the executable (e.g.Exec=/opt/MyApp/MyApp-launcher %U), andexecutableArgs/forceX11flags are injected into the launcher rather than inlined intoExec.
This makes executableArgs apply consistently across all Linux targets and keeps the .desktop Exec a plain command.
Action is required only if you ship a custom .desktop override, an AppArmor/snap profile, a MIME handler, or external tooling that hard-codes the Exec command or assumes the executable itself is the launch target. Point those at the *-launcher script (or the executable, as appropriate).
Desktop-entry field codes in executableArgs are now literal. v26 passed an argument like %F into the .desktop Exec key unquoted (and skipped appending %U). In v27 executableArgs are injected into the launcher script and quoted, so %F reaches your app as the literal string "%F" instead of the file list. If you relied on a field code to receive opened files, remove it from executableArgs and set linux.desktop.entry.Exec instead. electron-builder warns when it sees one.
AppImage --no-sandbox default changed. The default --no-sandbox launch argument is now injected only for the legacy FUSE2 runtime (toolsets.appimage: "0.0.0"). With the default static FUSE3 runtime (unset / "latest" → 1.1.0), --no-sandbox is no longer added automatically — AppRun adds it on its own only when user namespaces are unavailable. If you need the Chromium sandbox disabled unconditionally, set executableArgs: ["--no-sandbox"] explicitly.
node_modules are now arch/os-filtered on every build
v27 filters node_modules by each package's package.json cpu / os fields against the target arch and platform on every build (previously this effectively only mattered for universal macOS builds). A dependency that declares an incompatible cpu/os for the target is excluded from the packaged app, whereas v26 copied host-installed node_modules verbatim.
No action is required for typical projects — this fixes universal-build failures and produces correctly-scoped output. Be aware of it only if you intentionally bundled a cross-arch or cross-os optional binary that is now dropped; in that rare case, include it explicitly via extraResources / files.
arch: "all" now expands to x64 and arm64; 32-bit fails fast on Electron 44+
Two related architecture changes ship in v27:
arch: "all"no longer includesia32.Platform.WINDOWS.createTarget(target, Arch.all)(and the equivalent--arch all/ default multi-arch build) now expands to x64 + arm64 on Windows and Linux, instead of the old x64 + ia32. Electron 44 removed Windowsia32builds, and Linuxia32zips already ended at Electron 19, so the old expansion produced broken or impossible builds on current Electron. To keep building 32-bit, requestia32explicitly (e.g.Arch.ia32, or--ia32).- Windows
ia32/ Linuxarmv7lfail fast on Electron ≥ 44. Requesting either of those arches against Electron>= 44(which removed those builds) now throws a clearInvalidConfigurationErrorbefore downloading, instead of dying on an opaque404. The check is downgraded to a warning when a customelectronDistor Electron mirror is configured (it may still provide 32-bit builds).
Error: Electron 44.0.0 does not provide Windows ia32 builds — Electron 44 removed
Windows ia32 and Linux armv7l support. Use electronVersion <= 43.x to keep building
for ia32 (32-bit is supported until the v43 series reaches end-of-life in January 2027),
or drop the ia32 target.
Action: if you ship 32-bit Windows/Linux builds, either pin electronVersion to 43.x or earlier (supported until January 2027) and request ia32/armv7l explicitly, or drop those targets. Projects that relied on "all" implicitly producing ia32 must now list it explicitly.
macOS productName and executableName are validated, not sanitized
electron-builder now rejects a macOS productName or executableName that would require filename sanitization, throwing an InvalidConfigurationError at build start (assertSafeHelperName). Previously such names were silently normalized/sanitized, which could make the generated <Name> Helper.app bundles diverge from CFBundleName and break Electron's helper-process discovery.
Action is required only if a build starts failing with an "is not a valid macOS app bundle name" error — choose a productName/executableName that needs no sanitization (avoid path separators, control characters, and other characters stripped by filename sanitization).
Names are also no longer NFD-normalized. The .app bundle, the helper bundles, and CFBundleName now use the product name exactly as configured. For any name containing accented characters this changes the bytes of the on-disk filename, which is what external notarization and upload tooling matches on — update anything that hard-codes the normalized form. Relatedly, archive creation falls back from 7z to zip for NFD-normalized filenames.
Redundant production dependencies are excluded, not rejected
In v26, listing electron or electron-builder under dependencies aborted the build with a hard InvalidConfigurationError. In v27 these packages are excluded from the copied node_modules (logged once) and the build proceeds. They stay valid production dependencies for tooling such as SBOM, license, and vulnerability tracking — electron-builder simply provides them another way (the Electron runtime is embedded separately), so a copy inside app.asar would be redundant.
The excluded set is configurable through the new ignoredProductionDependencies build option, which defaults to:
{
"build": {
"ignoredProductionDependencies": ["electron", "electron-builder"]
}
}
Overriding the option replaces the default list, so keep electron and electron-builder in it unless you intend to ship them.
-
Add a name to exclude a production dependency that does not need a copy in
node_modules. This is the common case when a bundler (Vite, webpack, esbuild, …) already inlines the package into your app code: keep it independenciesso SBOM/license tooling still sees it, and add it here so a duplicate copy is not packaged.{"build": {"ignoredProductionDependencies": ["electron", "electron-builder", "react", "react-dom"]}} -
Remove a name to keep it in the copied
node_modulesafter all (e.g. drop"electron-builder"from the list to ship it inside the app).
Only dependencies your app declares directly are eligible for exclusion. A listed name is dropped together with the transitive dependencies required only by it, while anything also required by a legitimate production dependency (npm/pnpm dedupe it into a single hoisted entry) is kept — and a matching name that appears solely as a transitive dependency of a kept package is never excluded. One footgun to be aware of: a kept package that require()s an excluded name at runtime without declaring it (relying on hoisting of your app's copy) will crash with MODULE_NOT_FOUND — declare that dependency properly in the package that needs it, or remove the name from the list. Matching is by the declared dependency name, so an npm alias ("custom-electron": "npm:electron@^30.0.0") must be listed by its alias key.
ALLOW_ELECTRON_BUILDER_AS_PRODUCTION_DEPENDENCY is removed — and the default flipped
In v26, this (undocumented) environment variable was the only way to keep electron-builder in dependencies without erroring, and setting it meant the package was bundled into the app. In v27 the variable is ignored and the behavior is the opposite: electron-builder is in the default ignore list, so it is silently excluded from the copied node_modules (an info-level log is the only trace). If you relied on the env var to actually ship electron-builder inside your app, you must now override the list and drop "electron-builder" from it, e.g. "ignoredProductionDependencies": ["electron"].
electron-prebuilt / electron-rebuild no longer error — and are NOT excluded
The v26 hard error also rejected electron-prebuilt and electron-rebuild in dependencies. v27 removes that guard without adding them to the default ignore list, so if you still declare them they now ship inside your app silently — electron-prebuilt drags a full Electron binary along. Both packages are long deprecated (electron-prebuilt was renamed to electron in 2016; electron-rebuild moved to @electron/rebuild), which is why this major release drops the special-casing instead of carrying it forward. electron-nightly was never guarded and is likewise not in the default list. If you use any of them:
- Preferred: migrate — replace
electron-prebuiltwithelectron, andelectron-rebuildwith@electron/rebuildindevDependencies. - Otherwise: move them to
devDependencies, or add them toignoredProductionDependencies(e.g.["electron", "electron-builder", "electron-prebuilt", "electron-nightly"]) if they must stay declared as production dependencies for tooling.
This is a runtime behavior change, not a config-key rename, so electron-builder migrate-schema does not modify your config for it.
Suffixed update channels now expand to lower channels
With generateUpdatesFilesForAllChannels enabled and a non-GitHub provider, beta published beta.yml + alpha.yml and latest published all three — but only when the channel was exactly alpha, beta, or latest. A channel with a suffix, such as the per-arch channel: "${channel}-${arch}" pattern that resolves to beta-arm64, fell through and published a single file, so suffixed pre-release users were never promoted.
v27 reads the base channel off the front of the name and reattaches the suffix:
beta-arm64→beta-arm64.yml,alpha-arm64.ymllatest-arm64→latest-arm64.yml,alpha-arm64.yml,beta-arm64.ymlalpha-arm64→ unchanged (lowest channel)- bare
alpha/beta/latest→ unchanged
This only affects you if you use generateUpdatesFilesForAllChannels with a suffixed channel name on a non-GitHub provider. In that case a suffixed channel now writes 2–3 update files where it previously wrote one, and a latest-x64 publish overwrites beta-x64.yml / alpha-x64.yml in the same bucket — changing what live beta-x64 users are offered. That is the same behaviour bare latest has always had, but it is new for suffixed channels. Set generateUpdatesFilesForAllChannels: false if you want suffixed channels to keep publishing a single file.
Dependencies & file copying
allowMissingDependencies now fails the build
A production dependency that cannot be resolved during node_modules collection is now a hard error. In v26 this logged cannot find path for dependency / dependency not found on disk warnings and produced a package that failed at runtime with MODULE_NOT_FOUND; v27 fails the build up front, listing every missing dependency at once.
{ "build": { "allowMissingDependencies": true } } // restore the v26 warn-only behavior
{ "build": { "allowMissingDependencies": ["some-pkg"] } } // allow only specific names
false / null / omitted (the default) fails on any missing production dependency. Missing optional dependencies (declared in optionalDependencies, e.g. fsevents on Linux/Windows) are always allowed and never fail the build.
Action: if a build that previously succeeded now fails here, fix the installation, list the affected names in allowMissingDependencies, or set it to true to keep the v26 behavior.
Native binaries are verified against the target
After the app is packed, electron-builder reads the header of every .node addon in app.asar, app.asar.unpacked (or the unpacked app directory) and checks its format and machine type (ELF e_machine, Mach-O cputype including every slice of a fat binary, PE Machine) against the target platform and arch. A mismatch is now a hard error naming the file, what it was built for and what was expected. In v26 a stale or host-platform binary left in node_modules by a skipped or cached rebuild was packaged silently and only failed at runtime (invalid ELF header, not a valid Win32 application, incompatible architecture). For a macOS universal build each per-arch slice is checked before the slices are merged.
Other native files (.so, .dylib, .dll, .exe, extensionless executables) that mismatch only log a warning, since packages legitimately bundle helper binaries for several platforms. Files whose package declares another platform in package.json os/cpu, or whose path names another platform/arch (prebuildify's prebuilds/linux-arm64/, @img/sharp-darwin-arm64), are skipped.
{ "build": { "nativeModules": { "verifyNativeBinaries": "warn" } } } // log mismatches, keep packaging
{ "build": { "nativeModules": { "verifyNativeBinaries": false } } } // skip the check
Relatedly, nativeModules.buildDependenciesFromSource no longer skips the native rebuild for a target whose platform differs from the host (which shipped the host's binary). Native modules cannot be cross-compiled from source, so for such a target the rebuild runs with prebuilt binaries for the target instead, and a warning is logged.
Action is required only if a build fails here — rebuild native dependencies for the target (for a cross-platform target, the module needs a prebuilt binary for it), or opt out as above.
extraFiles / extraResources destinations are validated
A to path that is absolute (POSIX, drive-letter, or UNC) or that escapes the build output directory now throws an InvalidConfigurationError. v26 accepted these and copied files onto the build machine outside the packaged app.
Action is required only if a build starts failing here — make the to path relative and keep it inside the output directory.
Windows publisherName is validated against the signing certificate
A configured win.sign.publisherName (v26: win.signtoolOptions.publisherName / win.azureSignOptions.publisherName) that does not match the signing certificate's subject now fails the build instead of producing an installer that electron-updater would reject every update for. The common triggers are a certificate rotation and the wrong certificate leaking into WIN_CSC_LINK / CSC_LINK in CI.
Action is required only if a build fails here — update win.sign.publisherName to the new certificate subject, sign with the intended certificate, or set publisherName to null to opt out of update signature verification entirely.
A code-signed build whose publisher name cannot be determined now also fails with InvalidConfigurationError. This covers every custom win.sign.sign hook without win.sign.publisherName: a hook may sign with another certificate than any electron-builder can read, so its publisher name is never derived from a certificate, not even one in the config (certificateFile, certificateSubjectName, certificateSha1, cscLink) or from WIN_CSC_LINK / CSC_LINK. It also covers a certificate without a Common Name (including a certificate-store subject). It applies only to builds that write app-update.yml: nsis, nsis-web and electronUpdaterAware appx targets with a publish configuration, including one inferred from a GitHub repository.
Action: set win.sign.publisherName to the subject of the certificate that signs your app, copied from a binary it already signed (how) — every component you list must match. Set win.verifyUpdateCodeSignature: false instead only if your updates are not Authenticode-signed or you don't use electron-updater.
Tip:
electron-builder migrate-schemaprints an advisory for a customwin.sign.signhook withoutwin.sign.publisherName, whatever certificate the config names.
Related, on the updater side: an
app-update.ymlwith nopublisherNamemeans signature verification is silently skipped. v27 warns; v28 will fail closed.
macOS DMG
DMG filesystem defaults to APFS
The default DMG volume filesystem changed from HFS+ to APFS. APFS is the modern macOS filesystem and produces smaller, faster-to-mount images on current macOS.
- No action for most projects.
- If you must support pre-10.13 (High Sierra) macOS, which cannot mount APFS volumes, set the filesystem back to
HFS+explicitly:
{ "dmg": { "filesystem": "HFS+" } }
This is a runtime default, not a config-key rename, so migrate-schema does not change it.
New: dmg.format: "ULMO" (additive)
v27 adds a new dmg.format value, "ULMO" — an LZMA-compressed disk image (macOS 10.15+ only) — alongside the existing UDZO / ULFO / UDBZ / … options. This is purely additive; the default format is unchanged and no migration action is required.
Auto-update (electron-updater)
disableWebInstaller defaults to true
AppUpdater.disableWebInstaller now defaults to true. NSIS web installers (the small installer that downloads the full payload at install time from a manifest-supplied URL) are no longer loaded unless you opt in, because that payload may not undergo signature verification.
Unless disableWebInstaller is false, a web-installer update is rejected with ERR_UPDATER_WEB_INSTALLER_DISABLED — at download time (including an update already cached by a previous launch), before an install on next launch, and at install time.
- You do not use a web installer (the common case): no action — this is the safer default.
- Your installs were made by an
nsis-webinstaller built with v27+: no action — they opt in automatically. NSIS installers now write aresources/package-typemarker, andNsisUpdaterreads it to defaultdisableWebInstallertofalsefor web-installer installs. - Your web-installer installs lack that marker (installed by an installer built before v27, or with a custom script), or you are switching an app from
nsistonsis-web: opt in manually (below), or those installs will not update.
A web update cached by a previous launch, or pending an install on next launch, is used only if its web package still matches the freshly fetched manifest. A web-installer update whose update info has no sha512 for the web package is rejected with ERR_UPDATER_NO_CHECKSUM. The nsis-web installer now also verifies the package it installs: an explicit --package-file must match the SHA-512 of one of the packages built with it (any arch), and a package downloaded from the default publish-derived (versioned) URL must match the package for the detected arch; a package downloaded from an explicit appPackageUrl (e.g. a version-independent latest URL) is not verified. A mismatch aborts the installation (exit code 2) before the installed version is removed, so that version stays installed. The new nsisWeb.allowUnverifiedAppPackage option (default false) opts out, for one web installer used with packages of other builds. See Web Installer.
A local package — passed via --package-file (electron-updater does this for updates) or found next to the installer — is first copied into the installer's own temporary directory, and the installer verifies and installs that copy. The local package file is now left in place (the installer's copy is moved into the app's update cache instead; electron-updater removes the package it passed from its pending directory once the updated app starts), and a --package-file package that cannot be copied aborts the installation (exit code 2), also before the installed version is removed; a package next to the installer that cannot be copied is ignored and downloaded instead, as when its checksum doesn't match.
Because the manifest vouches for the web package, don't set updateManifest: false for nsis-web apps: signed update manifests cover its path, SHA-512 and size.
Action: if you publish and rely on an NSIS web installer and your installs lack the marker, or you are switching from nsis to nsis-web, opt in by setting disableWebInstaller: false in your main process:
import { NsisUpdater } from "electron-updater"
const updater = new NsisUpdater()
updater.disableWebInstaller = false // only if you intentionally ship a web installer
Tip: running
electron-builder migrate-schemaon a project that builds annsis-webtarget now prints an advisory: web-installer updates are rejected unless the app opts in, when to setautoUpdater.disableWebInstaller = falseat runtime, and when to setnsisWeb.allowUnverifiedAppPackage. The advisory is informational only — it never rewrites your config (disableWebInstalleris an electron-updater runtime setting, not a build-config key).
NSIS: per-machine builds set isAdminRightsRequired in the update info
electron-builder now sets isAdminRightsRequired: true in the update info of every per-machine nsis and nsis-web build (perMachine: true), including assisted installers (oneClick: false) that don't set packElevateHelper and builds with differentialPackage: false. For nsis-web builds it is written into the file entry of the installer, where electron-updater reads it. electron-updater starts these updates with elevate.exe right away and, like other per-machine updates, does not install them automatically at launch with autoInstallEvent: "onNextLaunch".
Action: none for most apps. With autoInstallEvent: "onNextLaunch", per-machine apps call installPendingUpdateIfAvailable() to install a pending update.
Tip:
electron-builder migrate-schemaprints an advisory whennsis.perMachineornsisWeb.perMachineistrue.
Update credentials stay on the feed's origin
electron-updater now sends the credential headers from requestHeaders / addAuthHeader — headers such as Authorization, the same set that is removed on a cross-origin redirect — and the query string of the feed url only to the feed's origin. This applies to the generic, s3, spaces, r2, keygen, bitbucket, github and gitlab providers.
- An installer, web package, blockmap or differential range request on another origin is requested without those headers and without the feed query. Another origin is another host, or the same host with another port or scheme (
https://updates.example.com→https://updates.example.com:8443orhttp://updates.example.com); as on redirects, anhttp→httpsupgrade of the same host on the default ports keeps the headers and the feed query. Such a URL keeps its own query string, so pre-signed URLs work. With every provider, including custom ones, a URL that electron-updater resolves against the feed URL (such asfiles[].urlor a blockmap) gets the feed query only on the feed's origin. - Downloads on the feed origin (relative
files[].url, the default) are unchanged. For GitLab the feed origin ishttps://<host>, where electron-builder publishes the release assets. - When the app sets
previousBlockmapBaseUrlOverride, the old blockmap is downloaded with the credentials if it is on that origin. - A blockmap is downloaded from
${url}.blockmapwith the file URL's query string, which does not fit a pre-signed installer URL. The new, optionalfiles[].blockMapUrlinlatest*.ymlnames the blockmap's own URL; a relative value resolves likeurl, an absolute one is used as-is; it follows the same origin rule and is covered by the manifest signature when present. With it, the old blockmap comes from the local cache orpreviousBlockmapBaseUrlOverride, else that update is downloaded in full. electron-builder does not write it. - A custom provider that extends a built-in provider (e.g.
GenericProviderorGitHubProvider) inherits itsfeedBaseUrl. Any other custom provider must declare it by overridingProvider.feedBaseUrl: its feed URL (the credential headers then only go to that origin) ornull(the request headers go to every download URL, as before). If it declares neither and the download headers include a credential header (fromrequestHeaders,addAuthHeaderor the provider'sfileExtraDownloadHeaders),downloadUpdate()fails withERR_UPDATER_FEED_BASE_URL_NOT_DECLAREDbefore any download request. Without credential headers nothing changes.
electron-builder warns once per feed when a generic publish url (the feed written to app-update.yml) has a query string, naming its parameters, and electron-updater announces the change at run time: the first download that loses credential headers logs a warning naming the headers and the two origins, and the first one that does not get the feed query logs one naming the query parameters (never their values). A signature failure on a manifest with a blockMapUrl says that it is covered by the signature.
Action: a custom provider that does not extend a built-in one and is used with credential headers must override Provider.feedBaseUrl (see above). Otherwise none if your update files are served from the feed origin. If latest*.yml uses absolute URLs on another origin that authenticate with your requestHeaders / addAuthHeader, or with a token in the feed URL's query (e.g. url: https://updates.example.com/?key=…), those downloads now fail (for example with 401 or 403). Serve the files from the feed origin, or use pre-signed URLs. The manifest signature, required by default in v27, covers files[].url and packages.<arch>.path, so pre-signed URLs have to be in latest*.yml before it is signed (see Key storage); installs with a public key refuse a manifest whose URLs changed after signing.
Tip:
electron-builder migrate-schemaprints an advisory for agenericpublish configuration whoseurlhas a query string.
latest*.yml drops legacy top-level path/sha512
The deprecated top-level path and sha512 fields are no longer written to the generated update-metadata files (latest.yml / latest-mac.yml / latest-linux.yml) by default. They were kept for electron-updater 1.x – 2.15.0, which predate the files[] array (introduced in 2.16.0, released 2017). Both fields are now emitted only when electronUpdaterCompatibility declares a range that intersects those legacy versions — the same mechanism that already controlled the Windows sha2 field and the legacy latest-mac.json. The default electronUpdaterCompatibility is now >=2.16 (previously >=2.15), and UpdateInfo.path / UpdateInfo.sha512 are now optional on the exported TypeScript type.
- No action for virtually all projects: every electron-updater since 2.16.0 reads
files[]and ignores the top-level fields. Already-publishedlatest*.ymlfiles are unaffected. - If your app code reads
info.path/info.sha512(e.g. in anupdate-downloadedhandler), switch toinfo.files[0].url/info.files[0].sha512. - If external tooling parses
latest.yml(a custom update server or dashboard), point it atfiles[]. - If you still ship apps embedding electron-updater 1.x – 2.15, keep emitting the legacy descriptor by declaring a compatibility range that includes them, e.g.
"electronUpdaterCompatibility": ">=1.0.0"(also settable per platform, e.g.win.electronUpdaterCompatibility).
Related: metadata validated only by the legacy SHA-256
sha2checksum is deprecated — v27 warns and v28 will reject sha2-only metadata (fail-closed). Avoid pinningelectronUpdaterCompatibilityto a legacy range unless you actually ship 1.x–2.15 clients.
Signed update manifests are required
Ed25519 signing of latest*.yml is new in v27 and required by default. A build with a publish policy that includes a target writing auto-update metadata — NSIS, AppImage, deb/rpm/pacman, macOS zip/dmg, or AppX with electronUpdaterAware — now fails unless a signing key resolves. The check runs at build start, before anything is packed or uploaded:
auto-update manifests must be signed, but no Ed25519 signing key was found for mac. Generate one with
`electron-builder create-update-key`, then supply it via the ELECTRON_BUILDER_UPDATE_SIGN_KEY (PEM contents)
or ELECTRON_BUILDER_UPDATE_SIGN_KEY_FILE (path) environment variable, or via updateManifest.signingKey /
updateManifest.signingKeyFile in the build configuration. To publish unsigned manifests anyway, opt out
explicitly with `updateManifest: false`.
Only a publishing build fails. A build without a publish policy warns instead, so local builds keep working — which matters because app-update.yml is generated whenever a publish configuration exists or can be inferred from your repository field.
To adopt it (recommended):
npx electron-builder create-update-key # once; store the PEM as a CI secret
ELECTRON_BUILDER_UPDATE_SIGN_KEY="$(cat update-private-key.pem)" electron-builder --publish always
To opt out, set updateManifest: false at the top level or under a platform key. Every build that emits a manifest then logs a warning. false also short-circuits the environment variables, so a leftover ELECTRON_BUILDER_UPDATE_SIGN_KEY cannot silently re-enable signing. A platform value of null is not an opt-out — it falls back to the top-level value.
A publishing build whose targets write no update info (for example only snap, flatpak, MSI/MSIX, portable, mas/pkg or plain archives) needs no key. Two more cases need no configuration at all: when every publish provider sets publishAutoUpdate: false no manifest is emitted, so the requirement is waived; and a publicKey with no private key does not satisfy it, since electron-builder still has nothing to sign with (that HSM/KMS workflow either builds without a publish policy, or publishes with publishAutoUpdate: false and generates the manifest itself — see Key storage).
Once a release embeds a public key, its installs are fail-closed: an unsigned manifest is rejected with ERR_UPDATER_MANIFEST_NOT_SIGNED and those installs stop updating entirely. Do not switch to updateManifest: false after shipping signed manifests, and make sure every pipeline that can publish a release — including a hotfix built from a laptop — has the key.
Note that installs already in the field are unaffected but also unprotected: they carry whatever app-update.yml they shipped with, so those built before you adopted signing keep accepting unsigned manifests. Full protection begins with the installs built after the public key was embedded.
See Signed Update Manifests and Key Rotation.
app-update.yml feed follows the update-writing targets
The feed an installed app polls — the first provider embedded in its app-update.yml, together with the manifest public key — is now resolved from the publish settings of the targets that write update info, not only from the platform/top-level publish. This changes package contents in three cases:
- Target-level
publish(e.g.nsis.publish,appImage.publish,dmg.publish) now decides the feed. Annsis.publish-only build used to ship noapp-update.yml(or, with a GitHubrepository, a GitHub feed while the manifests went elsewhere); it now ships that feed and the key that verifies its signed manifests. - Providers with
publishAutoUpdate: falseare skipped when a later provider receives the manifest, also in platform/top-level lists:win.publish: [{ provider: "s3", …, publishAutoUpdate: false }, "github"]now embeds GitHub, where v26 embedded the S3 feed that never gets alatest*.yml. When every provider setspublishAutoUpdate: false(the external-signing route), the first one is still embedded. - Targets of one app must agree. One packaged app holds a single
app-update.yml, so two update-writing targets built from it (for exampledmgandzip, ornsisand an updater-awareappx) whose feeds differ fail a publishing build at build start with anInvalidConfigurationError; a build without a publish policy warns that publishing will fail and writes noapp-update.ymlfor that app. Only feed-identifying options are compared — upload-only ones such aspublishAutoUpdateortimeoutmay differ.
Builds with no update-writing target, or whose only such target opts out (publish: null), keep the platform/top-level feed and the GitHub fallback as before. Action: only if you use target-level publish, per-target feeds within one app, or a disabled first provider — configure publish once at the platform level, or check that the embedded feed is the one you publish latest*.yml to. See which settings become the auto-update feed.
New: allowUnverifiedLinuxPackages (opt-in Linux package-signature verification)
AppUpdater.allowUnverifiedLinuxPackages is a new flag (default true, preserving historical behavior). Set it to false to enforce GPG signature checks when installing .deb / .rpm auto-updates on package managers that support verification. Because electron-builder does not sign Linux packages itself, the default remains permissive; this is additive and requires no migration action.
import { autoUpdater } from "electron-updater"
autoUpdater.allowUnverifiedLinuxPackages = false // enforce GPG signature checks on .deb/.rpm updates
quitAndInstall takes an options object
AppUpdater.quitAndInstall(isSilent?, isForceRunAfter?) replaced its positional boolean arguments with a single destructured options object. This is a hard compile break in TypeScript (plain JavaScript callers must update by hand — positional booleans are silently ignored):
// Before (v26)
autoUpdater.quitAndInstall(true, false)
// After (v27)
autoUpdater.quitAndInstall({ isSilent: true, isForceRunAfter: false })
Defaults are unchanged (isSilent: false, isForceRunAfter: false), so quitAndInstall() with no arguments behaves exactly as before. The object form also carries the new v27 waitUntilNextLaunch flag, which defers the install to the next application launch instead of spawning the installer on quit — see Install on Next Launch.
autoInstallEvent replaces autoInstallOnAppQuit
The autoInstallOnAppQuit boolean on AppUpdater is replaced by a single enum, autoInstallEvent: "manual" | "onQuit" | "onNextLaunch" (default "onQuit"), and is removed with no compat alias. A single boolean cannot represent the three states, so this is a clean replacement rather than a shim.
| Before | After |
|---|---|
autoInstallOnAppQuit = true (default) | autoInstallEvent = "onQuit" (default) |
autoInstallOnAppQuit = false | autoInstallEvent = "manual" |
| — (new in v27) | autoInstallEvent = "onNextLaunch" |
// Before
autoUpdater.autoInstallOnAppQuit = false
// After
autoUpdater.autoInstallEvent = "manual"
The default "onQuit" preserves prior behavior, so most apps need no change. "onNextLaunch" defers the install to the next launch to avoid the OS killing the installer during session end, and is planned to become the default in v28 — see Install on Next Launch.
downloadUpdate() resolves with a DownloadExecutorResult object
AppUpdater.downloadUpdate() — and UpdateCheckResult.downloadPromise, the same promise handed back by checkForUpdates() when autoDownload is enabled — used to resolve with a positional Array<string>: [updateFile], or [updateFile, packageFile] for NSIS web installers. It now resolves with a DownloadExecutorResult object (exported from electron-updater) that names both files:
interface DownloadExecutorResult {
readonly updateFile: string // the downloaded installer / AppImage / zip
readonly packageFile?: string // the NSIS web-installer package, only set for web installers
}
This is a hard compile break in TypeScript; plain JavaScript callers that index the result (files[0]) receive undefined and must update by hand:
// Before (v26 / electron-updater 6)
const files = await autoUpdater.downloadUpdate()
const installer = files[0]
const result = await autoUpdater.checkForUpdates()
const [downloaded] = (await result?.downloadPromise) ?? []
// After (v27 / electron-updater 7)
const { updateFile, packageFile } = await autoUpdater.downloadUpdate()
const result = await autoUpdater.checkForUpdates()
const downloaded = (await result?.downloadPromise)?.updateFile
Code that only awaits the promise for its side effects, or that relies on the update-downloaded event, needs no change. Additively, the update-downloaded event payload (UpdateDownloadedEvent) now also carries packageFile for web installers, next to the existing downloadedFile.
verifyUpdateCodeSignature renamed to verifyUpdateFileAuthenticodeSignature
NSIS Authenticode verification no longer uses the null-means-success / string-means-error convention. NsisUpdater.verifyUpdateCodeSignature is renamed to verifyUpdateFileAuthenticodeSignature and returns a result object:
type VerifyUpdateFileResult = { response: "success" } | { response: "failure"; message: string }
// Before (v6)
autoUpdater.verifyUpdateCodeSignature = async (publisherNames, path) => (isValid ? null : "why it failed")
// After (v7)
autoUpdater.verifyUpdateFileAuthenticodeSignature = async (publisherNames, path) =>
isValid ? { response: "success" } : { response: "failure", message: "why it failed" }
The old name is kept as a compatibility shim that translates in both directions, so existing code keeps working; it is deprecated and will be removed in v28. The same applies to the protected _verifyUpdateCodeSignature member, which is renamed to _verifyUpdateFileAuthenticodeSignature — a subclass that assigns the old name still works through a deprecated accessor, but a subclass that redeclares it as a class field shadows that accessor and must be migrated.
If you override the protected BaseUpdater.verifyInstallerSignatureOnLaunch (added earlier in the same v27 pre-release cycle, so only 7.0.0-alpha users are affected), return { response: "success" } rather than null — null is now read as a verification failure and blocks every install-on-next-launch.
Additively, all updaters gain AppUpdater.verifyUpdateFile for app-defined verification of the downloaded file on every path that can lead to an install. See Custom downloaded-file verification.
Programmatic & plugin-author API changes
This section only affects you if you import from
app-builder-liband access thepackagerorplatformPackagerobjects directly. Standard project configurations are unaffected.
info: Packager is now protected
PlatformPackager exposed a public info: Packager field in v26. In v27 it is protected — a hard compile break, not a soft deprecation: external code that chains through .info. (a custom target, or a plugin importing app-builder-lib) no longer type-checks. All commonly-needed properties are now directly accessible on PlatformPackager — drop the .info. chain:
| Before (deprecated) | After |
|---|---|
packager.info.tempDirManager | packager.tempDirManager |
packager.info.metadata | packager.metadata |
packager.info.framework | packager.framework |
packager.info.cancellationToken | packager.cancellationToken |
packager.info.repositoryInfo | packager.repositoryInfo |
packager.info.relativeBuildResourcesDirname | packager.relativeBuildResourcesDirname |
packager.info.stageDirPathCustomizer | packager.stageDirPathCustomizer |
packager.info.areNodeModulesHandledExternally | packager.areNodeModulesHandledExternally |
packager.info.isPrepackedAppAsar | packager.isPrepackedAppAsar |
packager.info.appDir | packager.appDir |
packager.info.getWorkspaceRoot() | packager.getWorkspaceRoot() |
packager.info.emitArtifactBuildStarted(e) | packager.emitArtifactBuildStarted(e) |
packager.info.emitArtifactBuildCompleted(e) | packager.emitArtifactBuildCompleted(e) |
packager.info.emitArtifactCreated(e) | packager.emitArtifactCreated(e) |
packager.info.emitMsiProjectCreated(p) | packager.emitMsiProjectCreated(p) |
packager.info.emitAppxManifestCreated(p) | packager.emitAppxManifestCreated(p) |
The getters already on PlatformPackager in v26 are unchanged (config, projectDir, buildResourcesDir, packagerOptions, appInfo, debugLogger).
platformSpecificBuildOptions is now protected
External consumers should use the two new public helpers instead:
| Access pattern | v27 replacement |
|---|---|
packager.platformSpecificBuildOptions (direct read) | packager.platformOptions |
deepAssign({}, packager.platformSpecificBuildOptions, config.X) | packager.getOptionsForTarget<T>("X") |
platformOptions is a typed getter returning the same platform-level config object. getOptionsForTarget<T>(key) performs the standard merge of platform options with the named per-target key (e.g. "appx", "msi") and returns the result as T. All built-in targets have been migrated; custom targets extending PlatformPackager must update any direct .platformSpecificBuildOptions access.
Removed exports
ProtonFramework, LibUiFramework, and SnapOptions are removed from the public exports of app-builder-lib and electron-builder. CustomNsisBinary is removed from the public exports of app-builder-lib, together with the customNsisBinary option; use ToolsetCustom (for toolsets.nsis) instead. Use the Electron framework and the snapcraft config shape respectively. The removed framework support means framework: "proton" | "libui" no longer has any effect — see framework removed.
Renamed type exports
These exported TypeScript types were renamed with no compatibility alias — import { OldName } is now a hard compile error. Update the import to the new name:
| v26 export | v27 export |
|---|---|
ElectronDownloadOptions | ElectronGetOptions |
WindowsAzureSigningConfiguration | WindowsAzureSigningConfig |
WindowsSigntoolConfiguration | WindowsSigntoolSigningConfig |
ElectronGetOptions is re-exported from the top-level electron-builder package, so this affects ordinary TypeScript consumers, not just app-builder-lib plugin authors.
Design notes
Rationale behind the user-facing breaking changes, for those who want full transparency.
Why native ESM?
electron-builder historically shipped CommonJS. The move to native ESM was driven by the ecosystem: major dependencies (chalk, figures, ora, etc.) have dropped CJS support, and Node.js 22.12's stabilized require(esm) makes it safe to ship ESM without breaking CJS consumers. The minimum was set to 22.12.0 specifically because earlier Node.js versions require --experimental-require-module to require() an ESM package.
Why move native-module options under nativeModules?
In v26, options like buildDependenciesFromSource, nodeGypRebuild, npmRebuild, and nativeRebuilder were scattered at the root of Configuration alongside unrelated properties. They were grouped under nativeModules for the same reason directories and toolsets are sub-keys: related options should be co-located. The rename nativeRebuilder → rebuildMode makes the field name consistent with other enum-style selectors.
Why unify all Windows signing under win.sign?
In v26, Windows signing was split across two sibling root-level keys — signtoolOptions and azureSignOptions — with precedence rules that were easy to misconfigure (both present → Azure wins, silently). v27 replaces them with a single win.sign key typed as a discriminated union. This makes the active signing mode explicit, self-documenting in IntelliSense, and mirrors the macOS mac.sign shape so both platforms have the same mental model. The win.sign: false sentinel explicitly disables signing, distinguishing it from "not configured" (env-based discovery).
Why replace [k: string] in WindowsAzureSigningConfiguration?
The v27 Azure integration switches from PowerShell Invoke-TrustedSigning to signtool.exe /dlib /dmdf, which reads a metadata.json file. An explicit additionalMetadata: Record<string, string> field is cleaner than an index signature because it makes intent visible in IntelliSense and prevents accidental shadowing of the typed fields. The legacy PowerShell path remains available (triggered when toolsets.winCodeSign is pinned below "1.3.0").
Why restructure snap into snapcraft?
The snapcraft shape makes the base explicit and nests per-base options under a base-named sub-key, so a single config can describe multiple bases unambiguously and core24 (which uses the snapcraft CLI directly) can coexist with legacy bases. migrate-schema automates the move but assumes core20 when no base is present, because that is the v27 1-to-1 migration target — verify the assumption for your project.
Why consolidate macOS signing under mac.sign?
In v26, ~15 signing options were scattered across the root of MacConfiguration intermixed with unrelated packaging options. Grouping them under a single sign object makes the signing surface self-documenting and matches the Windows win.sign grouping. sign is now a typed pass-through (ElectronSignOptions) to @electron/osx-sign, so upstream options are forwarded directly. Note that ElectronSignOptions is a closed type (additionalProperties: false): a field added by a newer @electron/osx-sign is rejected until electron-builder regenerates its schema, so it is a pass-through of the options that exist, not an open bag. signIgnore was renamed to sign.ignore to match the osx-sign canonical name. The same grouping rationale produced mac.universal (a pass-through to @electron/universal).
Why rename electronDownload to electronGet?
v27 upgrades to @electron/get v5, which downloads via fetch (replacing the got-based path) and exposes a mirrorOptions object rather than the old flat mirror/customDir fields. Renaming the config key to electronGet and typing it directly as the library's own options removes electron-builder's hand-maintained translation layer — what you set is what @electron/get receives. A few legacy fields (customDir, customFilename, strictSSL) have no v5 equivalent; migrate-schema drops them with a warning. cache is not gone — it moved out of the config surface and is set with the ELECTRON_BUILDER_CACHE environment variable instead.
Why consolidate ASAR options under asar?
In v26, asarUnpack lived alongside asar, and disableSanityCheckAsar / disableAsarIntegrity lived at the root of Configuration. This was confusing: asarUnpack is meaningless when asar: false, yet nothing in the type system expressed that relationship. Moving all options under a single asar key makes the dependency explicit. The asar: true sentinel was left in place — it is redundant next to {}, but rejecting it would break working configs for no benefit, so migrate-schema merely drops it when folding in other ASAR keys.
PlatformPackager.info is now protected
The public info: Packager field created a hard coupling: any internal Packager refactor became a breaking change for all plugins. In v27, direct pass-through getters and methods were added for the properties plugin authors actually need, and info was changed from public to protected. This is a hard break in v27 (not a deferred deprecation): external packager.info.X access no longer compiles, so migrate to the direct getters listed above.
Internal changes (non-user-facing)
These changes have no impact on your build configuration or the public API. They are listed for transparency about what shipped in v27.
- Native ESM build pipeline. Babel was removed entirely; packages are compiled by
tscto native ESM withexportsmaps and full type declarations. - Code-quality modernization. Production code paths adopt native Node.js APIs and modern TypeScript patterns (
sleep()fromtimers/promises, native process signal handlers,Error.cause,Reflect.get/set, the WHATWGURLAPI, SHA-256 for WiX directory-ID generation). - Dead-code removal.
ProtonFramework,LibUiFramework, andbinDownload.tswere deleted; all binary downloads were consolidated into a singledownloadBuilderToolsetpath. - Flags consolidation. All boolean
process.envflags were consolidated into a singleflags.ts, andvalidateShellEmbeddablemoved tobuilder-util/envUtil. - Source reorganization. Platform-specific files were split into per-platform subdirectories. The public API surface is unchanged.
- Test suite. Duplicated test files were replaced by runtime-generated tests that fan out across toolset version combinations.
@electron/*dependency major bumps.@electron/get3→5 (nowfetch-based; drives theelectronGetrename),@electron/osx-sign1→2 and@electron/universal2→3 (drive themac.sign/mac.universalpass-throughs), plus@electron/asar3→4,@electron/notarize2→3, and@electron/fuses1→2. The fuses bump adds an optionalwasmTrapHandlers?: booleanfield toelectronFuses; no existing fuse field was removed.