6月4日 23:10

npm在CI/CD中的最佳实践:缓存策略、npm ci和安全审计

本地 npm install 跑得好好的,推到 CI 就各种失败——超时、依赖不一致、缓存不命中、构建产物丢失。这篇文章把 npm 在 CI/CD 里的常见坑和最佳实践都过一遍,以 GitHub Actions 为主,其他 CI 工具的思路一样。

npm ci vs npm install:CI 里永远用 ci

bash
# ❌ 错误:CI 里用 npm install npm install # ✅ 正确:CI 里用 npm ci npm ci

两者区别:

npm installnpm ci
依赖来源参考 package-lock,但可能更新它严格按 package-lock,不一致则报错
node_modules增量安装,不清除先删 node_modules 再装
速度较慢(要解析依赖树)更快(直接按 lock 文件装)
确定性不保证(可能偷偷升级)保证(锁文件必须和 package.json 一致)

CI 环境的核心要求是可重现——同样的代码两次构建结果必须一样。npm install 可能悄悄修改 lock 文件,npm ci 不允许。

前提npm ci 要求 package-lock.json 必须存在且和 package.json 一致。如果不一致直接报错,不会偷偷修——这正是 CI 需要的行为。

缓存策略

依赖安装是 CI 里最耗时的步骤之一。缓存 node_modules 或 npm 全局缓存可以节省 80% 以上的安装时间。

GitHub Actions

yaml
- uses: actions/setup-node@v4 with: node-version: 20 cache: npm # 自动缓存 ~/.npm 目录 - run: npm ci # 从缓存安装,速度极快

cache: npm 是最简单的方案——actions/setup-node 自动根据 package-lock.json 的 hash 生成缓存 key,lock 文件变了缓存自动失效。

手动配置缓存(更精细控制):

yaml
- name: Cache node modules uses: actions/cache@v3 with: path: | ~/.npm node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - run: npm ci

缓存 node_modules 还是 ~/.npm?

策略优点缺点
缓存 ~/.npm缓存体积小,命中率高还需要跑 npm ci(但只从本地缓存读)
缓存 node_modules跳过安装步骤缓存体积大,不同 job 间可能不兼容
都缓存最快缓存体积最大

推荐:只缓存 ~/.npmnpm ci 配合本地缓存的安装速度已经够快(通常 5-15 秒),而缓存 node_modules 的缓存体积大且跨 job 兼容性差。

GitLab CI

yaml
cache: key: files: - package-lock.json paths: - .npm/ install: script: - npm ci --cache .npm --prefer-offline

--cache .npm 指定缓存目录,--prefer-offline 优先从缓存读,缓存没有再从网络下载。

环境变量

CI 环境下几个关键的环境变量:

bash
# 跳过 npm fund 和 npm audit 输出(CI 里不需要) export npm_config_fund=false export npm_config_audit=false # 不生成 package-lock.json(npm ci 不需要) export npm_config_package_lock=false # 设置日志级别(减少 CI 日志噪音) export npm_config_loglevel=warn

在 GitHub Actions 里:

yaml
env: npm_config_fund: false npm_config_audit: false

安全审计集成

在 CI 里自动检测安全漏洞:

yaml
- name: Security audit run: npm audit --audit-level=high continue-on-error: true # 先不阻断,只报告 - name: Block on critical run: | critical=$(npm audit --json | jq '.metadata.vulnerabilities.critical // 0') if [ "$critical" -gt 0 ]; then echo "::error::Found $critical critical vulnerabilities" exit 1 fi

--audit-level=high 只在发现 high 或 critical 漏洞时返回非零退出码。lowmoderate 不阻断构建但会输出警告。

完整的 GitHub Actions 工作流

yaml
name: CI on: push: branches: [main] pull_request: branches: [main] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: npm - run: npm ci - run: npm run lint - run: npm run build - run: npm test - name: Upload coverage if: matrix.node-version == 20 uses: actions/upload-artifact@v4 with: name: coverage path: coverage/

常见坑

package-lock.json 和 package.json 不一致

本地 npm install 后忘了提交 lock 文件,CI 里 npm ci 就会报错。解决:每次 npm install 后检查 lock 文件是否有变化,有就提交。

yaml
- name: Check lock file run: | npm install --package-lock-only git diff --exit-code package-lock.json

monorepo 下缓存 key 不对

monorepo 有多个 package-lock.json,缓存 key 只用了根目录的。解决:hashFiles 支持通配符:

yaml
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

CI 里的 Node 版本和本地不一致

本地用 Node 20,CI 默认跑 Node 18。npm ci 可能在 Node 18 下装的依赖和 Node 20 不兼容。解决:用 matrix.node-version 跑多个版本,或锁定 CI 的 Node 版本。

标签:NPM