Jenkins Vue 流水线接入存量 yarn 项目:依赖安装连环失败与产物目录不匹配的排查
记录时间:2026-09-04
环境:Jenkins 2.568.1(K8s 动态 Slave) / node 容器gzeport-node-pnpm:{20|22}/ 项目dyck-vue-gzeport-gmet-ui(关贸e通前端,存量 yarn.lock v1 项目)
一、问题现象
接入关贸e通前端 gzeport-gmet-ui 时,连续几轮构建全部失败,且每次换包管理器、错的还都不一样:
| 构建 | 包管理器 | 报错 | 阶段 |
|---|---|---|---|
| #3 | pnpm | ERR_PNPM_OUTDATED_LOCKFILE(pnpm-lock.yaml 与 package.json 不同步) |
依赖安装 |
| #4 | npm | Cannot read properties of null (reading 'matches') |
依赖安装 |
| #7 | pnpm | Rollup failed to resolve import "js-base64" |
生产构建 |
| #8 | npm | ERESOLVE unable to resolve dependency tree |
依赖安装 |
| #14 | yarn | 依赖安装/构建全通过,但产物在 gmet/ 目录,流水线打包 dist 时找不到目录 |
打包 |
一眼看全是流水线在报错,我的第一反应也是流水线有问题。逐个拆完才发现,根因基本都在业务仓库侧,流水线只是把业务欠的账全部暴露出来了。
二、排查过程
2.1 第一轮:pnpm 报锁文件过期(#3)
报错 ERR_PNPM_OUTDATED_LOCKFILE,说 pnpm-lock.yaml 与 package.json 不一致,有个依赖 pdfjs-dist@4.8.69 被删了。我去 GitLab 拉仓库看,根目录根本没有 pnpm-lock.yaml,只有 yarn.lock。当时就觉得奇怪,没有锁文件怎么会报”过期”?
查 pnpm 官方文档才明白:pnpm 在 CI 环境(检测到 CI/BUILD_NUMBER 等变量)默认开启 --frozen-lockfile。没有锁文件直接报 ERR_PNPM_NO_LOCKFILE;如果工作区里残留了上次构建生成的旧 pnpm-lock.yaml,就报 OUTDATED_LOCKFILE。我们工作区是持久化 PVC,上一轮构建生成的 pnpm-lock.yaml 留在原地,跟新的 package.json 对不上,就撞上了。
注意:pnpm 10+ 在 CI 下默认冻结锁文件,等价于”要求业务必须先提交 pnpm-lock.yaml”。业务仓库只有 yarn.lock,第一步就卡死。
2.2 第二轮:npm 解析崩溃(#4)
换 npm 跑,报 Cannot read properties of null (reading 'matches'),npm 装都没装完就崩。这个错眼熟,搜了下是 npm 的 arborist(依赖解析器)在解析结构不对的 node_modules 时崩的。
回头看工作区:上次 pnpm 装出来的 node_modules 是符号链接结构(node_modules/.modules.yaml 标记),npm 的平铺解析器不认这种结构,读着读着读到 null 就崩了。pnpm 装的东西 npm 没法增量复用,留着必崩。
2.3 第三轮:js-base64 找不到(#7)
pnpm 加上 --no-frozen-lockfile 后安装终于通了,1074 个包装完,结果构建阶段报 Rollup failed to resolve import "js-base64"。查代码:src/utils/index.ts 里 import { Base64 } from 'js-base64',但 package.json 的 dependencies/devDependencies 都没有 js-base64。
再翻 yarn.lock,js-base64 在里面,但是传递依赖(被别的包装进来的)。yarn v1 会把传递依赖摊平到顶层(hoist),所以业务本地一直”碰巧能跑”;pnpm 严格按 package.json 声明隔离,顶层不装没声明的东西,就炸了。这是 yarn 项目转 pnpm 最经典的坑。
2.4 第四轮:npm peer 校验太严(#8)
再换 npm,ERESOLVE unable to resolve dependency tree:element-plus@1.0.2-beta.71 声明 peer 依赖 vue@3.1.x,项目实际装 vue@3.2.47。npm 7+ 默认严格校验 peer,直接拒绝安装;yarn v1 时代从不校验 peer,业务本地根本没感觉。
2.5 第五轮:产物目录对不上(#14)
技术部给项目配了 yarn,yarn install + build:uat 全过了,构建产物也出来了,但我看工作区:产物在 gmet/ 目录(vite.config.ts 里 outDir: 'gmet'),而流水线固定打包 dist,[ -d dist ] 判断不成立,直接跳过归档,镜像阶段没有产物。
三、根因分析
一锅粥,但拆开其实就三类:
- 业务仓库只有 yarn.lock v1,没有 pnpm-lock.yaml / package-lock.json:pnpm 在 CI 默认冻结锁文件直接拒绝,npm 虽然没锁也能装,但被下面两个问题挡住。
- 业务依赖声明不干净:代码 import 了没声明的 js-base64(yarn hoist 掩盖);element-plus 用了远古 beta 版本,peer 声明跟实际 vue 版本对不上(npm 7+ 严格校验会拒绝)。
- 业务构建产物目录不合约定:vite
outDir写死成gmet,而流水线按全仓约定打包dist。
流水线只是忠实执行,把业务仓库的这三笔欠账挨个暴露出来。yarn 时代这几个问题全被 yarn 的宽松语义盖住了,换严格语义的 pnpm/npm 就现原形。
四、解决方案
4.1 流水线侧:三类包管理器的安装兜底
改 vars/stageVueBuild.groovy(前端构建已从 vueHelmDevopsByk8s 拆出成独立 stage 函数),按包管理器分别兜底:
| 包管理器 | 安装命令 | 兜底点 |
|---|---|---|
| pnpm | pnpm install --no-frozen-lockfile --dangerously-allow-all-builds |
无锁自动生成、锁过期自动重算 |
| npm | npm install --legacy-peer-deps |
复刻 yarn v1 宽容 peer 语义,跳过 ERESOLVE |
| yarn | command -v yarn || npm i -g yarn@1.22.22; yarn install |
镜像无 yarn 时兜底装 v1 |
另外 npm 安装前加了一步残留清理:
# 容器 sh 是 dash,字符串比较用 = 不用 ==
if [ "${pm}" = "npm" ] && [ -e node_modules/.modules.yaml ]; then rm -rf node_modules; fi
识别 pnpm 残留(.modules.yaml 标记)清掉再装,避免 arborist 崩溃。
4.2 流水线侧:产物目录可配置(map.put outDir)
业务 outDir 不规范的问题,与其推业务改代码,不如流水线支持自定义产物目录。stageVueBuild 读 env.outDir(业务 Jenkinsfile 用 map.put("outDir","gmet") 声明),默认 dist;打包前统一规范:
# 产物目录保底:优先用配置的 outDir,未找到回退 dist,都没有才跳过归档
if [ -d "${outDir}" ]; then
[ "${outDir}" = "dist" ] || mv "${outDir}" dist
elif [ -d dist ]; then
echo "配置的产物目录 ${outDir} 不存在,回退使用默认 dist 目录"
fi
if [ -d dist ]; then
tar -zcf dist-${IMAGE_TAR}.tar.gz dist
else
echo "No ${outDir}/dist directory, skipping artifact archive."
fi
注意:tar 包顶层目录必须规范成
dist,因为createVueDockerfile的ADD xxx.tar.gz /usr/share/nginx/html/解压路径和 chart 的 nginx root 都是按 dist 配套的。不规范化,镜像里就多了层目录。
4.3 业务侧:转技术部
- 代码 import 的依赖必须在 package.json 声明(js-base64 这类被 hoist 掩盖的,逐个补齐);
- 老项目 peer 声明陈旧(element-plus beta 声明 vue 3.1.x),要么升级要么接受
--legacy-peer-deps语义; - vite
outDir保持约定dist(或交给流水线 outDir 参数适配)。
五、验证
- 流水线兜底生效:
pnpm install无锁自动生成 pnpm-lock.yaml(#7,1074 包 1m28s);yarn 分支全链路(#14,yarn install5m31s +build:uat成功,3387 模块编译完成); - outDir 三场景本地 shell 验证:配置目录存在 →
mv规范化打包;配置目录缺失但 dist 在 → 回退提示 + 打包;两者都没有 → 跳过归档; - #14 后镜像阶段打通,
mv gmet dist+ tar 归档路径正常,镜像可产出。
六、注意事项
- pnpm 在 CI 环境默认
--frozen-lockfile,业务没提交 pnpm-lock.yaml 会直接拒绝安装,不是”生成不了”,是 pnpm 不给生成的机会; - yarn v1 兜底装 1.22.22,不走 corepack:corepack 默认激活 yarn 4(berry),不认 v1 锁文件,装了也白装;
- npm 的 hoist 语义和 pnpm 的严格隔离是两码事:yarn/npm 时代”碰巧能跑”的未声明依赖,换 pnpm 必炸;
- 产物目录判断要有兜底:业务某天把 outDir 改回 dist,配置的 gmet 就不存在了,没有回退逻辑会直接跳过归档,镜像空包。