pnpm在CI中的严格模式
最近在进行项目的底层库升级,顺带将前端项目的包管理工具从yarn切换为了pnpm。在 CI 部署的时候遇到了一个问题:当 pnpm-lock.yaml 跟 package.json 里面的包版本不一致时,CI 会直接报错退出。但是在本地执行 pnpm install 却一切正常,依赖照样能装上。
难道是pnpm在本地和 CI安装时会有一些差异?
这个问题最终在pnpm的文档中找到了答案,这其实是 pnpm 的一个精心设计,本文将记录一下这个问题。
问题现象
先来看看具体的表现。
在 CI 环境中,执行 pnpm install 会报类似这样的错误:
ERR_PNPM_LOCKFILE_MISMATCH
pnpm-lock.yaml is not up to date with package.json然后安装失败,整个流程中断。
但在本地执行同样的命令:
pnpm install却什么事都没有,lockfile 被自动更新,依赖正常安装。看起来一切正常。
这就很奇怪了,明明是同一个项目,为什么本地和 CI 的行为完全不一样?
答案揭晓
先说结论:这不是 bug,而是 pnpm 的设计。
简单来说就是:
本地 pnpm 是"修复模式",CI 中 pnpm 是"校验模式"。
在本地,pnpm 发现 lockfile 和 package.json 不一致时,会自动更新 lockfile,然后继续安装。这是为了提升开发体验,让你不用每次都手动处理这些琐碎的事情。
但在 CI 环境中,pnpm 会变得非常严格:发现不一致就直接报错退出,不会尝试修复。
为什么要这么做呢?这是为了保证可复现构建(Reproducible Builds)。想象一下,如果 CI 也像本地一样自动修复 lockfile,那么同一个 commit 在不同时间构建,可能会因为依赖版本变化而产生不同的结果。这在生产环境中是非常危险的。
pnpm 如何判断 CI 环境
那么问题来了,pnpm 是怎么知道自己运行在 CI 环境中的呢?
答案很简单:环境变量 CI。
根据文档描述
pnpm 的判断逻辑本质上等价于:
exports.isCI = !!(
env.CI || // Travis CI, CircleCI, Cirrus CI, GitLab CI, Appveyor, CodeShip, dsari
env.CONTINUOUS_INTEGRATION || // Travis CI, Cirrus CI
env.BUILD_NUMBER || // Jenkins, TeamCity
env.RUN_ID || // TaskCluster, dsari
exports.name ||
false
)只要满足下面两个条件:
CI环境变量存在- 且值不是
false
pnpm 就认为当前是 CI 环境。
这也解释了为什么几乎所有 CI 平台都会触发这个行为,因为主流 CI 平台都会自动注入 CI=true:
| CI 平台 | 是否设置 CI 变量 |
|---|---|
| GitHub Actions | ✅ |
| GitLab CI | ✅ |
| Jenkins | ✅ |
| CircleCI | ✅ |
| Travis CI | ✅ |
这已经是事实上的行业标准了,由于我们项目构建是在Jenkins上进行的,因此就会进入CI模式。
CI 模式下的行为差异
在 CI 环境中,pnpm 的行为等价于:
pnpm install --frozen-lockfile--frozen-lockfile 参数的含义是:
- 不允许修改
pnpm-lock.yaml - 如果
package.json与 lockfile 不一致,直接报错 - 严格依赖 lockfile 安装
这样做的好处是,可以保证同一个 commit 在任意时间、任意机器上安装出的依赖树完全一致。
如果 CI 允许修改 lockfile,那就麻烦了:
- 依赖版本可能悄悄变化
- 构建结果不可预测
- 生产事故风险极高
相比之下,本地执行 pnpm install 的默认行为是:
- 检查
package.json - 发现与
pnpm-lock.yaml不一致 - 自动修复 lockfile
- 继续安装
可以看见,本地安装是"自愈型安装",这是为了提升开发体验,而不是为了部署安全。
常见的 lockfile 不一致原因
那么,lockfile 不一致通常是怎么产生的呢?我总结了几个常见的原因:
- 修改了
package.json,但没跑pnpm install - 合并分支时 lockfile 冲突未正确处理
- 不同 pnpm 版本生成的 lockfile 格式不同
- 只提交了
package.json,忘记提交pnpm-lock.yaml - 手动修改了依赖版本号
正确的解决方案
遇到这个问题,正确的做法是:
- 本地执行
pnpm install - 确保
package.json和pnpm-lock.yaml保持一致 - 两个文件一起提交
- CI 正常通过
有些人可能会想到一些"取巧"的办法,比如在 CI 中关闭 frozen:
pnpm install --no-frozen-lockfile或者直接删除 lockfile:
rm pnpm-lock.yaml
pnpm install强烈不推荐这么做。前者会让 CI 偷偷改 lockfile,构建不可复现;后者会让依赖版本完全失控,生产事故高发。
本地自检技巧
如果想在本地提前发现 CI 问题,可以模拟 CI 环境:
CI=true pnpm install或者更明确地使用:
pnpm install --frozen-lockfile这是一个非常好的本地自检习惯,可以在提交代码前就发现问题。
小结
pnpm 通过 CI 环境变量区分本地与 CI,并在 CI 中默认启用 --frozen-lockfile。
- 本地:开发友好,自动修复
- CI:部署安全,严格校验
- corepack:确保行为一致,而不是制造问题
这是 pnpm 的设计哲学和 CI 的最佳实践,而不是异常行为。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
