你发布了 Electron build,发布页面已上线,但咖啡还没冲好,第一张支持工单就来了。一位用户说应用根本没发现更新;另一位下载了却无法安装;第三位还在运行旧版二进制文件,导致认证流程中断,而你的 logs 几乎没提供任何有用信息。
这就是 Electron 应用自动更新的残酷现实。API 更新器只是其中一个组件。生产环境的发布还依赖于平台签名、传输策略、manifest、托管、生命周期事件、可观测性、发布控制以及回滚路径。任何一个环节如果被视为可选,一次常规补丁就可能演变成突发事故。
目录
- 促使我编写本指南的凌晨 2 点更新事故
- 选择正确的 Electron 更新方案
- 在主进程中实现自动更新流程
- 配置 CI/CD 以实现签名发布和清单管理
- 发布、渠道与回滚策略
- 将自动更新视为一种安全控制手段
- 生产环境更新指南与检查清单
引发本指南的凌晨 2 点更新事故
该版本已通过 CI,看起来一切正常。周五深夜,一名开发者推送了一个未签名的 Electron build,而发布任务上传了足够多的资源,让该版本看起来已发布完成。应用在测试环境中可以启动,但没人实际验证过从已安装的生产构建执行更新的路径。
凌晨 2:08,PagerDuty 向值班工程师发出了警报。部分设备上的新身份验证流程失败,收到更新的用户无法完成登录。其他用户则停留在旧版本,因为更新程序无法验证或安装该 artifact。部分客户拿到的是有问题的发布,而其余设备却运行着另一个版本,且没有明确解释。
调查过程分为五个检查步骤:
- 检查发布源。 二进制文件确实存在,但预期的元数据并未明确说明哪些客户端应该接收该版本。Manifest 是发布 pipeline 与已安装客户端之间的契约,而不是可选的上传细节。
- 检查签名。 发布版本未正确签名,导致受影响平台上的验证失败。签名缺失或无效时,必须阻止发布。
- 对比客户端 logs。 更新错误从未到达中央遥测系统。应用程序直接吞掉了该事件并继续运行,导致团队无法获得可靠的证据。
- 检查发布控制。 由于缺乏内部渠道或分阶段测试队列,所有符合条件的客户端都使用了相同的 Feed,导致故障在没有隔离点的情况下迅速扩散。
- 搜索回滚方案。 团队缺乏经过验证的流程来重新发布旧版本,也无法引导客户端避开损坏的发布版本。
Electron 官方文档明确说明了该平台的限制。 Linux 不支持内置的自动更新器,且 macOS 的更新请求必须满足 App Transport Security 要求文档还指出,签名是确保 macOS 更新可靠及进行版本验证的必要前提。 Electron autoUpdater 文档 定义了 API 约束,而发布系统必须执行相关的运维控制。
复盘教训: 一个无法解释发生了什么的更新程序,本质上就是一个缺乏遥测数据的远程安装尝试。
代价远不止工程时间。客户对桌面客户端失去了信心,支持团队不得不解释不一致的行为,而团队在接下来的工作日里忙于重建一套本应在事故发生前就存在的发布流程。
请将自动更新视为一种 运维系统。签名是发布关卡,manifest 定义了客户端契约,发布渠道(rollout channels)限制了风险范围,而回滚(rollback)应当是一条经过测试的路径,而非应急时的临时方案。
选择正确的 Electron 更新方案
凌晨 2 点时,错误的更新选择会演变成运维事故。原生二进制更新必须处理签名、manifest、安装程序和回滚。而仅针对渲染层的 JavaScript 或 CSS 变更则遵循不同的路径。托管限制也至关重要:一个托管在 GitHub 上的小型项目,不需要像企业级分发服务那样复杂的发布控制。
对于使用 electron-builder 并发布已签名 artifact 的项目, electron-updater is usually the practical default. Its ecosystem covers publish targets, release manifests, artifact downloads, and installation on the next launch. It supports several hosting models, but your team still owns signing, feed availability, channel policy, rollout controls, and monitoring. The 为 Capgo 集成 Electron updater 在评估 Web 层 bundle 与 Native Build 并行的混合交付模式时,这一点非常重要。
update-electron-app 适用于希望围绕 GitHub Releases 进行轻量化集成的团队。它会在启动时及后续定期检查更新,配置简单,但在高级 Channel 选择、分阶段发布和自定义回滚规则方面的灵活性较低。只要 GitHub Releases 及其可用性符合您的运维需求,该方案对于小型发布流程来说是合理的。
| 选项 | 托管控制 | 签名支持 | Channels 与分阶段发布 | 维护成本 |
|---|---|---|---|---|
| electron-updater | S3、GitHub、通用 HTTPS 及其他发布目标 | 与打包后的发布签名集成 | 基础扎实,自定义策略通常基于 Feed 实现 | 中等 |
| 更新 Electron 应用 | 主要是简单的 GitHub Release 工作流 | 使用底层的 Electron 签名模型 | 除非添加周边服务,否则功能有限 | 低 |
| Squirrel.Windows 或 Squirrel.Mac | 面向平台的发布流程 | 依赖平台的签名要求 | 可行,但通常需要额外的发布基础设施 | 对于旧版应用而言难度中等 |
| 自定义服务 | 完全控制 manifest、授权、分流 (cohorts) 和 feed | 自主掌控验证设计与密钥管理 | 极高的灵活性 | 高 |
| Capgo 实时更新 | Web 层 Bundle 的托管分发 | 使用其自带的更新与分发模型 | 受众定向与基于 Channel 的分发 | 将运维模式与原生二进制更新分离 |
当发布授权、租户定向、审计记录或受监管的部署规则足以证明实现成本合理时,使用 Hazel、Nuts 或内部 feed 等自定义服务是合适的。代价是需要持续承担维护责任。你的团队必须定义 manifest 语义、保护签名密钥、保持客户端兼容性,并测试下载失败、发布被拒和回滚行为。
Live Updates 可以仅发布渲染层变更,无需重新构建原生 Shell。但当 Electron、原生模块、权限或安装程序行为发生变化时,它们无法替代二进制更新。 除非需要自定义灰度发布逻辑,否则请使用 electron-updater。 如果必须使用自定义逻辑,请围绕既有的 manifest 和 artifact 约定进行构建,而不是重新实现下载和差分更新行为。可靠的方案,是团队在压力下仍能观测、分阶段发布并回滚的方案。
在主进程中实现自动更新流程
主进程应负责更新检查和安装。渲染进程可以显示状态,但不应决定更新包是否可信,也不应决定应用程序何时退出。
首先配置发布目标
一个最简化的 electron-builder 配置如下:
{
"build": {
"appId": "com.example.desktop",
"publish": [
{
"provider": "s3",
"bucket": "example-electron-releases",
"channel": "stable"
}
],
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true
}
}
}
将 beta 和 stable 渠道分开。Channel 是一种发布策略,而非 UI 上的标签。每个 channel 都应指向正确的已签名 artifact 和 manifest。
安排检查计划并暴露生命周期事件
仅在 checkForUpdates() 启动时调用是常见的生产环境错误。用户可能会连续几天不关闭应用,因此主进程需要受控的检查间隔,以及一套适配离线状态的重试策略。
const { app, BrowserWindow, ipcMain } = require('electron');
const { autoUpdater } = require('electron-updater');
let mainWindow;
let isQuitting = false;
let retryDelay = 60 * 1000;
function sendUpdateStatus(status, payload = {}) {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('update-status', { status, ...payload });
}
}
function scheduleUpdateCheck() {
setTimeout(async () => {
try {
await autoUpdater.checkForUpdates();
retryDelay = 60 * 1000;
} catch (error) {
sendUpdateStatus('error', { message: error.message });
retryDelay = Math.min(retryDelay * 2, 30 * 60 * 1000);
}
scheduleUpdateCheck();
}, retryDelay);
}
app.whenReady().then(() => {
mainWindow = new BrowserWindow({
webPreferences: {
preload: require('path').join(__dirname, 'preload.js')
}
});
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater.on('checking-for-update', () => {
sendUpdateStatus('checking');
});
autoUpdater.on('update-available', info => {
sendUpdateStatus('available', { version: info.version });
});
autoUpdater.on('download-progress', progress => {
sendUpdateStatus('progress', { percent: progress.percent });
});
autoUpdater.on('update-downloaded', info => {
sendUpdateStatus('downloaded', { version: info.version });
});
autoUpdater.on('error', error => {
sendUpdateStatus('error', { message: error.message });
});
autoUpdater.checkForUpdates().catch(error => {
sendUpdateStatus('error', { message: error.message });
});
scheduleUpdateCheck();
});
ipcMain.handle('install-update', () => {
isQuitting = true;
autoUpdater.quitAndInstall(false, true);
});
app.on('before-quit', event => {
if (!isQuitting) {
return;
}
});
具体的更新事件行为因平台和打包设置而异,因此请通过安装后的 artifact 进行测试,而非在开发模式下。Electron 文档也提到了 Windows 上的启动时机问题,包括 Squirrel 的首次运行场景。请务必在应用完成平台特定的初始化后再触发更新检查。
在不阻塞工作的前提下通知渲染进程
preload bridge 应暴露一个精简的 API:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('updates', {
onStatus(callback) {
ipcRenderer.on('update-status', (_event, status) => callback(status));
},
install() {
return ipcRenderer.invoke('install-update');
}
});
渲染进程端的进度条可以保持极简设计:
window.updates.onStatus(status => {
const progress = document.querySelector('#update-progress');
const message = document.querySelector('#update-message');
if (status.status === 'progress') {
progress.hidden = false;
progress.value = status.percent;
message.textContent = `Downloading update, ${Math.round(status.percent)}%`;
}
if (status.status === 'downloaded') {
message.textContent = `Version ${status.version} is ready to install`;
}
if (status.status === 'error') {
message.textContent = 'The update could not be downloaded. We will retry later.';
}
});
除非应用有必须立即重启的充分理由,否则在生产环境中应确保安装过程需经用户同意。请在 isQuitting 之前设置 quitAndInstall()标志,否则常规的窗口关闭处理器可能会阻止安装程序接管。

有两种失败情况需要进行显式测试。首先,正在运行的客户端必须按计划调用 checkForUpdates() 而不仅仅是在启动时。其次, error 事件必须记录到 logs 和 telemetry 中。如果应用在未报告的情况下吞掉了该事件,你的 应用故障排查流程 将从凭据推测变成盲目猜测。
为签名发布和 Manifest 配置 CI/CD
发布 pipeline 是用户安装内容的唯一事实来源。仅在某位开发人员机器上运行正常的本地 build,并不能证明发布的 binary、manifest、signature 和 channel 描述的是同一个 release。
electron-builder 的发布模型要求 release metadata 和 update target 必须同步传输。对于许多配置而言,这意味着需要一个类似 latest.yml 适用于 Windows 以及 latest-mac.yml 适用于 macOS,并包含特定平台的软件包和 blockmap 文件。如果缺少 manifest,即使二进制文件完全有效,客户端也无法识别。
在 CI 中明确签名流程
简化的 GitHub Actions 模式如下:
name: release
on:
push:
tags:
- "v*"
jobs:
build:
strategy:
matrix:
os: [macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run test
- run: npm run build
- name: Build and publish
shell: bash
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: npx electron-builder --publish always
使用特定平台的 secrets,并将签名材料保存在仓库之外。签名失败应当停止任务,而不是生成一个未签名的备选版本供人手动上传。
| 变量 | 用途 |
|---|---|
CSC_LINK |
macOS 证书或证书引用 |
CSC_KEY_PASSWORD |
macOS 签名材料的密码 |
WIN_CSC_LINK |
Windows 证书或证书引用 |
AWS_ACCESS_KEY_ID |
使用权限范围极小的发布凭据 |
AWS_SECRET_ACCESS_KEY |
与发布凭据配对的 Secret |
发布配置应保持 provider 和 channel 的一致性:
{
"build": {
"publish": {
"provider": "s3",
"bucket": "example-electron-releases",
"channel": "stable",
"publishAutoUpdate": true,
"updaterCacheDirName": "example-desktop-updater"
}
}
}
发布前,需根据 package 版本、tag、commit SHA 及 smoke-test 结果对 job 进行拦截。发布后,需验证 feed 是否包含预期的 manifest,且该 manifest 指向的是该 job 生成的准确 artifact。这套 持续集成设置指南 在你将这些检查流程化时非常有用,同时,正在对比 pipeline 编排方案的团队也能从中获益,了解 何时将 Jenkins 与 Ansible 结合使用.
那些常导致发布不完整的命令,其设计初衷就是为了避免出错:
npx electron-builder --publish never
test -f dist/latest.yml
test -f dist/latest-mac.yml
find dist -name "*.blockmap" -print
这些检查并不能替代签名安装测试。但它们可以捕捉到由于上传 binary 时缺失 client 发现所需的 metadata 而导致的运维失误。
发布、Channels 与回滚策略
Release feed 的行为应更像是一个 deployment target,而非简单的下载文件夹。请保持 内部, 测试版,以及 最新 渠道应当相互隔离,每个渠道都由其独立的 manifest 和签名 artifact 集支持。发布流程应该是将经过测试的版本在不同策略间进行晋升,而不是在客户端下载时直接覆盖文件。
渠道隔离还能防止测试构建误入生产环境。更新程序在评估 feed 之前,应当明确客户端属于内部测试组、Beta 用户还是稳定版用户。
在大规模推送前先使用 Cohort 进行灰度测试
可以通过自定义 manifest 字段来实现分阶段发布:
version: 4.8.0
path: Example-Setup-4.8.0.exe
sha512: signed-artifact-hash
rolloutPercentage: 10
主进程可以为每个用户分配一个稳定的 bucket,然后将其与 rolloutPercentage稳定的分配至关重要。如果用户在每次检查时都在“符合条件”和“不符合条件”的状态间切换,会导致行为不可预测,并增加排查支持问题的难度。
只有在版本度过观察期后,才扩大 Cohort 范围。观察期的长短应反映您的实际使用模式,但决策应基于各项指标而非仅仅依赖时间。需追踪更新检查结果、下载完成情况、启动健康度、崩溃率、渲染器异常以及身份验证成功率。
| 信号 | 操作 | 原因 |
|---|---|---|
| Feed 或签名错误超过了团队设定的阈值 | 暂停发布 | 客户端可能无法验证或发现该发布版本 |
| 更新后启动检查失败 | 回滚 Feed | 二进制文件可能安装成功,但启动时失败 |
| 扩大发布后渲染器异常增加 | 保持在当前用户群组 | 原生安装程序可能正常,但新的应用 code 却异常 |
| 信号仍在发布预算内 | 扩大用户群组 | 证据支持扩大覆盖范围 |
不要将回滚与删除构建产物混为一谈。现有客户端可能缓存了元数据,部分客户端甚至可能已在运行有问题的版本。回滚方案需要有一个之前已签名的发布版本、一次 Feed 变更,以及客户端具备可恢复的行为。
运维规则: 在发生故障时,值班工程师必须能够执行回滚,而无需重新构建应用程序。
在实践中,runbook 应将之前的版本清单重新发布到受影响的 channel,使 staging 标记失效,并确认新的检查已指向安全版本。如果问题出在 renderer code 而非 native shell,针对 Web 层的回滚可能会更快。像这样的平台: Capgo 分阶段发布 对于该独立的交付层可能非常有用,但不应模糊 native binary 回滚与 web-bundle 回滚之间的界限。
将自动更新视为安全控制手段
Electron updater 会下载可执行的 code,并且几乎无需用户交互即可完成安装。这使得更新路径成为了一个 安全边界,而不仅仅是一个便捷功能。Electron 官方文档描述了 macOS ATS 等平台限制,且安全研究已记录了 2022 年的一个案例:攻击者通过控制更新基础设施,可以分发仍能通过 code 签名检查的恶意包,正如 Electron Builder 自动更新安全文档.
Code 签名仍然是基础,但它并非信任模型的全部。请为每个版本签名,在 CI 过程中验证证书和身份,并维护一套文档化的密钥轮换流程。在 macOS 上,请将签名与公证(notarization)以及适用于您应用的 hardened runtime 结合使用。在 Windows 上,确保证书的所有权、续期和构建权限是可审计的。由于 Electron 在 Linux 上不提供内置的通用更新程序,因此需要针对特定发行版制定策略。
像保护二进制文件一样保护元数据
如果元数据通道被攻破或配置错误,即使是已签名的二进制文件也可能与错误的发布版本关联。建议添加一个清单签名(manifest signature),并使用嵌入在应用中的公钥进行验证;同时强制执行最低允许版本,除非有明确授权的恢复路径,否则拒绝任何非预期的降级操作。
Feed 流也需要生产级控制:
- 限制发布权限: 仅授予 CI 发布 release assets 所需的权限。
- 保护签名密钥: 将证书和私钥保存在托管的密钥存储中,而不是代码仓库文件中。
- 锁定依赖项: 在 CI 中锁定 Electron、electron-builder 以及传递依赖。
- 审查构建产物: 扫描生成的 package,并将其与预期的 commit 和版本进行比对。
- 要求安全传输: 更新请求须遵循 ATS 和严格的 HTTPS 要求。
- 监控验证失败: 将重复的签名或清单验证失败视为安全事件,而非普通的网络波动。
Electron 完善的工具生态系统在不断扩展打包和更新器的覆盖范围,但维护工作并不能消除进行威胁建模的需求。实际目标是确保即使攻击者攻破了存储桶、CDN 或构建步骤,也无法让客户端接受未经授权的发布版本。上述 签名验证指南 为设计额外的验证层提供了有用的参考。

生产更新 Runbook 与检查清单
只有当另一名工程师能在压力下操作时,发布才算准备就绪。请将检查清单置于部署任务和事件频道附近。
预发布门禁
- 版本标识: 确认包版本、发布标签、commit SHA 及更新日志一致。
- 签名: 验证所有平台的构建产物均已签名,且已完成公证或同等验证。
- 清单一致性: 确认
latest.yml,latest-mac.yml,哈希值、路径及 blockmaps 与上传的产物匹配。 - 渠道安全: 在发布到正式渠道前,先发布到内部或 beta 渠道。
- 遥测数据: 确认更新检查、下载进度、安装完成、启动状态及错误日志均正常上报。
金丝雀发布与全量发布
- 分批控制: 首先从规模较小的内部测试或 Beta 用户群开始。
- 健康预算: 如果发布失败、更新下载失败、渲染器异常或身份验证失败率超过团队设定的阈值,请暂停推广。
- 推广审批: 在将版本推送到稳定版 Feed 之前,必须经过明确的“通过”或“拒绝”决策。
- 客户影响: 在进行大规模分发之前就准备好支持信息,而不是在发生第一起事故后才开始。
事件响应
如果新二进制文件无法启动、进程生成失败或更新下载无法完成,请立即停止推广。恢复之前的签名 manifest,使 staging 标记失效,并验证新客户端是否已解析到旧版本。在宣布结束前,通过遥测数据确认集群正在恢复。
具体的 rollback 命令取决于你的服务商,但操作流程应始终保持文档化: 将 channel manifest 切回上一个版本,使 staging 标签失效,如果需要恢复则推送强制更新标记,并通过实时遥测验证降级路径。未经测试的 rollback 仅仅是一种奢望。

Capgo 提供 Electron updater,无需为每次渲染层变更重新构建 Native Build,即可实现已签名 Web 层变更的交付、定向频道管理、发布控制以及更新可观测性。如果您希望将原生二进制文件发布与受控的 JavaScript 和 CSS 交付分离,请访问 Capgo 并结合您现有的 Electron 发布 pipeline 进行评估。