Next.js 仓库 CI 失败本地复现指南:模式、环境变量与日志分析实战
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
本篇基于 Next.js 仓库内置的.agents/skills/pr-status-triage技能文档(local-repro.md),讲解如何把 CI 上失败的测试任务在本地精确复现出来:包括按 CI job 模式选择正确的测试命令(dev/start × webpack/turbopack)、镜像 CI 环境变量(如IS_WEBPACK_TEST、NEXT_SKIP_ISOLATE、__NEXT_CACHE_COMPONENTS等)、理解"隔离安装"对模块解析验证的影响,以及用"一次跑、多次析"的方式高效分析测试日志。读完后,你可以把任意一个 CI 红色 job 转化为一条可复制、可验证的本地复现命令,并快速定位失败根因。
背景:从 PR Status 到本地复现
Next.js 是一个大型 monorepo,CI 会按"模式 × bundler × 特性开关"的矩阵反复运行同一批 e2e 测试。当某个 PR 的 CI 失败时,仓库内置的 PR 分诊技能给出了一套标准流程。SKILL.md 中的工作流为:
- 在后台运行
node scripts/pr-status.js --wait(超时 1 分钟),然后读取scripts/pr-status/results/index.md; - 分析
scripts/pr-status/results/下每个job-{id}.md和thread-{N}.md文件,提取失败与评审反馈; - 按"构建 > lint > 类型 > 测试"的优先级处理阻塞任务(见 workflow.md 中的排序);
- 在断定某测试是 flaky 之前,先查该 job 输出中的 "Known Flaky Tests" 小节——"在未被证伪之前,一律视为真实失败";
- 用与 CI 相同的模式和环境变量在本地复现——这正是本文核心文档 local-repro.md 解决的问题;
- 处理完评审意见后,用
reply-and-resolve-thread一次性回复并解决线程; - 若仅剩已知 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-turbo或pnpm 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=1、NEXT_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.md指scripts/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)'dev、start两种模式(next-dev.ts、next-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 withNEXT_SKIP_ISOLATEthere 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逐步说明:
pnpm test-dev-turbo test/path/to/test.ts:把 CI 失败的那个测试文件路径作为 jest 的位置参数传入(pnpm会把--之后的参数透传给 jest,见 package.json 中脚本末尾的--);HEADLESS=true保证无头浏览器运行,避免在服务器上因缺显示而挂起(pnpm test-*系列脚本本身已内置--headless,此处显式写出是为了在裸jest调用场景下也生效);> /tmp/test-output.log 2>&1:stdout 与 stderr 一起落盘,jest 的失败汇总、Playwright 的报错、服务进程的堆栈都会保留在同一文件里;grep "●":●是 jest 输出中失败用例/失败块的标记符,一条命令列出所有失败点,相当于失败清单;grep -A5 "Error:":抽取每个Error:及随后 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。
【免费下载链接】next.jsThe React Framework项目地址: https://gitcode.com/GitHub_Trending/next/next.js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考