agilelabs-fx-docs main reference/frontend-ci-build-troubleshooting.md

前端 CI 构建故障排查与修复经验

本文沉淀一次真实前端 CI 故障的排查经验。相关问题先后出现在 feinian.idsyaginx 项目中,典型表现是 Drone 前端构建步骤先卡在 pnpm install,修复后又进入 tsc 阶段暴露直接依赖缺失。

这类问题不要只看最后一行失败日志。前端 CI 通常至少包含两个阶段:

  1. 安装依赖:pnpm install
  2. 编译构建:pnpm run build

两个阶段的失败原因完全不同,修复方式也不同。先把阶段分清楚,才能避免把缓存、镜像、TypeScript 和包声明混成一团。

背景与现象

这次问题发生在 Linux amd64 runner 上,前端步骤通过 Docker 运行 Node Alpine 镜像,并挂载共享 pnpm store:

pnpm install --store=/pnpm_store && pnpm run build

第一次失败发生在 pnpm install 阶段。Drone 日志中可以看到 pnpm v11.1.1,并报出:

ERR_PNPM_IGNORED_BUILDS

提示被忽略 build scripts 的依赖包括:

  • @parcel/watcher
  • @swc/core
  • core-js
  • esbuild
  • protobufjs

修复 install 阶段后,第二次失败进入 pnpm run build,由 TypeScript 报出:

Cannot find module '@ant-design/pro-layout'
Cannot find module 'react-router'

这说明第一笔修复是有效的:依赖已经能安装,构建流程继续向前走,才暴露源码与依赖声明之间的真实不一致。

问题一:pnpm 11 忽略依赖构建脚本

发生原因

pnpm 11 对依赖 build scripts 更严格。对于会在 install 阶段执行脚本的包,如果项目没有显式声明允许策略,CI 会阻止这些脚本执行并失败。

这类包常见于前端工具链:

  • @swc/core 需要选择或准备平台相关 native binding。
  • esbuild 需要完成平台二进制准备。
  • @parcel/watcher 带平台 watcher 能力。
  • core-jsprotobufjs 会在安装阶段执行 postinstall。

只提交 pnpm-lock.yaml 不足以表达“这些依赖的 install scripts 可以在 CI 执行”。lockfile 固定的是依赖图和版本,不是 CI 对 build scripts 的信任策略。

为什么会突然出现

这类故障常出现在以下变化之后:

  • CI Node 镜像升级,内置 pnpm 版本随之变化。
  • 项目从旧 pnpm 行为切到 pnpm 10/11 的严格 install 策略。
  • 共享 pnpm store 让本地或历史构建看起来能过,但干净容器中策略不完整。
  • Alpine 镜像触发 musl 平台包选择,使 native optional dependency 更显眼。

修复方式

在前端项目的 pnpm-workspace.yaml 中显式允许这些依赖执行 build scripts:

allowBuilds:
  '@parcel/watcher': true
  '@swc/core': true
  core-js: true
  esbuild: true
  protobufjs: true

如果项目之前写的是 ignoredBuiltDependencies,它表达的是“忽略这些包的构建脚本”,在 pnpm 11 的严格场景下会直接导致 CI 失败。此时应替换为 allowBuilds,而不是清缓存或回退镜像来绕过。

修复后,Drone 日志中应能看到相关脚本执行完成,例如:

.../node_modules/@swc/core postinstall: Done
.../esbuild@0.21.5/node_modules/esbuild postinstall: Done
.../node_modules/@parcel/watcher install: Done

问题二:TypeScript 直接依赖缺失

发生原因

install 阶段修好后,构建继续进入 tsc && vite build。这时 TypeScript 开始检查源码 import,暴露两个依赖声明问题。

第一类是源码直接导入 @ant-design/pro-layout

import { PageHeader } from '@ant-design/pro-layout';

package.json 中只声明了 @ant-design/pro-components,没有声明 @ant-design/pro-layout。即使 lockfile 中因为传递依赖或历史解析已经出现过 @ant-design/pro-layout,只要源码直接 import,就应当把它声明为项目的直接依赖。

第二类是源码从 react-router 导入:

import { useNavigate } from 'react-router';

而项目声明的是 react-router-dom,其它文件也统一从 react-router-dom 导入:

import { useNavigate } from 'react-router-dom';

在 pnpm 的依赖隔离模型下,不能假设可以直接访问传递依赖。react-router 即使被 react-router-dom 依赖,也不代表当前项目可以直接 import 它。

修复方式

对直接依赖缺失,补齐 package.json 与 lockfile importer:

{
  "dependencies": {
    "@ant-design/pro-layout": "7.19.11"
  }
}

pnpm-lock.yaml 的 importer 中也需要出现对应条目:

importers:
  .:
    dependencies:
      '@ant-design/pro-layout':
        specifier: 7.19.11
        version: 7.19.11(...)

对错误导入路径,优先改源码导入,使它与项目已经声明的依赖一致:

import { useNavigate } from 'react-router-dom';

不要为了一个错误 import 额外声明 react-router,除非项目确实有直接使用 react-router 包 API 的设计意图。

推荐排查顺序

前端 CI 失败时,按这个顺序查,通常会快很多:

  1. 先定位失败 step

    看 Drone 中具体失败的是哪个前端应用步骤,例如 adminuiSystemManagerTenantManager

  2. 区分 install 阶段和 build 阶段

    pnpm install 失败通常是包管理、lockfile、build scripts、网络或 registry 问题。pnpm run build 失败通常是 TypeScript、Vite、源码 import、环境变量或构建配置问题。

  3. 确认 Node 镜像和 pnpm 版本

    漂移镜像、latest tag 或构建镜像更新,可能带来 pnpm 行为变化。日志里应打印 node --versionpnpm --version

  4. 对照 package、lockfile 和源码 import

    源码直接 import 的包必须在 package.json 中直接声明。不要依赖传递依赖、历史 hoist 或本地 node_modules 侥幸通过。

  5. 复现最小命令

    本地或容器中优先复现:

    pnpm install --frozen-lockfile
    pnpm run build
    
  6. 一层一层修

    先修 install,再修 typecheck/build。每修一层都复跑,确认失败点是否向前推进。

修复清单

遇到 pnpm 11 + Drone 前端构建失败时,至少核对这些文件:

  • .drone.yml:前端步骤使用的 Node 镜像、pnpm 版本、工作目录、共享 store、是否冻结 lockfile。
  • package.json:源码直接 import 的包是否都是直接依赖。
  • pnpm-lock.yaml:importer 是否包含新增直接依赖,锁文件是否与 package 声明一致。
  • pnpm-workspace.yaml:是否显式声明 allowBuilds,是否仍误用 ignoredBuiltDependencies
  • src/**/*.tssrc/**/*.tsx:是否从未声明的传递依赖中 import。

常用检查命令:

rg -n "from ['\"]react-router['\"]" src
rg -n "@ant-design/pro-layout" src package.json pnpm-lock.yaml
pnpm install --frozen-lockfile
pnpm run build

经验规则

  • CI 构建不能依赖本地 node_modules、历史 pnpm store 或 hoist 副作用。
  • pnpm-lock.yaml 只能保证依赖版本可复现,不能代替 build-script 信任策略。
  • 使用 pnpm 10/11 时,包含 native binding、postinstall 或 install script 的前端工具链应显式维护 allowBuilds
  • 源码直接 import 的包必须进入 package.json 的直接依赖。
  • 不要把“安装失败”和“编译失败”混修。install 失败修好后,出现新的 TypeScript 错误是正常推进,不代表前一笔修复无效。
  • CI 日志里要保留 Node/pnpm 版本、install 输出和 build 输出。没有版本信息时,排查会变成猜镜像。
  • 修复前端 CI 时优先选择最小闭环:确认阶段、修一个原因、复跑、再处理下一层。

与规范的关系

这篇文档是故障复盘和排查经验,不替代正式规范。正式规则和自查入口继续看: