Lesson 0007 给了你自动化验证——npm test 3 秒出结果。但这里有一个 gap:测试只在你记得跑的时候才有用。本课补上缺失的另一半:让测试在每次 push 和 PR 时自动运行。你会学会编写 GitHub Actions workflow 文件,理解 CI 是如何成为"编码 → 发布"流水线中的安全守门人。
npm test"进化为"push 代码 → CI 自动跑 → 不过不会合并"。本课不引入任何 npm 依赖——GitHub Actions 是 GitHub 内置服务,对公开仓库免费。
Lesson 0007 教你写了 8 个 fixture + 8 个测试用例。你每次改代码后可以跑 npm test,3 秒确认没弄坏什么。但这里有一个关键问题:
手动 npm test | pre-push hook | CI 自动运行 | |
|---|---|---|---|
| 触发方式 | 你记得跑 | git push 时自动触发 | 每次 push / PR 自动触发 |
| 运行环境 | 你的机器(可能有隐藏状态) | 你的机器 | 全新虚拟机——从零安装 |
| 能否被绕过 | ❌ 你忘了就跑不了 | ⚠️ --no-verify 一秒绕过 | ❌ 无法绕过(见第 4 节) |
| 谁看到结果 | 只有你 | 只有你 | 整个团队(GitHub PR 页面上直接显示 ✅/❌) |
| 防回归 | ⚠️ 取决于你的记忆力 | ⚠️ 取决于你的自律 | ✅ 强制执行——没过不能合并 |
| 发布前保护 | ❌ 你可以在测试失败时打 tag | ❌ 无法阻止打 tag | ⚠️ 需配合分支保护规则(本课第 4 节讨论) |
| 最佳场景 | 开发中的快速反馈 | 本地最后一道防线 | 合并前的强制守门人 |
三者互补,不是替代——它们分别守在不同位置:
npm test:开发中 3 秒快速反馈--no-verify 可绕过,它是便利工具而非安全机制
你完全可以三者都用:pre-push hook 快速拦截低级错误,CI 在云端做最终裁决。但 pre-push hook 的"不通过就不 push"有一个根本局限:它是客户端脚本——团队里任何一个人 git push --no-verify 就跳过了。真正不可绕過的守门人,是服务端的 CI + 分支保护规则。
GitHub Actions 是 GitHub 内置的 CI/CD 服务。它的核心概念用一张表就能说清:
| 概念 | YAML 键 | 作用 | 类比 |
|---|---|---|---|
| 工作流 | name | 一个 YAML 文件定义一个自动化流程 | 一个"脚本" |
| 触发器 | on | 什么时候运行——push、PR、tag、定时、手动 | "什么时候执行这个脚本" |
| 作业 | jobs | 独立的工作单元——默认并行运行 | 一个"函数" |
| 运行器 | runs-on | 操作系统环境——ubuntu、windows、macOS | "在哪台机器上跑" |
| 步骤 | steps | 顺序执行的命令或市场动作 | 函数内的"语句" |
| 市场动作 | uses | 社区共享的可复用步骤——checkout、setup-node | "import 一个库" |
| Shell 命令 | run | 直接在运行器上执行的 bash 命令 | 终端里敲的命令 |
on 决定了 workflow 什么时候启动。最常用的四种:
| 触发器 | YAML 写法 | 什么时候触发 |
|---|---|---|
| Push | push: branches: [main] | 向 main 分支推送任何 commit |
| Pull Request | pull_request: branches: [main] | 向 main 分支发起 PR,或 PR 有新的 commit |
| Tag | push: tags: ['v*'] | 推送一个匹配 v* 的 tag |
| 手动 | workflow_dispatch | 在 GitHub Actions 页面手动点击"Run workflow" |
其他 CI 工具(Jenkins、Travis CI、CircleCI)也能做同样的事。但 GitHub Actions 有两个不可替代的优势:
对于一个独立开发者的 VS Code 扩展项目,外部 CI 工具的额外复杂度不带来任何收益。
以下是一个最简可用的测试 workflow。把它放在 .github/workflows/test.yml,GitHub 就会自动执行:
# ===== .github/workflows/test.yml ===== name: Test ← 在 Actions 页面显示的名称 on: ← 触发器 push: branches: [main] ← 向 main 推送时触发 pull_request: branches: [main] ← 向 main 发起/更新 PR 时触发 jobs: ← 作业列表 test: ← 作业 ID(自定义名称) runs-on: ubuntu-latest ← 运行器操作系统 steps: ← 步骤列表(顺序执行) - uses: actions/checkout@v4 ← 步骤 1:克隆仓库代码 - name: Setup Node.js uses: actions/setup-node@v4 ← 步骤 2:安装 Node.js with: node-version: '20' ← 指定 Node 版本 - name: Install dependencies run: npm ci ← 步骤 3:安装依赖 - name: Run tests run: npm test ← 步骤 4:运行测试
步骤 1——actions/checkout@v4:这是一段 GitHub 维护的开源代码。它的作用是把你的仓库克隆到运行器上。没有这一步,运行器的文件系统是空的——没有源代码,什么都做不了。@v4 是 Git 标签,锁定大版本以保证稳定性。
步骤 2——actions/setup-node@v4:安装指定版本的 Node.js。它还自动处理 node_modules 缓存——如果存在 package-lock.json,它会智能地跨运行复用已下载的包,加速后续运行。
步骤 3——npm ci:这是最关键的一步,也是新手最容易踩的坑。
npm ci 而不是 npm install?npm ci(clean install)专为 CI 环境设计。它做了三件 npm install 不做的事:node_modules——确保每次都是干净安装package-lock.json——如果 lock 文件与 package.json 不同步,直接报错退出(而非静默更新 lock 文件)npm install 是一个反模式——它会悄悄修改 lock 文件,让 CI 的"干净环境"失去意义。
步骤 4——npm test:这是唯一与你代码直接相关的命令。但——等等——package.json 目前还没有 test 脚本。这个 gap 正是第 6 节要解决的问题。
理解一个关键事实:当 workflow 启动时,ubuntu-latest 是一台全新的虚拟机。它上面没有你的源代码、没有安装 Node.js、没有 node_modules。每一步都在从头构建环境:
Runner 初始状态:空目录 ├─ Step 1 (checkout) → 克隆仓库代码 ✅ ├─ Step 2 (setup-node) → 安装 Node.js 20 ✅ ├─ Step 3 (npm ci) → 安装 npm 依赖 ✅ └─ Step 4 (npm test) → 运行测试 ✅ 每一步依赖前面步骤构建的状态
这正是 CI 的核心价值:它证明你的代码能在没有你机器上隐藏状态的前提下运行。如果你的测试在本地通过但在 CI 失败,那一定是因为你的机器上有某些"特殊条件"——全局安装的包、未提交的配置文件、环境变量——CI 帮你发现了它们。
node:test 的一个"副作用"现在显现了——因为 node:test 是 Node 内置模块,CI 中不需要额外安装任何测试框架。如果 0007 选了 Jest,CI workflow 就要多一步 npm install jest。零依赖的好处会传递到 CI 配置中。
git push 之前跑 npm test,不过就不 push。这听起来比"先 push 再让 CI 跑"更合理。但这里有一个关键的信任边界问题:.git/hooks/ 目录里(不会被 Git 跟踪),git push --no-verify 一键就能跳过。你无法强制团队其他人启用它。main 分支,不是个人分支。
你的项目已经有一个 workflow 了——.github/workflows/publish.yml。把它和我们要创建的 test.yml 放在一起看:
test.yml(本课创建) | publish.yml(已存在) | |
|---|---|---|
| 触发器 | push / PR → main | push tag → v* |
| 目的 | 验证代码正确性 | 发布到 Marketplace |
| 运行器 | ubuntu-latest | ubuntu-latest |
| 步骤 | checkout → node → install → test | checkout → node → install → publish |
| 密钥 | 无 | VSCE_PAT + OVSX_PAT |
把它们串起来,就是完整的 dev→publish 流水线:
开发 → 本地测试 → push 到 main ↓ CI 运行 test.yml ↙ ↘ 通过 ✅ 失败 ❌ → 修 bug → 回到"开发" ↓ 打 tag v1.5.0 → 推送 ↓ CI 运行 publish.yml ↓ 发布到 Marketplace + Open VSX
test.yml 和 publish.yml 是两个独立 workflow。如果你推送一个有 bug 的 commit 并同时打 tag,test.yml 可能失败但 publish.yml 仍然会成功——它们之间没有依赖关系。在当前设计下,你需要自己确保打 tag 之前 CI 通过。needs: test 声明依赖——这样 test 不过 publish 不会跑。详见 GitHub Actions: jobs.<job_id>.needs。
VS Code 扩展有一个特殊考量:不同版本的 VS Code 内嵌了不同版本的 Node.js。你的扩展在 package.json 中声明 "engines": {"vscode": "^1.74.0"}——这意味着它支持的 VS Code 版本跨度可能覆盖多个 Node 版本:
| VS Code 版本 | 内嵌 Node.js | 状态 |
|---|---|---|
| 1.74(扩展最低要求) | Node 16 | EOL(生命周期结束) |
| 1.85 | Node 18 | 维护 LTS |
| 1.92 | Node 20 | 活跃 LTS |
| 1.96+ | Node 22 | 当前版本 |
你的扩展用户可能运行在 Node 18、20 或 22 上。测试所有三个版本是务实的折中——Node 16 已 EOL,不值得维护;18/20/22 覆盖了绝大多数用户。
GitHub Actions 的 matrix strategy 让你用几行 YAML 同时在多个 Node 版本上运行测试:
jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: ['18', '20', '22'] ← 3 个值 = 3 个并行 job steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} ← 引用矩阵变量 - run: npm ci - run: npm test
os: [ubuntu-latest, windows-latest],就是 3 × 2 = 6 个 job。对于这个纯 Node.js 项目(无原生模块),跨 OS 测试没必要——但概念知道即可。${{ }} 是什么?GitHub Actions 的表达式语法。在 workflow 文件中,任何需要动态求值的部分都用 ${{ }} 包裹。matrix.node-version 在每次 job 启动时被替换为当前 job 的矩阵值(18、20 或 22)。
test 脚本到 package.json
回头看 temp_repo/package.json 的 scripts 块——它缺少最关键的一行:
"scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "lint": "eslint src --ext ts", // ← 没有 "test" 脚本! "package": "vsce package", "publish": "vsce publish" }
有两种方式定义 test 脚本:
| 方案 A:tsx(直接跑 .ts) | 方案 B:tsc 先编译(本课推荐) | |
|---|---|---|
| 命令 | "test": "node --import tsx --test src/**/*.test.ts" | "test": "node --test out/**/*.test.js" |
| 依赖 | 需要新增 tsx devDependency | 零新依赖——复用现有 tsc |
| 速度 | 快——无需编译步骤 | 慢一次编译——但可以用 pretest 自动化 |
| 哲学 | 方便优先 | 与 0007 的"零依赖"一致 |
node:test 的理由——不引入新依赖,利用已有基础设施。但 tsx 是一个值得了解的好工具:零配置 TypeScript 执行器,让 node --test 直接跑 .ts 文件。当你测试文件变多、每次编译变得烦人时,考虑迁移。
npm 有一组内置的生命周期钩子:如果你定义了 pretest,npm 会在执行 test 之前自动运行它。利用这个机制,npm test 可以自动编译:
"scripts": { "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "lint": "eslint src --ext ts", "pretest": "npm run compile", ← npm 在 test 前自动运行 "test": "node --test out/**/*.test.js", ← 新增 "package": "vsce package", "publish": "vsce publish" }
现在 npm test 的执行顺序是:pretest(编译 TypeScript)→ test(运行测试)。一个命令,两步自动完成。
npm test 不需要 run?对——test 是 npm 的四个内置生命周期快捷方式之一(另外三个是 start、stop、restart)。对于这些,npm <name> 等价于 npm run <name>。所有其他脚本都需要 npm run <name>。
把所有概念组合在一起,这是最终的 .github/workflows/test.yml:
# ===== .github/workflows/test.yml ===== name: Test on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: ['18', '20', '22'] steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci - name: Compile TypeScript run: npm run compile - name: Run tests run: npm test
npm run compile。虽然 pretest 在本地会自动编译,但在 CI 中显式列出编译步骤有几个好处:out/ 目录),独立步骤更容易操作
temp_repo/package.json,在 scripts 中添加 "pretest": "npm run compile" 和 "test": "node --test out/**/*.test.js"。运行 npm test——即使没有测试文件,node --test 也应该干净退出(无匹配文件不算失败)。确认 pretest → test 的执行顺序temp_repo/.github/workflows/ 目录下创建 test.yml,使用第 3 节的最简版本(单 Node 版本,无矩阵)。逐行检查 YAML 缩进——必须是 2 个空格,不能是 tab。不要把 out/ 目录提交到 Git——CI 会在运行时编译test.yml,加入 strategy.matrix(node-version: ['18', '20', '22'])。在修改之前先回答:这个矩阵会产生几个 job?如果加 os: [ubuntu-latest, windows-latest] 呢?git push origin mainfeat/foo 分支向 main 发起 PRgit tag v1.6.0 && git push origin v1.6.0feat/bar 分支 push(没有对应的 PR)test.yml 推送后会出现的同一页面npm test 之前加一个 step 运行 npm run lint。思考:lint 失败应该阻止测试运行吗?还是应该并行跑?如果你不确定答案——这说明你已经触碰到了 CI 设计的核心权衡:快速失败 vs 全面反馈在 CI workflow 中使用 npm ci 而非 npm install 的主要原因是什么?
当一个 GitHub Actions workflow 启动时,runs-on: ubuntu-latest 的运行器上有什么?
你有两个 workflow:test.yml(触发:push/pull_request → main)和 publish.yml(触发:tags → v*)。你执行 git push origin main --tags(同时推送了一个 commit 和一个 v1.5.0 tag)。哪些 workflow 会运行?
strategy.matrix 中 node-version: ['18', '20', '22'] 会产生怎样的执行行为?
npm test 可以省略 run(不需要写成 npm run test)。这是为什么?
npm ci vs npm install 的详细行为差异actions/cache 如何缓存 node_modules 以加速后续运行吗?想了解如何用 workflow_dispatch 手动触发 workflow 而不需要 push 代码吗?想看到 needs 关键字如何让 publish job 依赖 test job 通过吗?这些问题都可以继续追问。