首页
/ Socket.IO 发版流程详解:从 RELEASING 文档看 socket.io 的版本发布、npm Trusted Publishing 与客户端 Bundle 同步

Socket.IO 发版流程详解:从 RELEASING 文档看 socket.io 的版本发布、npm Trusted Publishing 与客户端 Bundle 同步

2026-09-04 23:25:53作者:彭桢灵Jeremy

本文围绕 packages/socket.io/RELEASING.md 中定义的 6 步发版流程展开,结合 monorepo 的 workspaces 结构、client-dist/ 静态资源的服务机制以及 .github/workflows/publish.yml 中的 CI 配置,完整解析 socket.io 是如何通过 Git tag 触发 GitHub Actions,并借助 npm trusted publishing 安全发布到 npm 的。读完后你将掌握一套可复用的 monorepo 版本发布方案:手动同步版本号与 changelog、用 tag 约定驱动 CI 自动发布、以及服务端包如何内嵌客户端浏览器 bundle。

一、发版流程总览:6 步完成一次发布

packages/socket.io 是 Socket.IO 的 Node.js 服务端主包(当前版本为 4.8.3,见 packages/socket.io/package.json),同时它也是 monorepo 中唯一“打包分发”的包:npm 包内不仅包含服务端编译产物,还内嵌了客户端浏览器 bundle。packages/socket.io/RELEASING.md 定义的发布流程共 6 步:

  1. 更新 package.json 中的版本号
  2. conventional-changelog -p angular 更新 CHANGELOG.md
  3. 把 client 项目产出的 bundle 拷贝到 client-dist/
  4. 提交 package.jsonCHANGELOG.mdclient-dist/ 三处变更
  5. 创建 tag socket.io@x.y.z 并推送,由 CI workflow 自动发布到 npm
  6. 在 GitHub 仓库创建 Release 页面

前 4 步是维护者手动完成的内容准备,第 5、6 步分别对应自动化发布与面向用户的公告。整个流程没有任何“手动执行 npm publish”的环节——发布权完全交给 CI,这也是后文重点解释的部分。

1. 版本号与 CHANGELOG 的更新

package.jsonversion 字段是发布的唯一事实来源。以当前仓库为例,packages/socket.io/package.jsonpackages/socket.io-client/package.json 的版本均为 4.8.3——服务端包与客户端包保持锁步(lockstep)版本,这是第 3 步“拷贝 bundle”能够成立的前提:socket.io 服务端内嵌的客户端 bundle 必须来自同一版本的 socket.io-client

changelog 通过 conventional-changelog -p angular 生成,即基于 conventional commits 规范并使用 angular 预设(feat/fix/perf 等类型映射到 Major/Minor/Patch)。生成结果写入 packages/socket.io/CHANGELOG.md,其顶部是一张版本与发布日期的索引表,例如:

| Version                          | Release date  |
|----------------------------------|---------------|
| 4.8.3 (2025-12-23)               | December 2025 |
| 4.8.2 (2025-12-22)               | December 2025 |
| 4.8.1 (2024-10-25)               | October 2024  |

这意味着日常开发中提交的 commit message 规范(feat:fix: 等)直接决定了 changelog 的内容与版本号应升哪个层级。

2. 与客户端包的对应关系

socket.io 发布前必须同步 socket.io-client 的构建产物,这一步的细节见下节。值得注意的是 monorepo 的完整工作区清单:根 package.json 声明了 11 个 workspace(engine.ioengine.io-clientsocket.io-parsersocket.io-adapter 等),但 RELEASING.md 只覆盖 socket.io 这一个包。从源码结构看,其余包的发布依赖同一个 tag 触发机制(tag 命名 <包名>@<版本>,如 socket.io-client@x.y.z),packages/socket.io-client/RELEASING.md 额外要求:更新 support/package.esm.json 中的版本号、执行 npm run compilenpm run build、提交 dist/ 产物,最后还需把 bundle 同步到独立的 CDN 仓库供 cdn.socket.io 使用。

二、client-dist/:服务端包为什么内嵌客户端 bundle

RELEASING.md 第 3 步“Copy the bundles from the client project to client-dist/”看似简单,却揭示了 socket.io 的一个核心设计:服务端默认帮浏览器分发客户端 JS,因此 socket.io npm 包必须携带预构建的客户端 bundle。

当前仓库的 packages/socket.io/client-dist/ 目录包含 4 个 bundle(外加 source map):

  • socket.io.js / socket.io.min.js:UMD 格式,可直接用 <script> 标签引入,全局变量名为 io
  • socket.io.esm.min.js:ES Module 版本;
  • socket.io.msgpack.min.js:带 MessagePack 序列化支持的压缩版。

1. serveClient:bundle 的实际消费路径

服务端通过 serveClient 选项决定是否为客户端分发这些静态文件。从 packages/socket.io/lib/index.ts 可以看到实现细节:

  • 构造函数中 this.serveClient(false !== opts.serveClient)index.ts#L325)——默认开启,只有显式传 serveClient: false 才关闭;
  • 开启时,静态文件路由指向 path.join(__dirname, "../client-dist/", filename)index.ts#L577),也就是包内 client-dist/ 目录;
  • serveClient 为 false,则不注册静态文件处理器(测试用例见 packages/socket.io/test/server-attachment.ts#L154new Server(srv, { serveClient: false }))。

这正是 RELEASING.md 第 3 步存在的原因:每次发版前必须把 socket.io-client 新构建的 bundle 拷贝进 client-dist/,否则用户 npm install socket.io 后,服务端通过 /socket.io/socket.io.js 等路径分发的将仍是旧版本客户端。而 packages/socket.io/package.jsonfiles 字段确认了 client-dist/ 会随 npm 包一起发布:

"files": [
  "dist/",
  "client-dist/",
  "wrapper.mjs",
  "!**/*.tsbuildinfo"
]

2. 客户端 bundle 从哪里来

socket.io-clientbuild 脚本(见 packages/socket.io-client/package.json#L59)用 Rollup 串行执行三个配置,产出上述文件:

rollup -c support/rollup.config.umd.js \
  && rollup -c support/rollup.config.esm.js \
  && rollup -c support/rollup.config.umd.msgpack.js

packages/socket.io-client/support/rollup.config.umd.js 为例,它定义了两个输出:

  • devBundle:输入 ./build/esm-debug/browser-entrypoint.js,输出 ./dist/socket.io.js(UMD,全局名 io,带 sourcemap);
  • prodBundle:输入 ./build/esm/browser-entrypoint.js,输出 ./dist/socket.io.min.js,经 Terser 压缩。

两个 bundle 都会写入版本 banner,其中的版本号直接读取 ../package.jsonversion 字段:

const version = require("../package.json").version;
const banner = `/*!
 * Socket.IO v${version}
 * ...
 */`;

这解释了为什么客户端发版(packages/socket.io-client/RELEASING.md)必须先更新两个 package.json(主包 + support/package.esm.json)再执行 npm run build——bundle 内嵌的版本号、files 声明的 dist/ 目录(packages/socket.io-client/package.json#L13-L16)都以版本同步为前提。

3. ESM 互操作:wrapper.mjs

socket.io 是 CommonJS 包("type": "commonjs"),但对 ESM 导入提供了 packages/socket.io/wrapper.mjs 作为垫片:

import io from "./dist/index.js";

export const {Server, Namespace, Socket} = io;

package.jsonexports 字段将 import 条件指向 ./wrapper.mjsrequire 指向 ./dist/index.jspackages/socket.io/package.json#L28-L33)。该 wrapper 同样在 files 声明中,是发布产物的一部分。

三、tag 约定与 CI 自动发布

RELEASING.md 第 5 步是整套流程的自动化核心:

Create the tag socket.io@x.y.z and push it to the GitHub repository. The workflow .github/workflows/publish.yml will safely publish the package to npm using trusted publishing.

tag 采用 <包名>@<版本> 格式(例如 socket.io@4.8.3),推送后触发 .github/workflows/publish.yml。该 workflow 的关键配置如下:

on:
  push:
    tags:
      # expected format: <package>@<version> (example: socket.io@1.2.3)
      - '**@*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write        # 生成 OIDC token,用于 npm trusted publishing

    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - name: Use Node.js 26
        uses: actions/setup-node@v6
        with:
          node-version: 26
          registry-url: 'https://registry.npmjs.org'
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Compile each package
        run: npm run compile --workspaces --if-present

      - name: Publish package
        run: npm stage publish --workspace=${GITHUB_REF_NAME%@*} --access public

逐行解析:

  1. 触发条件 **@*:任何形如 xxx@yyy 的 tag 都会触发,一个 workflow 文件服务 monorepo 中所有包;tag 中的 @ 前的部分就是目标包名;
  2. permissions: id-token: write:这是 npm trusted publishing(OIDC)的必要权限,使 Actions 能向 npm 换取临时身份,全程无需在仓库中保存 NPM_TOKENcontents: read 说明发布流程只需要读代码,最小权限;
  3. npm run compile --workspaces --if-present:在 monorepo 根目录对每个声明了 compile 脚本的 workspace 执行编译。对 socket.io 而言,packages/socket.io/package.json#L47 定义 "compile": "rimraf ./dist && tsc";对 socket.io-client 则是清理 build/ 后做 CJS + ESM 双份 tsc 编译并执行 postcompile.shpackages/socket.io-client/package.json#L54)。之所以要编译,是因为 dist/ 产物不入库——CI 从源码现场构建,保证发布的字节码与 tag 指向的提交完全一致;
  4. npm stage publish --workspace=${GITHUB_REF_NAME%@*} --access public${GITHUB_REF_NAME%@*} 是 bash 参数展开,剥去 tag 名 @ 及之后的部分,即从 socket.io@4.8.3 提取出 socket.io,作为 --workspace 参数只发布这一个包。前缀 npm stage publish 表明这里使用的是 npm staged publishing(工作流文件头部注释也引用了 npm 的 trusted publishing 与 staged publishing 官方文档):包先被发布到暂存区,只有当该版本的所有产物(tarball、元数据等)都就位后才会对外可见,避免了传统 npm publish 可能出现的“半成品版本”;
  5. 注意该步骤没有指定版本号——npm 以 tag 触发时包内 package.jsonversion 字段为准。这就是 RELEASING.md 第 1 步(先改 package.json 再打 tag)与第 5 步 tag 名必须一致的原因:如果 socket.io@4.8.3 这个 tag 指向的提交里 package.json 仍写着 4.8.2,发布结果会是一个版本名与 tag 不符的包,因此“版本号更新 → 提交 → 打 tag”的顺序不可颠倒。

适用前提与限制

  • 该流程依赖仓库是 monorepo(npm workspaces),--workspaces --if-present 是根级命令;单包仓库需要相应简化;
  • trusted publishing 要求预先在 npm 控制台为 GitHub 仓库配置 publisher(仓库、workflow 文件、环境绑定),本仓库中看不到这部分配置(它在 npm 侧),因此文中“会自动发布”的结论以 RELEASING.md 的官方说明为准;
  • CI 使用 Node.js 26 执行发布,而包自身的 engines 声明(如 packages/socket.io/package.json#L81-L83node >=10.2.0)面向的是最终用户,两者不冲突但含义不同。

四、第 4 步提交的三个文件及其一致性

第 4 步要求提交 package.jsonCHANGELOG.mdclient-dist/ 三个路径的变更,三者构成一个自洽的版本快照:

提交内容 作用 一致性要求
package.json npm 元数据:nameversionfilesexportsdependencies version 必须与将创建的 tag 中版本一致
CHANGELOG.md 面向用户的变更说明,由 conventional-changelog 生成 新版本的 changelog 小节(如 # 4.8.3 (2025-12-23))需与 version 对应
client-dist/ 服务端分发的客户端 bundle 必须来自同版本的 socket.io-client 构建产物,bundle banner 中的版本号应一致

三者任一不同步都会产生问题:package.json 与 tag 不一致会导致发布版本错位;CHANGELOG.md 缺更新则 Release 说明不完整;client-dist/ 落后则出现“服务端 4.8.3 分发 4.8.2 客户端 bundle”的版本漂移,且由于 serveClient 默认开启(packages/socket.io/lib/index.ts#L325),这种漂移会静默影响所有依赖服务端分发客户端的项目。

五、第 6 步:创建 GitHub Release

流程的最后一步是在仓库的 Releases 页面手动创建 Release,向用户公告新版本及其变更内容。这一步不触发任何自动化,纯粹是发布流程的“面向人”的收尾;它与第 5 步的 tag 天然关联——Release 通常锚定在 socket.io@x.y.z 这个 tag 上,正文内容一般直接取自本次更新生成的 CHANGELOG.md 小节。

至此,一次完整发版的闭环是:规范提交(conventional commits)→ 手动升版本、生成 changelog、同步 client bundle → 提交三文件 → 打 <包名>@<版本> tag → CI 编译 + npm stage publish 经 trusted publishing 发布 → 创建 Release 公告。整个过程发布密钥零落地(OIDC 换取临时身份)、发布内容可追溯到具体 tag 指向的提交,是典型的 monorepo 安全发布范式。

六、可复用的发版要点清单

结合本文对 packages/socket.io/RELEASING.md 与配套 CI/源码的分析,可提炼出通用要点:

  1. tag 命名即路由<包名>@<版本> 格式让单个 publish workflow 服务所有包,${GITHUB_REF_NAME%@*} 提取包名,避免为每个包维护独立 workflow;
  2. 产物不入库、CI 现编译dist/build/ 均不在提交范围(files 字段只约束 tarball 内容),prepack/CI 中的 compile 脚本保证发布的产物来自 tag 指向的确切提交(如 packages/socket.io/package.json#L53"prepack": "npm run compile");
  3. 多文件版本同步点:主包有“package.json + 内嵌 bundle”两个同步点(对应第 3 步),客户端包另有 support/package.esm.json 这个 ESM 子包的版本副本,遗漏任一都会造成包内版本不一致;
  4. changelog 自动化conventional-changelog -p angular 将 commit 规范转化为发布说明,维护者无需手写;
  5. 安全发布:trusted publishing + staged publishing 组合,CI 仅持 id-token: write 权限即可完成 npm stage publish,仓库无需持久化任何 npm 凭据。

若需进一步了解各环节的实现,可重点阅读:发版文档 packages/socket.io/RELEASING.mdpackages/socket.io-client/RELEASING.md、发布工作流 .github/workflows/publish.yml、服务端静态分发逻辑 packages/socket.io/lib/index.tsserveClient 相关段落约 L325、L541-L577)、客户端 bundle 构建 packages/socket.io-client/support/rollup.config.umd.js,以及 ESM 互操作垫片 packages/socket.io/wrapper.mjs

登录后查看全文
热门项目推荐
相关项目推荐