首页
/ Next.js 仓库 CI 失败本地复现指南:模式、环境变量与日志分析实战

Next.js 仓库 CI 失败本地复现指南:模式、环境变量与日志分析实战

2026-09-05 23:06:59作者:裴锟轩Denise

本篇基于 Next.js 仓库内置的 .agents/skills/pr-status-triage 技能文档(local-repro.md),讲解如何把 CI 上失败的测试任务在本地精确复现出来:包括按 CI job 模式选择正确的测试命令(dev/start × webpack/turbopack)、镜像 CI 环境变量(如 IS_WEBPACK_TESTNEXT_SKIP_ISOLATE__NEXT_CACHE_COMPONENTS 等)、理解"隔离安装"对模块解析验证的影响,以及用"一次跑、多次析"的方式高效分析测试日志。读完后,你可以把任意一个 CI 红色 job 转化为一条可复制、可验证的本地复现命令,并快速定位失败根因。

背景:从 PR Status 到本地复现

Next.js 是一个大型 monorepo,CI 会按"模式 × bundler × 特性开关"的矩阵反复运行同一批 e2e 测试。当某个 PR 的 CI 失败时,仓库内置的 PR 分诊技能给出了一套标准流程。SKILL.md 中的工作流为:

  1. 在后台运行 node scripts/pr-status.js --wait(超时 1 分钟),然后读取 scripts/pr-status/results/index.md
  2. 分析 scripts/pr-status/results/ 下每个 job-{id}.mdthread-{N}.md 文件,提取失败与评审反馈;
  3. 按"构建 > lint > 类型 > 测试"的优先级处理阻塞任务(见 workflow.md 中的排序);
  4. 在断定某测试是 flaky 之前,先查该 job 输出中的 "Known Flaky Tests" 小节——"在未被证伪之前,一律视为真实失败";
  5. 用与 CI 相同的模式和环境变量在本地复现——这正是本文核心文档 local-repro.md 解决的问题;
  6. 处理完评审意见后,用 reply-and-resolve-thread 一次性回复并解决线程;
  7. 若仅剩已知 flaky 测试且无需代码改动,用 gh run rerun <run-id> --failed 重跑失败 job,等 5 分钟后回到第 1 步,循环最多 5 次。

其中第 5 步的关键原则是:本地复现必须与 CI job 的模式(mode)和环境变量(env vars)完全对齐。只要有一处不一致——比如 CI 是 webpack 而本地默认跑了 turbopack,或 CI 开了某个特性开关而本地没开——你复现出来的可能就是另一个问题,甚至"复现不出"问题。

对齐 CI Job 的测试模式

CI 的测试任务区分两类执行模式,对应仓库中两套测试命令:

  • Dev-mode 失败next dev 开发服务器场景):使用 pnpm test-dev-turbo(Turbopack bundler)或 pnpm test-dev-webpack(webpack bundler);
  • Start-mode 失败next build && next start 生产服务器场景):使用 pnpm test-start-turbopnpm test-start-webpack

这些脚本命令都定义在根 package.json 中,例如:

"test-dev-webpack": "scripts/run-jest.sh --mode=dev --bundler=webpack --headless --",
"test-dev-turbo": "scripts/run-jest.sh --mode=dev --bundler=turbo --headless --",
"test-start-turbo": "scripts/run-jest.sh --mode=start --bundler=turbo --headless --",
"test-start-webpack": "scripts/run-jest.sh --mode=start --bundler=webpack --headless --"

所有命令最终都收敛到 scripts/run-jest.sh,它把命令行参数翻译成环境变量后再 exec jest --runInBand。对照 run-jest.sh 的参数解析逻辑,映射关系非常清晰:

run-jest.sh 参数 导出的环境变量 含义
--mode=dev / --mode=start / --mode=deploy NEXT_TEST_MODE 决定测试通过开发服务器、生产服务器还是部署产物运行
--bundler=webpack IS_WEBPACK_TEST=1 强制使用 webpack 打包
--bundler=turbo IS_TURBOPACK_TEST=1 使用 Turbopack 打包
--bundler=rspack NEXT_RSPACK=1NEXT_TEST_USE_RSPACK=1 使用 Rspack 打包
--experimental __NEXT_CACHE_COMPONENTS=true 开启 cache components 特性
--headless HEADLESS=true 无头模式运行 Playwright

因此当你看到 CI job 叫 "test node streams prod"(start 模式 + webpack)时,本地对应的就是 pnpm test-start-webpack;而 CI job 是 "test dev"(turbopack)时,对应的就是 pnpm test-dev-turbo。选错模式是最常见的复现偏差来源:dev 模式下 Next 使用按需编译(on-demand compilation),start 模式下则是预构建产物,两者的报错栈与行为可能完全不同。

对齐 CI 环境变量

local-repro.md 要求"读取 index.md 的 Job Environment Variables 小节并在本地镜像它们"(index.mdscripts/pr-status/results/ 下由 pr-status 脚本生成的 CI 汇总文件)。其中最关键的变量有三个:

IS_WEBPACK_TEST=1:强制 webpack 模式

默认情况下本地测试运行在 turbopack 模式下,只有显式设置 IS_WEBPACK_TEST=1 才会切到 webpack。这一点从 run-jest.sh 中也能印证:--bundler=webpack 分支唯一做的事就是 export IS_WEBPACK_TEST=1。如果你的 CI job 是 webpack 维度的,而本地忘了带这个变量,等于在另一个 bundler 上复现,结果没有可比性。

NEXT_SKIP_ISOLATE=1:跳过包隔离(谨慎使用)

这个变量控制测试是否安装一个"隔离的 next"。看 test/lib/next-modes/base.ts 中的处理:

const skipIsolatedNext = !!process.env.NEXT_SKIP_ISOLATE
// 'Creating test directory with isolated next... (use NEXT_SKIP_ISOLATE=1 to opt-out)'

devstart 两种模式(next-dev.tsnext-start.ts)都会检查这个变量:正常跑测试时,每个测试项目会创建一份独立的 next 安装(isolated install),以模拟真实用户在干净 node_modules 里的行为;设置 NEXT_SKIP_ISOLATE=1 后则直接复用 monorepo 工作区里的 next,启动更快。

文档对此给出的规则是:验证模块解析(module resolution)、入口导出(entrypoint export)或内部 require 路径类修复时,绝不要使用 NEXT_SKIP_ISOLATE=1——因为这类问题恰恰只在干净的隔离安装中才会暴露,工作区安装会用各种 workspace 软链"掩盖"问题。

特性开关:__NEXT_USE_NODE_STREAMS__NEXT_CACHE_COMPONENTS

形如 __NEXT_USE_NODE_STREAMS=true__NEXT_CACHE_COMPONENTS=true 的特性 flag 会改变构建期 DefinePlugin 的替换结果,即直接改写打包产物中的常量表达式,属于"编译期行为分叉",必须与 CI 保持一致。

run-jest.sh 还揭示了另一层机制:__NEXT_TEST_AXIS 测试轴。当 CI 的 job 带有 axis 参数(如 __NEXT_TEST_AXIS=A)时,脚本会自动导出 __NEXT_CACHE_COMPONENTS=true;反之设置 __NEXT_CACHE_COMPONENTS=true 也隐含 axis A。该机制的完整语义(fixture 如何用 process.env.__NEXT_TEST_AXIS !== 'A' 配合 // @gate 注释在开/关两种状态间切换)可参见 test/lib/gate/README.md 中的示例:

__NEXT_TEST_AXIS=A NEXT_SKIP_ISOLATE=1 pnpm test-start-webpack test/e2e/app-dir/concurrent-router-queue/concurrent-router-queue.test.ts

完整示例:复现 "test node streams prod"

文档给出的端到端示例:CI job "test node streams prod" 失败时,本地需要的完整环境变量组合为:

IS_WEBPACK_TEST=1 __NEXT_USE_NODE_STREAMS=true __NEXT_CACHE_COMPONENTS=true NEXT_TEST_MODE=start

拆解一下各变量的作用:IS_WEBPACK_TEST=1 对齐 webpack bundler;__NEXT_USE_NODE_STREAMS=true 对齐 node streams 特性维度;__NEXT_CACHE_COMPONENTS=true 对齐 cache components 维度;NEXT_TEST_MODE=start 对齐生产 start 模式。注意前四个是"裸环境变量"写法(不经过 pnpm test-* 脚本,手动控制全部维度),这也是 run-jest.sh--mode=start 内部导出的同名变量,两种写法等价,但手写组合更贴近 CI job 的原始 env 列表,适合逐变量比对。

隔离规则(Isolation Rule)

文档单列了"Isolation Rule"一节,内容浓缩为一句话:当你在验证模块解析、入口导出或内部 require 路径的修复时,重跑测试时必须去掉 NEXT_SKIP_ISOLATE=1

结合源码可以推断这条规则背后的机制:正常流程中,测试基础设施会在每个测试项目目录内创建一份隔离的 next 安装(Creating test directory with isolated next...,见 base.ts),此时被测应用解析 next 时走的是这份独立副本的 dist 产物,与真实用户安装一致;而跳过隔离后,base.ts 处的注释也明确说明 "When running with NEXT_SKIP_ISOLATE there is no isolated install"——测试项目直接依赖 workspace 中的 next 源码结构,任何"只有干净安装才成立"的解析问题(例如某文件没有正确拷贝进 dist、exports map 在真实 node_modules 布局下失效)都不会触发。

实操含义:日常调试可以先开着 NEXT_SKIP_ISOLATE=1 快速迭代(CI 上 test/lib/gate/README.md 的示例命令就是这么用的,如 NEXT_SKIP_ISOLATE=1 pnpm test-start test/e2e/app-dir/segment-cache/basic),但在最终"验证修复有效"的那一跑,务必去掉该变量,以隔离安装的口径确认问题真的修好了。

一次跑、多次析:测试日志分析法

local-repro.md 的最后一节给出了"Capture once, analyze multiple times"的日志分析套路——e2e 测试单跑成本很高(需要 build、起服务、驱动浏览器),所以应该把完整输出落盘一次,之后用 grep/tail 反复切片分析,而不是每看一个失败点就重跑一遍:

HEADLESS=true pnpm test-dev-turbo test/path/to/test.ts > /tmp/test-output.log 2>&1
grep "●" /tmp/test-output.log
grep -A5 "Error:" /tmp/test-output.log
tail -5 /tmp/test-output.log

逐步说明:

  1. pnpm test-dev-turbo test/path/to/test.ts:把 CI 失败的那个测试文件路径作为 jest 的位置参数传入(pnpm 会把 -- 之后的参数透传给 jest,见 package.json 中脚本末尾的 --);HEADLESS=true 保证无头浏览器运行,避免在服务器上因缺显示而挂起(pnpm test-* 系列脚本本身已内置 --headless,此处显式写出是为了在裸 jest 调用场景下也生效);
  2. > /tmp/test-output.log 2>&1:stdout 与 stderr 一起落盘,jest 的失败汇总、Playwright 的报错、服务进程的堆栈都会保留在同一文件里;
  3. grep "●" 是 jest 输出中失败用例/失败块的标记符,一条命令列出所有失败点,相当于失败清单;
  4. grep -A5 "Error:":抽取每个 Error: 及随后 5 行上下文,快速浏览各类错误的堆栈头部;
  5. tail -5:查看运行末尾——jest 的汇总统计(Tests: X failed, Y passed)和进程退出前的最终消息都在这里。

这套方法的通用价值在于:对任何"昂贵的一次性输出"(CI 日志、构建日志、压测输出),先完整捕获再反复检索,能显著减少重复执行的成本,也方便把 /tmp/test-output.log 的片段贴给评审者或分诊流程中的 pr-status.js 结果文件做交叉比对。

小结

本地复现 CI 失败的三要素可以浓缩为:模式对齐(dev/start 决定 NEXT_TEST_MODE,选对 pnpm test-{dev|start}-{turbo|webpack})、环境变量镜像(逐条对照 CI job 的 env,特别注意 IS_WEBPACK_TEST 与特性 flag,并牢记模块解析类验证必须去掉 NEXT_SKIP_ISOLATE=1)、日志一次捕获多次分析(落盘后 grep Error:tail 快速定位)。相关参考材料都在仓库内:分诊入口 .agents/skills/pr-status-triage/SKILL.md、失败模式与优先级 .agents/skills/pr-status-triage/workflow.md、参数到环境变量的映射 scripts/run-jest.sh、隔离安装实现 test/lib/next-modes/base.ts,以及测试轴(axis)机制说明 test/lib/gate/README.md

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