版本管理:npm 发布 + Electron 打包 + CI/CD
本文面向维护同时提供 npm CLI 与 Electron 桌面端的开源项目、想搭建自动化发布流水线的开发者。预计阅读时间11 分钟最终效果掌握 SemVer 双版本号管理、一条命令发版脚本、npm Trusted Publisher 发布、Electron NSIS 打包以及标签驱动的 GitHub Actions CI/CD。一个开源项目从能跑到能发布中间隔着一整套发布工程。ChatCrystal 同时提供两种分发形态——npm CLI 工具和 Electron 桌面安装包如何用一套脚本 一条 CI 流水线把两者管好是本文要讲清楚的事。语义化版本先定规矩再发版ChatCrystal 遵循 SemVer语义化版本格式为MAJOR.MINOR.PATCHMAJOR不兼容的 API 变更如删除 CLI 子命令、修改数据库表结构MINOR向后兼容的功能新增如新增数据源适配器PATCH向后兼容的问题修复如解析器边界处理当前版本号写在两个package.json中package.json根目录—— Electron 桌面端版本server/package.json—— npm 包版本两者的版本可以独立演进也可以统一发布。为什么分开因为 Electron 安装包和 npm CLI 工具的发布节奏不同。修了一个 CLI 的小 bug不需要重新打包整个桌面端。release.mjs一条命令完成发版scripts/release.mjs是整个发布流程的入口。它做的事情很简洁检查工作区是否干净、修改版本号、更新 lockfile、提交、打 tag、推送。三种发布模式# 全量发布npm Electrontag 格式v0.4.11npmrun release# patch 递增npmrun release -- minor# minor 递增npmrun release -- major# major 递增npmrun release --0.5.0# 指定版本号# 仅 npm CLItag 格式npm-v0.4.11npmrun release:npm# 仅 Electron 桌面端tag 格式electron-v0.4.11npmrun release:electron脚本的核心逻辑分四步第一步前置检查。脚本会用git diff --quiet确认工作区没有未提交的改动。如果有脏文件直接报错退出。这个设计防止了忘了提交某个文件就发版的低级错误。第二步版本号计算。根据参数patch/minor/major/显式版本号解析当前版本并计算新版本。如果是显式版本号如0.5.0直接使用否则按 SemVer 递增对应段位。第三步写入版本号并提交。修改对应的package.json运行npm install --package-lock-only更新 lockfile然后git addgit commit -m chore: release v0.4.11。第四步打 tag 并推送。用git tag -a创建附注标签然后git push origin main --follow-tags一次性推送提交和标签。标签格式是关键设计决策v*触发全量构建npm-v*仅触发 npm 发布electron-v*仅触发 Electron 构建。CI 通过匹配标签前缀来决定执行哪些 Job。npm 发布从 server/ 到 registryChatCrystal 的 npm 包名是chatcrystal安装后提供crystal命令。这个映射关系定义在server/package.json的bin字段中{name:chatcrystal,bin:{crystal:dist/server/src/cli/index.js},files:[dist/server/,dist/shared/,README.md]}files字段控制哪些文件会被发布到 npm。只有编译产物和共享类型会被打包源码、测试、配置文件全部排除。用户执行npm install -g chatcrystal后npm 会把crystal链接到全局 PATH这就是为什么终端里敲crystal就能用。发布过程使用 npm Trusted PublisherOIDC配合--provenance参数。CI 运行时通过id-token: write权限向 npm 证明自己来自可信的 GitHub Actions 工作流。同时 release.yml 中仍保留了NPM_TOKENsecret 作为NODE_AUTH_TOKEN确保向后兼容。发布前会跑 lint 和 test构建 server 产物然后npmversion0.4.11--no-git-tag-version --allow-same-versionnpmpublish--provenance--accesspublic--provenance参数会在 npm 上记录这次发布对应的 Git commit 和构建来源用户可以在 npm 页面看到这个包确实由 GitHub Actions 构建。Electron 打包从源码到 .exe 安装器Electron 构建比 npm 发布复杂得多——它需要把 Node.js 运行时、Chromium 浏览器引擎、应用代码全部打包成一个用户双击就能运行的安装器。electron-builder.yml是打包的核心配置。几个关键点文件过滤。files字段用排除法控制打包内容。node_modules里有大量不需要的东西——测试目录、文档、ESLint 配置、TypeScript 编译器、Vite、electron-builder 自身。通过!前缀排除这些安装包体积可以减少数百 MB。原生模块处理。sql.js需要一个 WASM 文件sql-wasm.wasm它不在 JS bundle 中需要通过extraResources单独拷贝到安装目录。同时asarUnpack确保 WASM 文件和原生.node模块不会被塞进 asar 归档asar 是 Electron 的打包格式类似 tar但原生模块需要在文件系统上可访问才能被dlopen。NSIS 安装器配置。Windows 平台使用 NSISNullsoft Scriptable Install System生成.exe安装器nsis:oneClick:false# 不是一键安装显示安装向导allowToChangeInstallationDirectory:true# 允许用户选择安装路径perMachine:false# 安装到当前用户目录createDesktopShortcut:true# 创建桌面快捷方式createStartMenuShortcut:true# 创建开始菜单快捷方式oneClick: false意味着用户会看到一个标准的安装向导可以选择安装路径。这比静默安装对普通用户更友好。构建命令# 生成 NSIS 安装器输出到 release/ 目录npmrun build:electron# 快速打包仅生成解压目录不做安装器用于本地测试npmrun pack:electronbuild:electron的完整流程是npm run build构建 server 和 client-tsc -p electron/tsconfig.json编译 Electron 主进程-electron-builder --win打包 NSIS 安装器。pack:electron最后一步换成--dir只生成解压后的文件夹跳过 NSIS 打包本地测试时快很多。GitHub Actions标签驱动的 CI/CD.github/workflows/release.yml是整个自动化发布的核心。它由 Git 标签触发通过标签前缀决定执行路径。触发条件on:push:tags:-v*# 全量发布-npm-v*# 仅 npm-electron-v*# 仅 Electron也支持workflow_dispatch手动触发可以在 GitHub Actions 页面勾选是否发布 npm和是否构建 Electron适合补发或调试。prepare Job解析标签前缀输出两个布尔值do_npm和do_electron下游 Job 根据这两个值决定是否执行。publish-npm Job运行在ubuntu-latest执行 lint、test、构建 server然后npm publish --provenance。使用npm-publishenvironment可以配置审批规则比如只有 Rayner 批准后才能发布。publish-electron Job运行在windows-latest因为要构建 Windows 安装器执行 lint、test、全量构建然后electron-builder --win。有两个缓存层——Electron 二进制约 90 MB和 electron-builder 工具链NSIS 等避免每次构建都重新下载。构建完成后从CHANGELOG.md中提取当前版本的变更日志用softprops/action-gh-release创建 GitHub Release把.exe安装器和更新日志一起上传。如果版本号包含-如0.5.0-beta.1会标记为 prerelease。完整发布流程走一遍假设要发布v0.4.11全量实际操作是# 1. 确保工作区干净gitstatus# 2. 一行命令搞定npmrun release --0.4.11# 脚本输出# npm: 0.4.10 → 0.4.11# electron: 0.4.10 → 0.4.11## Releasing v0.4.11## [main abc1234] chore: release v0.4.11# ✅ v0.4.11 pushed. GitHub Actions will build and publish.# 3. 去 GitHub Actions 页面看构建进度# npm publish 大约 2-3 分钟# Electron 构建大约 8-10 分钟首次慢有缓存后 3-4 分钟从敲命令到 npm 上出现新版本、GitHub Release 里出现安装器全程不需要手动操作。如果只想修一个 CLI bug 而不重新打包 Electronnpmrun release:npm --0.4.11# 只会触发 npm 发布Electron 保持不变设计决策复盘双版本号。把 npm 和 Electron 的版本号分开管理代价是多了一层复杂性收益是发布更灵活。对于一个同时提供 CLI 和桌面端的项目这个权衡是值得的。标签前缀驱动。用v*、npm-v*、electron-v*三种标签前缀区分发布范围比用环境变量或手动选择更安全——标签一旦推送不可篡改CI 的行为完全可追溯。Trusted Publisher。npm 的 OIDC 发布机制消除了长期 token 管理的麻烦CI 权限最小化id-token: writecontents: read安全审计友好。Electron 缓存。Electron 二进制和 builder 工具链的缓存是必须的。没有缓存的话每次构建要多下载约 300 MB在 GitHub Actions 的网络环境下会慢很多。发布前校验。release.mjs 强制要求工作区干净CI 中强制跑 lint 和 test。两道关卡确保脏代码不会溜进发布包。版本管理不是写完代码之后才要想的事。它是一套契约和用户约定这个版本号意味着什么和 CI 系统约定看到什么标签就做什么事和自己约定发布流程必须可重复、可审计。ChatCrystal 的方案不算复杂但覆盖了从git commit到用户双击.exe的完整链路。项目地址github.com/ZengLiangYi/ChatCrystal如有疑问欢迎在 GitHub Issues 或私信交流很乐意解答。