Jenkins Vue 流水线接入存量 yarn 项目:依赖安装连环失败与产物目录不匹配的排查

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.tsimport { 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 treeelement-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 ] 判断不成立,直接跳过归档,镜像阶段没有产物。

三、根因分析

一锅粥,但拆开其实就三类:

  1. 业务仓库只有 yarn.lock v1,没有 pnpm-lock.yaml / package-lock.json:pnpm 在 CI 默认冻结锁文件直接拒绝,npm 虽然没锁也能装,但被下面两个问题挡住。
  2. 业务依赖声明不干净:代码 import 了没声明的 js-base64(yarn hoist 掩盖);element-plus 用了远古 beta 版本,peer 声明跟实际 vue 版本对不上(npm 7+ 严格校验会拒绝)。
  3. 业务构建产物目录不合约定: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 不规范的问题,与其推业务改代码,不如流水线支持自定义产物目录。stageVueBuildenv.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,因为 createVueDockerfileADD 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 install 5m31s + 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 就不存在了,没有回退逻辑会直接跳过归档,镜像空包。

七、参考资料

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇