前端 CI 构建故障排查与修复经验
本文沉淀一次真实前端 CI 故障的排查经验。相关问题先后出现在 feinian.ids 与 yaginx 项目中,典型表现是 Drone 前端构建步骤先卡在 pnpm install,修复后又进入 tsc 阶段暴露直接依赖缺失。
这类问题不要只看最后一行失败日志。前端 CI 通常至少包含两个阶段:
- 安装依赖:
pnpm install - 编译构建:
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/corecore-jsesbuildprotobufjs
修复 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-js、protobufjs会在安装阶段执行 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 失败时,按这个顺序查,通常会快很多:
先定位失败 step
看 Drone 中具体失败的是哪个前端应用步骤,例如
adminui、SystemManager或TenantManager。区分 install 阶段和 build 阶段
pnpm install失败通常是包管理、lockfile、build scripts、网络或 registry 问题。pnpm run build失败通常是 TypeScript、Vite、源码 import、环境变量或构建配置问题。确认 Node 镜像和 pnpm 版本
漂移镜像、
latesttag 或构建镜像更新,可能带来 pnpm 行为变化。日志里应打印node --version和pnpm --version。对照 package、lockfile 和源码 import
源码直接 import 的包必须在
package.json中直接声明。不要依赖传递依赖、历史 hoist 或本地node_modules侥幸通过。复现最小命令
本地或容器中优先复现:
pnpm install --frozen-lockfile pnpm run build一层一层修
先修 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/**/*.ts、src/**/*.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 时优先选择最小闭环:确认阶段、修一个原因、复跑、再处理下一层。
与规范的关系
这篇文档是故障复盘和排查经验,不替代正式规范。正式规则和自查入口继续看: