侧边栏

pnpm在CI中的严格模式

发布于 | 分类于 前端/前端工程|本文包含AIGC内容

最近在进行项目的底层库升级,顺带将前端项目的包管理工具从yarn切换为了pnpm。在 CI 部署的时候遇到了一个问题:当 pnpm-lock.yamlpackage.json 里面的包版本不一致时,CI 会直接报错退出。但是在本地执行 pnpm install 却一切正常,依赖照样能装上。

难道是pnpm在本地和 CI安装时会有一些差异?

这个问题最终在pnpm的文档中找到了答案,这其实是 pnpm 的一个精心设计,本文将记录一下这个问题。

问题现象

先来看看具体的表现。

在 CI 环境中,执行 pnpm install 会报类似这样的错误:

text
ERR_PNPM_LOCKFILE_MISMATCH
pnpm-lock.yaml is not up to date with package.json

然后安装失败,整个流程中断。

但在本地执行同样的命令:

bash
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 的判断逻辑本质上等价于:

ts
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 的行为等价于:

bash
pnpm install --frozen-lockfile

--frozen-lockfile 参数的含义是:

  • 不允许修改 pnpm-lock.yaml
  • 如果 package.json 与 lockfile 不一致,直接报错
  • 严格依赖 lockfile 安装

这样做的好处是,可以保证同一个 commit 在任意时间、任意机器上安装出的依赖树完全一致。

如果 CI 允许修改 lockfile,那就麻烦了:

  • 依赖版本可能悄悄变化
  • 构建结果不可预测
  • 生产事故风险极高

相比之下,本地执行 pnpm install 的默认行为是:

  1. 检查 package.json
  2. 发现与 pnpm-lock.yaml 不一致
  3. 自动修复 lockfile
  4. 继续安装

可以看见,本地安装是"自愈型安装",这是为了提升开发体验,而不是为了部署安全。

常见的 lockfile 不一致原因

那么,lockfile 不一致通常是怎么产生的呢?我总结了几个常见的原因:

  • 修改了 package.json,但没跑 pnpm install
  • 合并分支时 lockfile 冲突未正确处理
  • 不同 pnpm 版本生成的 lockfile 格式不同
  • 只提交了 package.json,忘记提交 pnpm-lock.yaml
  • 手动修改了依赖版本号

正确的解决方案

遇到这个问题,正确的做法是:

  1. 本地执行 pnpm install
  2. 确保 package.jsonpnpm-lock.yaml 保持一致
  3. 两个文件一起提交
  4. CI 正常通过

有些人可能会想到一些"取巧"的办法,比如在 CI 中关闭 frozen:

bash
pnpm install --no-frozen-lockfile

或者直接删除 lockfile:

bash
rm pnpm-lock.yaml
pnpm install

强烈不推荐这么做。前者会让 CI 偷偷改 lockfile,构建不可复现;后者会让依赖版本完全失控,生产事故高发。

本地自检技巧

如果想在本地提前发现 CI 问题,可以模拟 CI 环境:

bash
CI=true pnpm install

或者更明确地使用:

bash
pnpm install --frozen-lockfile

这是一个非常好的本地自检习惯,可以在提交代码前就发现问题。

小结

pnpm 通过 CI 环境变量区分本地与 CI,并在 CI 中默认启用 --frozen-lockfile

  • 本地:开发友好,自动修复
  • CI:部署安全,严格校验
  • corepack:确保行为一致,而不是制造问题

这是 pnpm 的设计哲学和 CI 的最佳实践,而不是异常行为。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。