返回博客

npm install 静默改了 lockfile,下次 release 的 git checkout 就挂了

发版脚本 SSH 进自托管 Windows 构建机,走 git fetch && git checkout <sha>,结果:

error: Your local changes to the following files would be overwritten by checkout:
    desktop/sidecar/package-lock.json
Please commit your changes or stash them before you switch branches.
Aborting
[git checkout bd9555a...] exited with 1
Error: ssh-win[builder] exited with 1
✗ release FAILED at step "desktop-windows" (exit 1)

机器上根本没人手工改过 lockfile。git diff 打开一看,"hasInstallScript": true 多了一行——这是上次发版跑 npm install 时 npm 自己写进去的。

现象

  • builder 上 git status -s 长期显示 M package-lock.json,多次发版都没消失
  • release 日志里真正的报错被埋在 SSH stdout 深处,外层只看到 ssh exit 1,新手容易以为是网络或权限问题
  • 偶发还有 Cargo.tomlCargo.lock 也被标脏

根因

自托管构建机(不同于 GitHub Actions 每次干净 container)的 git working tree 跨多次发版共享。上次发版时:

  1. npm install 在 package.json 新增了 postinstall 字段后,自动把 "hasInstallScript": true 写入 package-lock.json
  2. cargo build 偶尔更新 Cargo.lock 的 transitive dep 条目
  3. 这些改动没有 commit,留在 working tree 里

等到下次发版,git checkout <new-sha> 发现本地有修改就拒绝覆盖——这是 git 的保护机制,正确行为,但在无人值守的 CI 场景下变成了拦路虎。

坑:npm install 会静默回写 lockfile

npm install 不是幂等的。只要 package.json 有变化(新增 script、更新 dep 范围),npm 就会更新 package-lock.json写入磁盘。在容器 CI 里这不是问题,每次干净开始;在自托管长驻构建机上,这份改动永久留在 working tree,直到下次 checkout 时才爆炸。

正解

立即救火

SSH 进 builder,手动还原脏文件:

git restore desktop/sidecar/package-lock.json
git restore desktop/src-tauri/Cargo.toml
# 顺手清调试残留
git clean -fd
git status -s   # 应为空

长期修法:checkout 前无条件清场(方案 A,最推荐)

在 release 脚本 SSH 块里,checkout 前加两行:

// 修复前
await sshWin(`
  cd '${win.repoPath}'
  git fetch origin
  git checkout ${sha}          // ❌ 遇到 dirty working tree 就挂
`)
 
// 修复后
await sshWin(`
  cd '${win.repoPath}'
  git fetch origin
  git reset --hard origin/main  // 丢弃所有本地 modification
  git clean -fd                 // 删 untracked 文件(调试残留)
  git checkout ${sha}           // ✅ 干净 working tree,checkout 必成
`)

自托管 builder 上永远不应该有手工改动,强制 reset 是最稳策略。

搭配:用 npm ci 替代 npm install

# release pipeline 里替换
npm ci   # 严格按 lockfile 安装,不回写,lockfile 与 package.json 不一致时直接报错

npm ci 不会修改 package-lock.json,从源头切断静默回写的路径。

正解:checkout 前 reset --hard + clean -fd,安装用 npm ci

两步组合:release 脚本在 git checkout 前无条件 git reset --hard && git clean -fd(builder 不存手工改动,强制覆盖零风险);依赖安装换 npm ci(严格模式,不回写 lockfile)。连跑 3 次发版验证 git status -s 全部为空即为修复。

注意:哪些构建工具会静默改 tracked 文件

| 工具 | 可能改动的文件 | |------|---------------| | npm install | package-lock.json | | pnpm install | pnpm-lock.yaml | | cargo build | Cargo.lock、偶发 Cargo.toml | | tauri build | tauri.conf.json(version 字段) | | 自定义 postinstall | 任意文件 |

这个问题只影响自托管持久化 builder,GitHub Actions / GitLab Runner 用 image: 的容器模式每次从干净环境起步,天然不受影响。Tauri 跨平台打包(Windows MSI 签名只能在 Windows 上做)必须走 self-hosted builder,绕不开这条路径。

一句话外卖

自托管构建机的 git working tree 是跨次发版持久的,每次 checkout 前必须 reset --hard + clean -fd,别让上次构建的副产物拦住下次发版。