本地 build 过,CI 挂——AI 改完最常见的坑

经典场景:AI 装了依赖没提 lockfile,或依赖了本地 Node 版本。

你在本地跑 npm run build 一切正常,push 完发现 Vercel / Netlify / GitHub Actions 红了,错误是 Cannot find module 'some-lib'SyntaxError: Unexpected tokenerror TS2307。这是 AI 辅助编码里最高频的一类”环境漂移”问题——Claude Code 或 Cursor 在你本地装了个新依赖、用了你本地有但 CI 没有的 Node 特性、或改动了某个被你的全局 git 配置忽略掉的文件。这篇拆 6 个最常见原因,给你一套 5 分钟内能定位并修复的流程。

常见原因

按命中率从高到低排序。

1. Lockfile 没 commit(最常见)

Agent 跑了 npm install some-lib 装了新依赖,package.json 进了 commit,但 package-lock.json / pnpm-lock.yaml / yarn.lock 没进。CI 上跑 npm ci 时报 “lock file out of sync”,或者跑 npm install 装了不同版本导致 API 不一致。

npm error code EUSAGE
npm error `npm ci` can only install packages when your package.json and
npm error package-lock.json or npm-shrinkwrap.json are in sync.
npm error Missing: some-lib@2.3.0 from lock file

如何判断git log --name-only -1 看最近 commit,如果 package.json 变了但 lockfile 没变,就是这个。

2. 本地 Node 版本和 CI 不同

你本地是 Node 22,CI 默认跑 Node 18。Agent 用了 Array.prototype.toSorted()(Node 20+)或 ESM 顶层 await(Node 14.8+),本地能跑,CI 上 SyntaxError

SyntaxError: Unexpected token 'await'
    at wrapSafe (node:internal/modules/cjs/loader:1378:18)

如何判断:本地跑 node -v 和 CI log 里第一行的 Node 版本对比。差一个大版本就高度可疑。

3. AI 用了本地有但 CI 没装的全局工具

Agent 在 build 脚本里加了 tsxesbuildvite,假定全局可用。你本地有,CI 没装到 devDependencies 里,CI 报 command not found: tsx

如何判断:在干净的 docker 里跑一遍 docker run --rm -v $(pwd):/app -w /app node:18 npm ci && npm run build,最快能复现 CI 失败。

4. 文件名大小写差异

macOS 和 Windows 文件系统默认不区分大小写,Linux(CI)严格区分。Agent 在 macOS 上写了 import Button from './button',但文件实际叫 Button.tsx,本地能解析,CI 报 Cannot find module './button'

如何判断git ls-files | grep -i 文件名 看实际文件名大小写,和 import 对比。

5. 环境变量本地有、CI 没有

Agent 加了 process.env.SOME_API_KEY,你本地 .env 里有这个值,CI 没设。build 时为 undefined,注入到 client 代码后运行时报 undefined is not a function。Astro / Next.js 在 build 期就会读 env,CI 直接挂。

如何判断:grep 最近 diff 里所有 process.env.XXX,对比 CI / Vercel / Netlify 的 env 设置面板。

6. AI 残留了 console.log 或开发期 mock

Agent 调试时在 next.config.jsastro.config.mjs 里塞了 mock 数据 / fixture 路径,本地有这些文件,CI 上没有,build 报 ENOENT: no such file or directory

如何判断git diff main -- '*.config.*' 看所有 config 文件改动。

最短修复路径

按收益排序。先做 step 1-2 通常能修 70%。

Step 1:本地用和 CI 一样的 Node 版本 + npm ci 复现

最快的第一步:

# 看 CI log 第一行确认 CI 用的 Node 版本,比如 18.19.0
nvm install 18.19.0
nvm use 18.19.0
rm -rf node_modules
npm ci          # 注意:是 ci 不是 install
npm run build

如果本地能复现,下一步就好定位了。如果本地依然过,说明问题在 env vars 或 CI 镜像差异,跳到 Step 4。

Step 2:检查 lockfile 是否在最近 commit 里

git log --name-only -5 | grep -E '(package-lock|pnpm-lock|yarn.lock)'

如果最近 5 个 commit 都没动 lockfile 但 package.json 动了,立刻补提:

npm install                                  # 重新生成 lockfile
git add package.json package-lock.json
git commit -m "chore: sync lockfile after AI dependency add"

Step 3:在 package.json 和 CI 里固定 Node 版本

package.json

{
  "engines": {
    "node": ">=18.19.0 <19.0.0",
    "npm": ">=10.0.0"
  }
}

GitHub Actions:

- uses: actions/setup-node@v4
  with:
    node-version: '18.19.0'
    cache: 'npm'

Vercel:项目设置 → General → Node.js Version 选具体版本。Netlify:在 netlify.toml 里加 [build.environment] NODE_VERSION = "18.19.0"

加一个 .nvmrc 文件让本地自动切:

echo "18.19.0" > .nvmrc

Step 4:环境变量对账

把代码里所有 process.env.X 列出来,和部署平台对账:

grep -rEo 'process\.env\.[A-Z_]+' src/ | sort -u

对比 Vercel/Netlify env 面板。差一个就在面板里加上,然后触发 redeploy。注意:客户端可见的变量在 Next.js 里是 NEXT_PUBLIC_*,Astro 里是 PUBLIC_*,Vite 里是 VITE_*,前缀写错也会编译时为 undefined。

Step 5:在干净的 docker 里跑一遍

终极复现:

docker run --rm -it -v "$(pwd):/app" -w /app node:18.19.0 sh -c "
  npm ci &&
  npm run build
"

这模拟 CI 完全干净的 node_modules + 严格 Node 版本。如果这里都过,剩下的差异基本就是 CI 平台的 env vars 或缓存。

预防建议

  • package.jsonengines.nvmrc、CI workflow、部署平台四处都固定相同的 Node 版本
  • 本地始终用 npm ci(而不是 npm install)跑,这样 lockfile 漂移会立刻报错
  • 在 CLAUDE.md / .cursorrules 里写:“装新依赖必须同时 commit lockfile,不要单 commit package.json”
  • pre-commit hook 加一条:package.json 改了就检查 lockfile 是否也改了
  • import 路径强制小写或和文件名严格一致——ESLint 加 import/no-unresolved 规则配合大小写检查
  • .env.example 提交进 repo,CI 启动时校验所有必需 env vars 都已设置

相关阅读

标签: #AI 编程 #排查 #排查