跳到主要内容

应用更新与自动发布集成总结

背景​

本次改造的目标是为 Keep Notes 增加应用内更新能力,并通过 GitHub Actions 自动完成打包、发布和安装包上传,避免每次发布都依赖手动打包操作。

最终产出包括:

  • 设置弹窗新增 关于 tab。
  • 关于 tab 展示当前版本号、检查更新入口、仓库地址和作者信息。
  • 支持通过 GitHub Release 检查更新。
  • 支持更新包下载进度展示。
  • 支持下载过程中通过图标按钮取消更新。
  • GitHub Actions 在 master 更新时自动构建 macOS、Windows、Linux 安装包。
  • 自动创建版本 tag 和 GitHub Release,并上传安装包及更新元数据。

应用更新集成​

应用更新能力基于 electron-updater 实现,核心代码位于:

  • src/main/updater.ts
  • src/main/ipc/updater.ipc.ts
  • src/preload/api/app.api.ts
  • src/shared/constants.ts
  • src/shared/types.ts
  • src/renderer/src/features/settings/components/settings-modal.tsx

主进程更新控制器​

AppUpdateController 负责封装更新相关能力,包括:

  • 获取应用信息:版本号、仓库地址、作者。
  • 检查 GitHub Release 是否存在新版本。
  • 控制更新下载。
  • 监听下载进度。
  • 取消更新下载。
  • 更新下载完成后触发安装。
  • 通过 IPC 将更新状态同步给渲染进程。

更新状态统一抽象为 AppUpdateState,渲染层只消费状态,不直接接触 Electron 或 Node 能力,符合当前项目中主进程负责系统能力、preload 暴露安全 API 的架构原则。

更新流程​

当前更新流程如下:

  1. 用户打开设置弹窗,进入 关于 tab。
  2. 页面通过 preload API 获取应用信息和更新状态。
  3. 用户点击 检查更新。
  4. 渲染进程通过 IPC 调用主进程的 checkForUpdates。
  5. 主进程通过 electron-updater 查询 GitHub Release。
  6. 如果发现新版本,自动开始下载。
  7. 下载期间持续接收 download-progress 事件并同步给页面。
  8. 页面显示下载进度。
  9. 下载中用户可以点击关闭图标取消下载。
  10. 下载完成后,页面显示安装入口。
  11. 用户确认安装后,主进程调用 quitAndInstall 重启并安装更新。

开发环境限制​

开发环境下不直接执行自动更新。原因是 electron-updater 的真实更新流程依赖打包后的应用信息、发布配置和安装包元数据。开发模式运行的 Electron 应用没有完整安装包上下文,不能代表真实更新环境。

因此当前实现中:

  • app.isPackaged === false 时,不进行真实更新检查。
  • 页面提示用户使用打包后的安装版检查 GitHub 更新。
  • 真实更新能力以 GitHub Actions 生成的 Release 安装包为准。

技术方案​

方案总览​

本次更新能力采用 Electron 主进程集中控制、渲染进程状态展示、GitHub Release 作为发布源、GitHub Actions 自动构建发布的方案。

整体链路可以拆成四层:

  1. 应用层:设置弹窗提供 关于 tab,负责展示版本信息、更新状态、下载进度和操作按钮。
  2. 通信层:preload 暴露受控 API,renderer 通过 IPC 调用主进程能力。
  3. 更新层:主进程封装 electron-updater,负责检查、下载、取消和安装更新。
  4. 发布层:GitHub Actions 调用 electron-builder 生成安装包,再上传到 GitHub Release,供客户端自动更新使用。

这个方案的核心取舍是:更新相关能力全部留在主进程,页面只做状态驱动的 UI 渲染。这样既能满足 Electron 自动更新的权限要求,也避免把 Node/Electron 原生能力直接暴露到渲染进程。

相关库与作用​

本次改造主要涉及以下技术和库:

技术/库使用位置作用
Electron主进程、preload、渲染进程桌面应用运行环境,提供应用生命周期、窗口、IPC、外链打开等能力
electron-updatersrc/main/updater.ts检查 GitHub Release、下载更新包、监听下载进度、安装更新
builder-util-runtimesrc/main/updater.ts提供 CancellationToken,用于取消更新下载
electron-builderelectron-builder.yml、GitHub Actions打包 Windows/macOS/Linux 安装包,生成自动更新需要的 yml 和 blockmap 元数据
electron-vitepnpm build构建 Electron 主进程、preload 和 renderer 产物
React设置弹窗 UI渲染 关于 tab、更新按钮、进度展示和取消按钮
TypeScript全项目定义更新状态、应用信息、IPC API 类型,减少主进程与渲染进程之间的数据结构歧义
lucide-react设置弹窗 UI提供检查更新、外链跳转、取消下载等图标
Radix UI设置弹窗基础组件承载设置弹窗、tab、按钮等交互基础能力
GitHub Actions.github/workflows/release.yml在推送到 master 后自动构建、创建 Release、上传安装包
softprops/action-gh-release.github/workflows/release.yml将各平台安装包和更新元数据上传到指定 GitHub Release
actions/upload-artifact.github/workflows/release.yml保存 CI 构建产物,便于排查和下载
pnpm本地和 CI依赖安装、脚本执行、锁定依赖版本
Vitest单元测试验证更新控制器和设置弹窗更新交互

关键技术选择​

1. 使用 electron-updater 而不是手写更新逻辑​

自动更新涉及平台差异、安装包校验、元数据解析、增量下载和安装触发。手写这部分逻辑成本高,也容易遗漏平台细节。

选择 electron-updater 的原因:

  • 与 electron-builder 的发布产物天然配套。
  • 支持 GitHub Release provider。
  • 自动识别 latest.yml、latest-mac.yml、latest-linux.yml。
  • 内置下载进度事件。
  • 支持更新下载完成后的 quitAndInstall。

2. 使用 electron-builder 统一打包和生成更新元数据​

自动更新不只是上传安装包,还需要上传平台对应的更新元数据。electron-builder 可以在打包时同步生成这些文件,避免手动维护。

关键产物包括:

  • Windows:.exe、.exe.blockmap、latest.yml
  • macOS:.dmg、.zip、.blockmap、latest-mac.yml
  • Linux:.AppImage、.deb、latest-linux.yml

3. 使用 GitHub Release 作为更新源​

当前项目已经托管在 GitHub,使用 GitHub Release 可以减少额外服务依赖。

优点:

  • 不需要自建更新服务器。
  • Release 资产可以直接作为更新包分发源。
  • GITHUB_TOKEN 可以在 Actions 内直接创建 Release 和上传资产。
  • 版本、tag、安装包可以统一管理。

限制:

  • 国内网络访问 GitHub 可能不稳定。
  • 大文件下载速度取决于用户网络环境。
  • 私有仓库场景需要额外处理访问权限。

4. 使用 GitHub Actions matrix 做跨平台构建​

Electron 应用的安装包通常需要在对应平台构建。workflow 使用 matrix 分别运行在 macOS、Windows、Linux runner 上,减少跨平台构建问题。

这样可以保证:

  • macOS 产物由 macOS runner 构建。
  • Windows NSIS 安装包由 Windows runner 构建。
  • Linux AppImage/deb 由 Ubuntu runner 构建。

5. 使用版本号驱动发布​

Actions 通过 package.json 的 version 生成 Git tag 和 Release 名称,例如 2.0.3 对应 v2.0.3。

这个策略让版本来源保持单一:

  • 应用内显示版本来自 app.getVersion()。
  • 打包产物版本来自 package.json。
  • GitHub Release tag 来自 package.json。
  • 自动更新比较版本时也基于发布元数据中的版本。

发布源​

发布源选择 GitHub Release,配置在 electron-builder.yml:

yaml
publish:
provider: github
owner: qiuweikangdev
repo: keep-notes

electron-updater 会基于该配置查询 Release 中的更新元数据,例如:

  • latest.yml
  • latest-mac.yml
  • latest-linux.yml
  • .blockmap
  • 平台安装包

打包配置​

electron-builder.yml 负责定义各平台产物:

  • Windows:NSIS 安装包,产物形如 keep-notes-2.0.3-setup.exe。
  • macOS:DMG 和 zip,产物形如 keep-notes-2.0.3.dmg、keep-notes-2.0.3-arm64-mac.zip。
  • Linux:AppImage 和 deb,产物形如 keep-notes-2.0.3.AppImage、keep-notes_2.0.3_amd64.deb。

其中 zip 和 yml 文件对 macOS 自动更新尤其重要,Windows 自动更新主要依赖 latest.yml 和 NSIS 安装包。

UI 设计​

关于 tab 采用较轻量的设置项布局:

  • 版本号显示在检查更新按钮旁边。
  • 检查更新按钮去掉默认蓝色背景,仅保留悬浮态反馈。
  • 下载中展示进度。
  • 取消更新使用图标按钮表达,避免占用过多空间。
  • 仓库地址通过外链图标跳转。
  • 作者信息直接展示。

这样可以让 关于 页面更接近设置页的信息密度,而不是营销式卡片布局。

GitHub Actions 集成​

工作流文件位于:

  • .github/workflows/release.yml

触发方式​

当前支持两种触发方式:

  • 推送到 master 分支。
  • 手动触发 workflow_dispatch。
yaml
on:
push:
branches:
- master
workflow_dispatch:

发布准备阶段​

prepare-release job 负责:

  1. 读取 package.json 中的版本号。
  2. 生成 tag 名称,例如 v2.0.3。
  3. 检查远端是否已有同名 tag。
  4. 如果 tag 不存在,则创建并推送 tag。
  5. 如果 tag 已存在且指向当前 commit,则复用。
  6. 如果 tag 已存在但指向其他 commit,则终止发布,要求先升级版本号。
  7. 创建或更新 GitHub Release。

这个设计避免了同一个版本号被不同 commit 重复发布,减少自动更新版本混乱的风险。

构建发布阶段​

build-and-publish job 使用 matrix 分别在三个系统构建:

  • macos-latest 构建 macOS 安装包。
  • windows-latest 构建 Windows 安装包。
  • ubuntu-latest 构建 Linux 安装包。

每个平台执行:

  1. Checkout 代码。
  2. 安装 pnpm。
  3. 安装 Node.js 20。
  4. 安装依赖。
  5. 执行 pnpm build。
  6. 执行 electron-builder install-app-deps。
  7. 执行 electron-builder 打包。
  8. 通过 softprops/action-gh-release 上传安装包和更新元数据。
  9. 通过 actions/upload-artifact 保存 workflow artifacts。

依赖安装策略​

Actions 中使用:

bash
pnpm install --frozen-lockfile --ignore-scripts --registry=https://registry.npmjs.org/

原因:

  • --frozen-lockfile 保证 CI 使用锁文件中的确定依赖版本。
  • --ignore-scripts 避免安装阶段提前执行 postinstall,减少平台依赖安装时机的不确定性。
  • 后续显式执行 pnpm exec electron-builder install-app-deps,让 Electron 原生依赖安装步骤更清晰。
  • --registry=https://registry.npmjs.org/ 避免历史 lockfile 中的镜像源证书问题影响 CI。

相关技术难点​

1. Electron 主进程与渲染进程边界​

自动更新涉及文件下载、应用重启、外链跳转等系统能力,这些能力不应该直接暴露给渲染进程。

解决方式:

  • 主进程封装 AppUpdateController。
  • preload 只暴露有限的安全 API。
  • 渲染进程只通过 IPC 调用更新动作和订阅状态。

这样既满足功能需求,也保持了项目已有的安全边界。

2. 更新状态同步​

更新过程不是一次性请求,而是一个持续状态流,包括:

  • 空闲
  • 检查中
  • 有可用更新
  • 下载中
  • 已下载
  • 已取消
  • 无更新
  • 失败

解决方式:

  • 主进程维护统一状态。
  • 通过事件监听 electron-updater。
  • 状态变化后广播到所有窗口。
  • 渲染进程根据状态渲染按钮、进度和提示文案。

3. 下载取消​

electron-updater 的下载取消需要使用 CancellationToken。本次实现中,每次下载开始时创建新的 token,并在用户点击取消图标时调用 cancel()。

需要注意:

  • 取消后要清理当前 token。
  • 取消状态不能被后续 error 事件覆盖成失败状态。
  • 取消后页面需要恢复到可再次检查更新的状态。

4. 自动发布版本一致性​

自动更新非常依赖版本号、tag 和 Release 的一致性。如果同一个版本号指向多个 commit,客户端可能拿到错误的更新包。

解决方式:

  • Actions 读取 package.json 版本号生成 tag。
  • tag 已存在但不指向当前 commit 时直接失败。
  • 每次发布新功能前需要先升级 package.json 版本号。

5. 跨平台构建产物​

不同系统的自动更新依赖不同产物:

  • Windows 需要 NSIS 安装包和 latest.yml。
  • macOS 通常需要 zip、DMG、latest-mac.yml 和 blockmap。
  • Linux 需要 AppImage/deb 以及 latest-linux.yml。

因此 workflow 不能只上传安装包,还必须上传 yml 和 blockmap 元数据。

遇到的问题与处理​

问题 1:主进程启动时报 autoUpdater named export 错误​

现象:

text
SyntaxError: Named export 'autoUpdater' not found.
The requested module 'electron-updater' is a CommonJS module

原因:

electron-updater 是 CommonJS 模块,不能在当前 ESM 构建方式下直接使用 named import。

处理方式:

改为默认导入后解构:

ts
import electronUpdater from "electron-updater";

const { autoUpdater } = electronUpdater;

问题 2:开发环境无法真实检查更新​

现象:

开发模式中点击检查更新时无法执行真实自动更新。

原因:

自动更新依赖打包后的安装应用、发布配置和 Release 元数据。开发模式缺少真实安装包上下文。

处理方式:

在 app.isPackaged === false 时返回明确提示:

text
开发模式仅预览更新界面;请使用安装版检查 GitHub 更新。

问题 3:Release 创建成功但没有安装包​

现象:

早期 workflow 创建了 tag 和 release,但安装包没有成功上传。

原因:

发布流程中需要明确区分:

  • 创建 GitHub Release。
  • 各平台构建安装包。
  • 将安装包和更新元数据上传到对应 Release。

处理方式:

将工作流拆成 prepare-release 和 build-and-publish 两个阶段,并使用 softprops/action-gh-release 上传 dist 下的安装包和更新元数据。

问题 4:GitHub Actions 安装依赖失败​

现象:

Actions 在 pnpm install --frozen-lockfile 阶段失败,日志中出现:

text
CERT_HAS_EXPIRED

失败地址来自:

text
https://r2.cnpmjs.org/...

原因:

pnpm-lock.yaml 中存在历史镜像源 tarball 地址,GitHub Actions 下载依赖时遇到镜像源证书过期。

处理方式:

  • 将 lockfile 中的 tarball 地址切换为 https://registry.npmjs.org/。
  • 在 workflow 中显式指定 npm 官方 registry。

最终 Actions 成功完成,v2.0.3 Release 中已上传安装包和自动更新元数据。

当前验证结果​

本次最终验证结果:

v2.0.3 Release 中包含关键产物:

  • keep-notes-2.0.3-setup.exe
  • keep-notes-2.0.3-setup.exe.blockmap
  • keep-notes-2.0.3.dmg
  • keep-notes-2.0.3.dmg.blockmap
  • keep-notes-2.0.3-arm64-mac.zip
  • keep-notes-2.0.3-arm64-mac.zip.blockmap
  • keep-notes-2.0.3.AppImage
  • keep-notes_2.0.3_amd64.deb
  • latest.yml
  • latest-mac.yml
  • latest-linux.yml

后续维护建议​

  1. 每次需要发布新版本时,先升级 package.json 中的版本号。
  2. 合并或推送到 master 后,GitHub Actions 会自动创建对应 tag 和 Release。
  3. 如果 Actions 提示 tag 已存在且指向其他 commit,说明当前版本号已经发布过,需要继续升级版本号。
  4. 可以后续清理早期失败发布产生的 v2.0.0、v2.0.1、v2.0.2 tag 和 Release。
  5. 如果需要正式面向 macOS 用户分发,后续应补充 Apple Developer 签名和 notarization。
  6. 如果需要减少 Release 中的辅助文件,可以进一步收窄 workflow 的上传 glob。