esbuild 发布运维手册:从 version.txt 到多平台 npm 包的完整发布链路解析
本文基于 esbuild 仓库中的 RUNBOOK.md 编写,完整还原该项目「基于 GitHub Actions 可信发布(Trusted Publishing)」的发布流程,以及新增平台包的标准维护步骤。读完本文,你将掌握 esbuild 一次发版背后的完整链路:版本号如何同步到 Go 源码与全部 package.json、make platform-all 如何为 25+ 个平台目标构建产物、发布工作流 publish.yml 每一步在做什么,以及 Deno 镜像包与官方下载脚本如何被同一版本变更级联更新。
一、发布机制总览
RUNBOOK 的开篇指出:由于 esbuild 现在使用 GitHub Actions 作为 npm 的 [trusted publisher](可信发布者),发布流程涉及许多联动部件(moving parts),因此专门用这份运维手册记录维护任务,避免操作被遗忘。
一次发版涉及三个仓库的级联反应:
- esbuild 主仓库:推送
version.txt变更触发本仓库的 publish.yml 工作流,完成 npm 发布、打 tag、创建 GitHub Release; - deno-esbuild 仓库(Deno 移植版):其定时运行的
release.yml工作流检测到版本变化后,克隆主仓库、执行make platform-deno、把新的deno目录内容提交回去并打 tag,再通过 Deno 的 webhook 通知 Deno 平台发布新版本到deno.land/x/esbuild;该工作流也可以手动触发以立即执行; - esbuild.github.io 仓库(官网静态站):其定时运行的
release.yml工作流检测到版本变化后,为新版本创建dl/v0.X.Y下载脚本、更新dl/latest脚本,并提交到gh-pages分支,由 GitHub Pages 部署到官网的下载入口。该工作流同样支持手动触发。
其中主仓库的 publish 工作流是核心,下文重点拆解。
二、发布一个版本的四个手动步骤
RUNBOOK 中「Publishing a release」一节规定了操作者必须手动完成的四步,缺一不可:
- 更新版本号:修改
version.txt,只写数字,不要加前导v。当前仓库中的实际内容为0.28.1,与 cmd/esbuild/version.go 中的const esbuildVersion = "0.28.1"以及 npm/esbuild/package.json 的"version": "0.28.1"三处一致,这正是后续自动化步骤要维护的不变量。 - 更新 CHANGELOG:把该版本号原样(不带
v)复制为CHANGELOG.md中的一个##标题,通常替换掉用于累积未发布变更的## Unreleased标题。 - 运行
make platform-all:把所有package.json文件中的版本号同步为新版本。如果不执行这一步,发布工作流会失败(原因见下文第五节的“拒绝未提交变更”检查)。 - 提交并推送:使用类似
publish 0.X.Y to npm的提交信息。该提交修改了version.txt,因此会触发下述发布工作流。
工作流触发条件:只监听 version.txt
从 publish.yml 的源码可以看到触发条件的精确定义:
on:
push:
branches:
- main
paths:
- version.txt
即只有向 main 分支推送、且 diff 中恰好包含 version.txt 时才会触发。这意味着发布是被「版本号变更」这一单一事实驱动的,操作者不需要(也不能)手动运行 workflow。工作流声明的权限也体现了可信发布的需求:id-token: write(用于向 npm 请求 OIDC 身份令牌)与 contents: write(用于推送 tag 和创建 Release)。
三、publish.yml 工作流的逐步拆解
对照 publish.yml 源码,一次自动发布按以下顺序执行,每一步都有明确的防御性设计:
3.1 读取版本信息并提前失败
工作流首先读取两个文件到环境变量(L21-L24):
go.version文件 →GO_VERSION(决定用哪个 Go 版本构建);version.txt→ESBUILD_VERSION(决定发布哪个版本)。
紧接着是「快速失败」步骤(L26-L30):
git fetch --tags
git tag "v$ESBUILD_VERSION"
如果该版本已经发布过,远端已存在同名 tag,git tag 会直接报错,工作流在烧掉大量构建时间之前就终止。这是 RUNBOOK 所说的「发布是自动触发」体系中对重复发版的防护。
3.2 从 CHANGELOG.md 提取 Release Notes
L32-L42 用一段 awk 脚本按版本号截取 changelog 段落:
CHANGELOG=$(awk -v "ver=$ESBUILD_VERSION" \
'/^## / { if (p) { exit }; if ($2 == ver) { p=1; next} } p' CHANGELOG.md)
逻辑是:遇到 ## 开头的行时,如果已经处于目标段落则结束(exit),如果标题后的第二段恰好等于当前版本号则开始收集(p=1)。随后 test -n "$CHANGELOG" 强制要求提取结果非空——这就是手动步骤 2 要求「必须把版本号写成 ## 标题」的硬性约束:找不到对应段落或段落为空,发布直接失败。这段文本最终会作为 GitHub Release 的 body 发布。
3.3 构建全部平台并拒绝脏工作区
构建步骤(L44-L57)按 go.version 安装 Go、按 Node 24 搭建环境,然后执行 make platform-all。
紧随其后的检查(L59-L63)是理解手动步骤 3 为何必填的关键:
git status --porcelain
test -z "$(git status --porcelain)"
make platform-all 的副作用是修改大量 package.json 文件(版本号、二进制哈希)。如果操作者在本地已运行过该命令并把结果一起提交,工作区就是干净的,检查通过;如果漏跑了第 3 步,CI 里运行 make platform-all 后工作区必然出现未提交的 diff,test 失败,发布中止。这正是 RUNBOOK 中「The publishing workflow will fail without this step」的底层机制。
3.4 固定 npm 版本并发布
L65-L72 全局安装特定版本的 npm(源码中固定为 npm@11.5.1,注释注明「Trusted publishing requires this specific version of npm」),然后执行 make publish-all。npm 在可信发布模式下无需 NPM_TOKEN,凭 GitHub Actions 签发的 OIDC 身份直接向 npm 换取发布凭证。
3.5 打 tag 并创建 GitHub Release
发布成功后才执行最后两步(L74-L88):
git push origin tag "v$ESBUILD_VERSION":把 3.1 步本地创建的v0.X.Ytag 推送到远端。RUNBOOK 指出的三个发布结果(npm 包、tag、含 Release Notes 的 GitHub Release)在此闭环;- 调用
actions/create-release@v1创建非草稿、非预发布的 Release,body即 3.2 步提取的 changelog 段落。
注意源码注释「Only do this after publishing was successful」:Release 的创建被刻意放在 npm publish 之后,保证用户在 GitHub Release 上看到版本号时,npm 上一定已经可以安装到它。
四、make platform-all 构建了什么
make platform-all 是发布链路中工作量最大的环节。在 Makefile 中,它聚合了如下目标:
platform-all: \
platform-android-arm \
platform-android-x64 \
platform-deno \
platform-neutral \
platform-openharmony-arm64 \
platform-wasi-preview1 \
platform-wasm
其中 platform-neutral(Makefile#L329-L358)覆盖 22 个原生二进制目标(aix、android-arm64、darwin、freebsd、netbsd、openbsd、openharmony 除外、sunos,以及 Linux 的 arm/arm64/ia32/loong64/mips64el/ppc64/riscv64/s390x/x64 与 Windows 的 ia32/x64/arm64)。每个平台目标本质上都调用同一个 platform-internal 模板(Makefile#L360-L369):
platform-internal:
@test -n "$(GOOS)" || (echo "The environment variable GOOS must be provided" && false)
@test -n "$(GOARCH)" || ...
@test -n "$(NPMDIR)" || ...
@test -n "$(BINPATH)" || ...
node scripts/esbuild.js "$(NPMDIR)/package.json" --version
$(GO_COMPILER) GOOS="$(GOOS)" GOARCH="$(GOARCH)" go build $(GO_FLAGS) -o "$(NPMDIR)/$(BINPATH)" ./cmd/esbuild
@shasum -a 256 "$(NPMDIR)/$(BINPATH)"
即:先更新该平台的 package.json 版本,再交叉编译 ./cmd/esbuild 到对应 npm 包目录下的二进制(Unix 系为 bin/esbuild,Windows 为 esbuild.exe,WASI 为 esbuild.wasm),最后输出 SHA-256 供人工核对。
4.1 版本号如何写入 Go 源码:version-go 目标
每个平台目标都依赖 version-go(Makefile#L316-L317),它执行 node scripts/esbuild.js --update-version-go。scripts/esbuild.js 中的实现是把 version.txt 的内容重写为 cmd/esbuild/version.go:
const updateVersionGo = () => {
const version_txt = fs.readFileSync(path.join(repoDir, 'version.txt'), 'utf8').trim()
const version_go = `package main\n\nconst esbuildVersion = "${version_txt}"\n`
// 先写临时文件再 rename,原子更新,避免构建中途被覆盖
const temp_path = version_go_path + Math.random().toString(36).slice(1)
fs.writeFileSync(temp_path, version_go)
fs.renameSync(temp_path, version_go_path)
}
值得注意的是 Makefile 中的注释:这一步曾经按文件 mtime 判断是否需要重建,但在「发布失败一次 → 回滚 version.go 变更 → 再次发布」的场景下会产生无效构建(version.go 的 mtime 更新但内容过期),所以现在无条件执行,同时只有内容变化时才真正写文件,避免无谓地使其依赖失效。
4.2 platform-neutral:生成主包并计算二进制哈希
platform-neutral 在所有原生目标构建完成后执行 buildNeutralLib(scripts/esbuild.js),用刚编出来的 esbuild 二进制自举生成主包 npm/esbuild 的发布产物:
lib/npm/node-install.ts→npm/esbuild/install.js(postinstall 安装脚本);lib/npm/node.ts→npm/esbuild/lib/main.js(JS API 入口,--define:ESBUILD_VERSION=注入版本号);lib/npm/node-shim.ts→npm/esbuild/bin/esbuild(CLI shim);- 直接拷贝
lib/shared/types.ts作为lib/main.d.ts类型定义; - 执行编译后的
node-platform.ts得到各平台的 npm 包名,把optionalDependencies统一指向当前版本; - 调用
generateBinaryHashes()(scripts/esbuild.js#L91-L120)对所有 22 个原生二进制的 SHA-256 摘要写入package.json的esbuild.binaryHashes字段——你在 npm/esbuild/package.json 中看到的 22 条哈希记录即来源于此,安装脚本借此校验下载到的平台包是否与发布时构建的比特级一致。Makefile 中的注释(L328)明确要求该哈希列表与platform-neutral的目标列表保持一致。
4.3 WASM 与 Deno 目标
platform-wasm(Makefile#L458-L463)构建 esbuild-wasm 包并校验 esbuild.wasm 的哈希;platform-deno(Makefile#L465-L468)则基于 WASM 构建产物执行 --deno 流程生成 deno/ 目录内容——后者正是 deno-esbuild 仓库定时工作流要同步的目标。platform-android-arm、platform-android-x64、platform-openharmony-arm64 这三个目标不产出原生二进制,而是复用 platform-wasm 产物作为这些平台上的 WASM 兜底方案(见 Makefile#L443-L456),这也解释了 npm/@esbuild/android-x64/package.json 一类包里放的是 esbuild.wasm。
五、make publish-all:发布顺序与发布后验证
publish-all 目标(Makefile#L470-L505)定义了两条纪律:
publish-all: check-go-version
# Make sure the npm directory is pristine (including .gitignored files)
rm -fr npm && git checkout npm
# 先发布所有平台相关包 ...
@$(MAKE) publish-aix-ppc64
...(共 21 个平台包,逐个 cd 进目录 npm publish)
# Publish platform-independent packages last to avoid race conditions
@$(MAKE) publish-neutral
@$(MAKE) publish-wasm
- 发布前先用
git checkout把npm/目录还原为提交状态,防止本地构建残留文件被打包进发布物; - 先发布 21 个
@esbuild/*平台包,最后才发布主包esbuild与esbuild-wasm。注释说明原因是避免竞态:如果主包先发布,它的optionalDependencies指向的平台包版本在 npm 上还不存在,用户此刻安装会解析失败;反过来则任何时刻安装主包,其声明的平台包都已可用。
每个平台包的发布目标(如 publish-linux-x64)都是一行 cd npm/@esbuild/linux-x64 && npm publish,依赖对应 platform-* 构建目标,保证发布物即刚才构建的产物。
发布完成后,仓库还提供了事后审计手段 validate-builds(Makefile#L594-L619):检出发布 tag,逐个平台重新构建,再从 npm 下载已发布的 .tgz,用 shasum + cmp 逐字节比对本地构建与线上包中的二进制,确认「published binaries are bitwise-identical to the locally-build binaries」。这对一个分发 20+ 个原生二进制、以安装脚本校验哈希为核心的项目是重要的供应链验证手段。
六、新增一个平台包的标准流程
RUNBOOK 的「Adding a new package」一节解释了动机:由于 esbuild 安装器的工作方式,每个「操作系统 + 架构」组合都需要一个独立的 optionalDependency npm 包;新包必须放在 @esbuild/ scope 下,以示官方身份。
具体操作分两部分:
第一部分:在仓库中接入新包。
在 npm/@esbuild 目录下为新平台创建包目录,然后修改仓库其余部分引用它。RUNBOOK 给出的实用技巧是:搜索一个同类已有包(如 linux-x64)的包名,看它在哪些位置被使用,逐处添加新包。从源码结构看,这些位置至少包括 Makefile 中的 platform-neutral 依赖列表与对应 platform-* / publish-* 目标、scripts/esbuild.js 的 generateBinaryHashes() 哈希列表(两处列表有注释明确要求保持同步),以及 lib/npm/node-platform.ts 中导出平台包名的表(buildNeutralLib 通过它生成 optionalDependencies)。
第二部分:为 npm 注册这个新包,准备下一轮发布。 RUNBOOK 列出了 6 个步骤:
- 创建预期名称的空包,版本号为
0.0.1; - 用
npm publish --access public发布(带 scope 的包默认是私有的,必须显式--access public); - 登录 npm 网站进入该包的设置页;
- 确认唯一 maintainer 是
esbuild用户; - 添加 GitHub 仓库作为可信发布者,三个字段分别为:
- Organization or user:
evanw - Repository:
esbuild - Workflow filename:
publish.yml
- Organization or user:
- 确认发布访问级别设为「Require two-factor authentication and disallow tokens (recommended)」。
第 5 步的 publish.yml 正是 本仓库 .github/workflows/publish.yml 文件,与上文 3.4 步 make publish-all 时 npm 校验的 OIDC 身份一一对应。完成以上配置后,下一次发布时只需把新包加入 publish-all 的依赖列表和哈希列表,即可纳入自动发布链路。
七、关键文件索引
| 文件 | 在发布链路中的角色 |
|---|---|
| version.txt | 版本号的唯一事实来源,修改并推送它即触发发布 |
| CHANGELOG.md | ## 0.X.Y 标题段落被自动提取为 GitHub Release Notes |
| go.version | 指定发布构建使用的 Go 版本 |
| .github/workflows/publish.yml | 发布工作流:校验 tag、提取 changelog、构建、可信发布、打 tag、建 Release |
| Makefile | platform-* 构建矩阵、publish-all 发布顺序、validate-builds 事后校验 |
| scripts/esbuild.js | 版本同步(--version / --update-version-go)、buildNeutralLib、二进制哈希生成 |
| cmd/esbuild/version.go | 由 version-go 目标自动重写的 Go 版本常量 |
| npm/@esbuild | 21 个平台包目录,新增平台包的落点 |
| npm/esbuild/package.json | 主包:optionalDependencies 与 esbuild.binaryHashes 由构建自动生成 |
理解这份 RUNBOOK 的核心收获是:esbuild 的发版不是「跑一个脚本」,而是一条由 version.txt 单点驱动、跨三个仓库级联、每一步都有前置校验(tag 冲突、changelog 非空、工作区干净、npm 版本固定)和事后验证(比特级比对)的自动化流水线;新增平台包则必须在构建矩阵、哈希列表、optionalDependencies 和 npm 可信发布者四处保持一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00