本地 npm install 成功但 CI 失败:复现锁文件漂移
修改 package.json 后,本机能启动,干净的 CI 却在 npm ci 报错。先检查清单和锁文件是否同步提交。npm install 可以更新依赖解析结果,npm ci 会检查锁文件与清单并使用锁定结果安装。
1. 离线模拟漏提交锁文件
把代码保存为 npm_lock_lab.py,确保 npm 在 PATH 中,再运行 python npm_lock_lab.py。脚本创建两个纯本地依赖包并生成 tarball,测试过程中不访问 registry。
import json
import os
from pathlib import Path
import subprocess
import tarfile
from tempfile import TemporaryDirectory
npm = "npm.cmd" if os.name == "nt" else "npm"
with TemporaryDirectory() as tmp:
root = Path(tmp)
for name in ("alpha", "beta"):
source = root / "package"
source.mkdir(exist_ok=True)
(source / "package.json").write_text(json.dumps({
"name": name, "version": "1.0.0",
}), encoding="utf-8")
with tarfile.open(root / f"{name}.tgz", "w:gz") as archive:
archive.add(source, arcname="package")
def write_manifest(names):
(root / "package.json").write_text(json.dumps({
"name": "lock-lab", "version": "1.0.0",
"dependencies": {name: f"file:{name}.tgz" for name in names},
}), encoding="utf-8")
def run(*args):
return subprocess.run(
[npm, *args, "--offline", "--ignore-scripts", "--no-audit",
"--no-fund", "--cache", str(root / "npm-cache")],
cwd=root, capture_output=True, text=True, timeout=20,
)
write_manifest(["alpha"])
run("install", "--package-lock-only").check_returncode()
run("ci").check_returncode()
print("initial lock: ci passed")
write_manifest(["alpha", "beta"])
result = run("ci")
assert result.returncode != 0 and "beta" in result.stderr, result.stderr
print("stale lock: ci rejected missing beta")
run("install", "--package-lock-only").check_returncode()
lock = root / "package-lock.json"
original = lock.read_bytes()
run("ci").check_returncode()
assert lock.read_bytes() == original
assert (root / "node_modules" / "beta" / "package.json").is_file()
print("updated lock: ci installed beta; lock unchanged")
本地实测输出如下。
initial lock: ci passed
stale lock: ci rejected missing beta
updated lock: ci installed beta; lock unchanged
2. 依次检查三份输入
| 检查项 | 常见问题 |
|---|---|
| package.json 和 package-lock.json | PR 只修改其中一个,或合并冲突后保留了错误的锁文件 |
| Node.js、npm 和项目 .npmrc | 本机和 CI 用了不同工具链或影响依赖树的参数 |
| 安装发生的目录 | monorepo 在子目录运行,读取的清单和锁文件不是预期的一对 |
确实要修改依赖时,在项目约定的工具链下更新锁文件,审查间接依赖变化,把两份文件一起提交,再从干净目录跑 npm ci。生成锁文件时如果使用了影响依赖树的参数,例如 legacy-peer-deps,CI 安装也要应用相同配置。
小结
让 CI 直接报告清单与锁文件的偏差,再在开发侧更新和评审锁文件。还原问题时保留 npm 版本、运行目录和项目配置。
数据来源与检索时间
检索时间 2026-11-13。
| 来源 | URL | 对应内容 |
|---|---|---|
| npm CLI | https://docs.npmjs.com/cli/v11/commands/npm-ci/ | 锁文件要求、干净安装与参数一致性 |
核验说明
- npm 11.17.0 实测本地包初次安装、漂移失败与更新后安装。私有 registry、平台依赖和生命周期脚本属于实际项目环境。