首页
/ Next.js 仓库 PR 状态分诊实战:基于 scripts/pr-status.js 的 CI 失败排查与评审线程闭环处理

Next.js 仓库 PR 状态分诊实战:基于 scripts/pr-status.js 的 CI 失败排查与评审线程闭环处理

2026-09-06 17:22:53作者:秋阔奎Evelyn

本文围绕 Next.js monorepo 内置的 pr-status-triage 技能文档,系统讲解如何用 scripts/pr-status.js 完成 PR 状态的端到端分诊:从拉取 build-and-test CI 结果、解析失败 job 日志、识别已知不稳定(flaky)测试,到本地复现环境对齐,再到评审线程(review thread)的回复与解决闭环。读完本文,你可以掌握 Next.js 仓库维护者的标准 CI 排障工作流,以及支撑该工作流的关键实现细节。

完整分诊工作流:七步闭环

技能文档 SKILL.md 定义的核心工作流共七步,覆盖了从「拉取状态」到「收敛 flaky」的完整链路:

  1. 后台运行分析脚本:执行 node scripts/pr-status.js --wait(建议后台运行,等待上限约 1 分钟),然后读取 scripts/pr-status/results/index.md
  2. 逐个分析产物:解析 scripts/pr-status/results/ 目录下的 job-{id}.md(失败 job 详情)与 thread-{N}.md(评审线程)文件;
  3. 按阻塞级别排序:优先处理 build、lint、types,最后才是 test 类 job;
  4. 默认按真实失败处理:所有失败先当作由本次改动引起,只有核对了 "Known Flaky Tests" 章节后才能考虑 flaky 结论;
  5. 本地复现:必须与 CI 相同的运行模式(dev/start)和环境变量;
  6. 闭环评审线程:处理完评审意见后,先回复说明所做的事情,再 resolve 线程;
  7. flaky 重跑循环:当仅剩已知 flaky 测试失败且无需改代码时,用 gh run rerun <run-id> --failed 重跑失败 job,等待 5 分钟后回到第 1 步,该循环最多重复 5 次。

其中第 7 步是自动化收敛手段:gh run rerun <run-id> --failed 只重跑失败的 job 而非整个 run,避免了重新排队数百个 job 的等待成本。

命令速查:分析模式与线程交互

技能文档给出的分析命令有四种形态:

node scripts/pr-status.js                  # 当前分支对应的 PR
node scripts/pr-status.js <number>         # 指定 PR 编号
node scripts/pr-status.js [PR] --wait      # 后台模式,等待 CI 跑完
node scripts/pr-status.js --skip-flaky-check  # 跳过 flaky 测试检测

线程交互命令有三个:

node scripts/pr-status.js reply-thread <threadNodeId> "<body>"           # 回复评审线程
node scripts/pr-status.js resolve-thread <threadNodeId>                  # 解决评审线程
node scripts/pr-status.js reply-and-resolve-thread <threadNodeId> "<body>"  # 回复并解决(一步完成)

从源码看(scripts/pr-status.js),main() 先做子命令分发:reply-threadresolve-threadreply-and-resolve-thread 三个子命令直接调用对应函数后返回;其余参数按 --wait--skip-flaky-check 标志和 PR 编号解析,其中第一个非 -- 开头的参数被当作 PR 编号(scripts/pr-status.js)。--wait 的实现在 CI 仍在跑时会调用 gh run watch <runId> --compact -R vercel/next.js 阻塞等待,完成后自动重新执行一遍完整分析(scripts/pr-status.js)——这解释了为什么技能文档建议「后台运行、超时 1 分钟」:gh run watch 可能持续数十分钟。

结果目录结构:一次分析生成哪些文件

脚本的输出根目录固定为 scripts/pr-status/,分为两个子目录(scripts/pr-status.js):

  • results/ —— 面向人的 Markdown 报告;
  • intermediate/ —— 按 job 日志分节切分出的原始文本段。

每次分析会先清空并重建输出目录(scripts/pr-status.js),因此产物始终对应当前这次运行。核心产物包括:

文件 内容
index.md 总报告:分支/PR、run 状态、失败 job 表格、Job Environment Variables、Known Flaky Tests、PR Reviews、Inline Review Comments、General Comments
job-{id}.md 单个失败 job:元信息、按 ##[group] 切分的 Sections 列表、测试通过/失败统计、失败用例表格、各测试文件链接
job-{id}-test-{path}.md 单个测试文件的多次重试记录(含 NEXT_TEST_MODE、失败断言详情),无结构化数据时回退为原始日志
thread-{N}.md 评审线程:diff hunk、全部评论,以及底部附带的可直接复制执行的回复/解决命令
review-{id}.md / comment-{id}.md 完整 review 与 PR 普通评论
flaky-tests.json 检测出的 flaky 测试文件路径列表

几个实现细节值得注意:

  • 失败判定集合failuretimed_outstartup_failure 三种 conclusion(scripts/pr-status.js),即超时和启动失败同样进入失败 job 列表;
  • CI 进行中 vs 已结束走不同策略:进行中会拉取全部 job 以统计进度(报告标题为 "CI Status Report"),结束后只拉失败 job 以提升效率(scripts/pr-status.js);
  • API 分页有重试保护:job 列表按每页 30 条分页,每页请求最多重试 3 次(间隔递增 2s/4s);首页彻底失败会抛错,后续页失败则带着已有数据继续(scripts/pr-status.js);
  • 日志解析双通道:脚本同时用正则提取 --test output start-- {JSON} --test output end-- 包裹的结构化 Jest 输出(scripts/pr-status.js)和 ##[group]❌ test/... ##[endgroup] 原始日志块,再按测试路径合并——所以即使某个测试只留下原始日志、没有结构化 JSON,也会在 job-{id}-test-*.md 里出现。

阻塞优先排序:build > lint > types > tests

参考文档 workflow.md 给出了明确的优先级顺序:

  1. Build 失败
  2. Lint 失败
  3. 类型检查失败
  4. 测试失败
  5. 评审意见(在 CI 阻塞项之后处理)

配套三条失败处理规则:

  • 每个失败 job 都按「由当前改动引起」来调查;
  • 不默认假设是 flaky
  • job 输出中的 "Known Flaky Tests" 章节只作为历史上下文,而不是自动豁免依据。

这套规则背后的工程逻辑是:Next.js 的 CI 矩阵中,build/lint/types 类 job 是廉价且确定性的——它们一旦失败几乎必然是代码问题,修复成本远低于先跑几百个测试 job。只有当阻塞项清除后,测试失败才值得投入排查时间。

Known Flaky Tests:脚本如何判定「已知不稳定」

index.md 中的 "Known Flaky Tests (failing on 2+ branches)" 章节由 getFlakyTests() 生成(scripts/pr-status.js),其判定逻辑为:

  1. 拉取 build-and-test 工作流最近 5 次失败 run(跨所有分支,且排除当前分支自身,避免自我匹配);
  2. 跳过失败 job 超过 20 个的 run(从源码结构看,这类 run 大概率是系统性故障而非个别 flaky);
  3. 并行抓取这些 run 中失败 job 的日志(每批 5 个请求控制 API 压力),提取每个失败测试文件路径及其所在分支;
  4. 同一测试文件在 2 个及以上不同分支上失败,即判定为 flakyscripts/pr-status.js)。

判定结果会写入 flaky-tests.json,并渲染进 index.md。但再次强调文档的处理规则:该列表是「历史上下文」,遇到其中的测试失败仍应先在本地验证;--skip-flaky-check 可在不需要时跳过这段耗时检测。

本地复现:模式与环境变量必须与 CI 对齐

local-repro.md 是本地复现的操作手册,核心原则是双对齐:运行模式对齐 + 环境变量对齐。

1. 模式对齐

CI 失败类型 本地命令
dev 模式 pnpm test-dev-turbopnpm test-dev-webpack
start 模式 pnpm test-start-turbopnpm test-start-webpack

这些脚本在仓库根 package.json 中均有定义,底层统一走 scripts/run-jest.sh --mode=dev --bundler=turbo --headless -- 之类的组合参数,即「模式 × bundler × 无头」三个维度自由组合。

2. 环境变量对齐

读取 index.md 的 "Job Environment Variables" 章节并在本地镜像这些变量。该章节并非脚本凭空生成——getJobEnvVarsFromWorkflow() 直接解析 CI 工作流文件 .github/workflows/build_and_test.yml 中各 job 的 afterBuild 段里 export KEY=VALUE 形式的语句,按 job 显示名前缀匹配失败 job 后渲染(scripts/pr-status.js)。

工作流文件中确实存在这类导出,例如部分 job 的 afterBuild 中写有 export IS_WEBPACK_TEST=1export NEXT_TEST_MODE=devexport __NEXT_CACHE_COMPONENTS=true 等。关键变量含义:

  • IS_WEBPACK_TEST=1 —— 强制使用 webpack 模式(本地默认是 turbopack);
  • NEXT_SKIP_ISOLATE=1 —— 跳过包隔离;文档明确要求:验证模块解析或构建期编译修复时绝不可使用它
  • 特性开关如 __NEXT_USE_NODE_STREAMS=true__NEXT_CACHE_COMPONENTS=true —— 会改变 DefinePlugin 的替换值,即切换框架运行时代码路径,直接影响 e2e 行为。

文档给出的完整示例:一个名为 "test node streams prod" 的 job 失败,本地复现需要:

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

隔离规则(Isolation Rule):当验证的是模块解析(module-resolution)、入口导出(entrypoint-export)或内部 require 路径修复时,重跑测试必须去掉 NEXT_SKIP_ISOLATE=1,否则被跳过的包隔离步骤恰好掩盖了你要验证的 bug。

3. 一次运行、多次分析

避免反复跑测试浪费时间,标准做法是「跑一次、落盘、多查几次」:

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

是 Jest 失败用例的标记符,grep -A5 用于抓取错误堆栈前 5 行上下文,tail 查看总结。

常见失败模式与修复命令

workflow.md 还沉淀了三类高频失败的处理套路:

失败 job 类型 排查/修复动作
rust check / build 先跑 cargo fmt -- --check,有格式差异则用 cargo fmt 修复
lint / build 对报错文件执行 pnpm prettier --write <file>,必要时再跑仓库 lint 命令
test 失败 本地跑精确的失败测试文件,并将 dev/start 模式对齐到该 CI job

这些模式的价值在于把「失败 job 名 → 高概率修复命令」映射成了查表操作,跳过逐行读日志的低效阶段。

评审线程闭环:回复先行,resolve 殿后

workflow.md 对评审线程的处理流程是:

  1. 处理完评审意见(或发现当前代码其实已满足要求)后,先回复线程并描述所做操作:
    node scripts/pr-status.js reply-thread <threadNodeId> "Done -- <changes description>"
    
  2. 再解决线程:
    node scripts/pr-status.js resolve-thread <threadNodeId>
    
    或者一步完成:
    node scripts/pr-status.js reply-and-resolve-thread <threadNodeId> "Done -- <changes description>"
    

文档特别强调:resolve 之前必须先回复说明改了什么,让评审者获得上下文。而 <threadNodeId> 无需手查——每个 thread-N.md 文件底部已由 generateThreadMd() 自动生成了填好真实线程 ID 的可复制命令(scripts/pr-status.js)。

源码层面有两个值得了解的实现选择(scripts/pr-status.js):

  • 回复走「GraphQL 查询 + REST 提交」两步:先用 GraphQL node(id:) 查询反查出线程所属 PR 编号和首条评论的 databaseId(REST 回复接口必需),再通过 REST 的 replies 端点发布。代码注释解释了原因:该 REST 端点总是立即发布回复,而 GraphQL 的 addPullRequestReviewThreadReply 会把回复挂到 pending 草稿 review 上,达不到即时反馈的效果;另外回复正文会被自动加上 :robot: 前缀,标明是机器人/AI 代发;
  • 解决走 GraphQL mutationresolveReviewThread 执行后校验返回的 isResolved,不一致时打印告警而非静默。

总结:这套分诊体系的可复用要点

把 Next.js 仓库的 pr-status-triage 技能抽象开看,它示范了一套可移植到任意 monorepo 的 CI 排障范式:

  1. 报告即接口:把 CI API 的原始数据预处理成分层 Markdown(总览 → job → 测试文件 → 线程),排查者读文件而非翻日志;
  2. 环境可复现性自动化:直接从 CI 工作流文件解析出每个 job 的环境变量并附在报告里,本地复现只需「照抄」;
  3. flaky 判定数据化:用「跨 2+ 分支的重复失败」代替主观猜测,但保留人工确认环节;
  4. 线程操作命令化:把回复/解决评审线程收敛为三个带 ID 的脚本子命令,且 ID 由报告自动生成,消除手工查询成本。

结合 SKILL.md 的七步工作流、workflow.md 的优先级规则与 local-repro.md 的复现手册,再配合 scripts/pr-status.js 的实现细节,就构成了从「CI 红了」到「PR 可合并」的完整操作闭环。

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