本文面向:维护同时提供 npm CLI 与 Electron 桌面端的开源项目、想搭建自动化发布流水线的开发者。
预计阅读时间:11 分钟
最终效果:掌握 SemVer 双版本号管理、一条命令发版脚本、npm Trusted Publisher 发布、Electron NSIS 打包,以及标签驱动的 GitHub Actions CI/CD。
一个开源项目从"能跑"到"能发布",中间隔着一整套发布工程。ChatCrystal 同时提供两种分发形态------npm CLI 工具和 Electron 桌面安装包,如何用一套脚本 + 一条 CI 流水线把两者管好,是本文要讲清楚的事。
语义化版本:先定规矩再发版
ChatCrystal 遵循 SemVer(语义化版本),格式为 MAJOR.MINOR.PATCH:
- MAJOR:不兼容的 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、推送。
三种发布模式:
bash
# 全量发布(npm + Electron),tag 格式:v0.4.11
npm run release # patch 递增
npm run release -- minor # minor 递增
npm run release -- major # major 递增
npm run release -- 0.5.0 # 指定版本号
# 仅 npm CLI,tag 格式:npm-v0.4.11
npm run release:npm
# 仅 Electron 桌面端,tag 格式:electron-v0.4.11
npm run release:electron
脚本的核心逻辑分四步:
第一步:前置检查。 脚本会用 git diff --quiet 确认工作区没有未提交的改动。如果有脏文件,直接报错退出。这个设计防止了"忘了提交某个文件就发版"的低级错误。
第二步:版本号计算。 根据参数(patch/minor/major/显式版本号)解析当前版本并计算新版本。如果是显式版本号(如 0.5.0),直接使用;否则按 SemVer 递增对应段位。
第三步:写入版本号并提交。 修改对应的 package.json,运行 npm install --package-lock-only 更新 lockfile,然后 git add + git 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/ 到 registry
ChatCrystal 的 npm 包名是 chatcrystal,安装后提供 crystal 命令。这个映射关系定义在 server/package.json 的 bin 字段中:
json
{
"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 Publisher(OIDC),配合 --provenance 参数。CI 运行时通过 id-token: write 权限向 npm 证明自己来自可信的 GitHub Actions 工作流。同时 release.yml 中仍保留了 NPM_TOKEN secret 作为 NODE_AUTH_TOKEN,确保向后兼容。
发布前会跑 lint 和 test,构建 server 产物,然后:
bash
npm version "0.4.11" --no-git-tag-version --allow-same-version
npm publish --provenance --access public
--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 平台使用 NSIS(Nullsoft Scriptable Install System)生成 .exe 安装器:
yaml
nsis:
oneClick: false # 不是一键安装,显示安装向导
allowToChangeInstallationDirectory: true # 允许用户选择安装路径
perMachine: false # 安装到当前用户目录
createDesktopShortcut: true # 创建桌面快捷方式
createStartMenuShortcut: true # 创建开始菜单快捷方式
oneClick: false 意味着用户会看到一个标准的安装向导,可以选择安装路径。这比静默安装对普通用户更友好。
构建命令:
bash
# 生成 NSIS 安装器(输出到 release/ 目录)
npm run build:electron
# 快速打包(仅生成解压目录,不做安装器,用于本地测试)
npm run pack:electron
build: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 标签触发,通过标签前缀决定执行路径。
触发条件:
yaml
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-publish environment(可以配置审批规则,比如只有 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(全量),实际操作是:
bash
# 1. 确保工作区干净
git status
# 2. 一行命令搞定
npm run 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 而不重新打包 Electron:
bash
npm run release:npm -- 0.4.11
# 只会触发 npm 发布,Electron 保持不变
设计决策复盘
双版本号。 把 npm 和 Electron 的版本号分开管理,代价是多了一层复杂性,收益是发布更灵活。对于一个同时提供 CLI 和桌面端的项目,这个权衡是值得的。
标签前缀驱动。 用 v*、npm-v*、electron-v* 三种标签前缀区分发布范围,比用环境变量或手动选择更安全------标签一旦推送不可篡改,CI 的行为完全可追溯。
Trusted Publisher。 npm 的 OIDC 发布机制消除了长期 token 管理的麻烦,CI 权限最小化(id-token: write + contents: read),安全审计友好。
Electron 缓存。 Electron 二进制和 builder 工具链的缓存是必须的。没有缓存的话,每次构建要多下载约 300 MB,在 GitHub Actions 的网络环境下会慢很多。
发布前校验。 release.mjs 强制要求工作区干净,CI 中强制跑 lint 和 test。两道关卡确保"脏代码不会溜进发布包"。
版本管理不是"写完代码之后才要想的事"。它是一套契约:和用户约定"这个版本号意味着什么",和 CI 系统约定"看到什么标签就做什么事",和自己约定"发布流程必须可重复、可审计"。ChatCrystal 的方案不算复杂,但覆盖了从 git commit 到用户双击 .exe 的完整链路。
项目地址:github.com/ZengLiangYi/ChatCrystal
如有疑问欢迎在 GitHub Issues 或私信交流,很乐意解答。