---
date: 2026-05-06
tags: [ops, production, deploy, git, ci]
status: blocker-resolved-pending-cleanup
---

# 生产 deploy 失败：服务器 git tree 有 hand patch 挡住 git pull

## 现象

PR #234 (staging → production) merge 后触发 `deploy-production.yml`（run #1230），
`deploy.sh production deploy` 在 `[1/8] 更新代码` 阶段失败：

```
[FFAI] error: Your local changes to the following files would be overwritten by merge:
[FFAI]   scripts/ops/env-check.sh
[FFAI] Please commit your changes or stash them before you merge.
[FFAI] Aborting
```

## 根因

PRD 服务器 `/srv/apps/ffworkspace` 的 git working tree 不干净，三种不同来源
（**很重要：不全是人改的，区分对待**）：

1. **`frontend/tsconfig.json`（M）— Next.js build 自动改写，框架行为不是人为**
   - deploy.sh 用零停机部署：`NEXT_BUILD_DIR=.next.tmp npm run build` → `mv .next.tmp .next`
   - **Next.js 在 build 时会自动改写 tsconfig.json**，向 `include` 加入
     `.next.tmp/types/**/*.ts` 等条目，并用自己的 JSON serializer 重写整个文件
     （single-line array → multi-line array，看起来像 prettier 格式化）
   - 这是 Next 的 well-known 行为，每次 build 都触发；生产 git tree 自首次部署
     起就一直是脏的，只是平时不挡 pull
   - 当上游 PR 真的改了 `frontend/tsconfig.json`（path alias / lib 等）才显式冲突

2. **`scripts/ops/env-check.sh`（M）— 4/29 手工调试**
   - mtime 2026-04-29 07:19，配套 `.bak.20260429-071937`
   - 改动：`pg_isready` → `nc -z`（推测：生产没装 postgresql-client）
   - 真违反铁律，但意图无害，错在没回流 PR

3. **`backend/scripts/bulk-set-ec-preferences.ts` / `resolve-ec-preferences.ts`（??）— 4/22 ad-hoc 数据脚本**
   - mtime 2026-04-22 03:37-03:38，scp 上来一次性跑过的出勤数据修复
   - 含 `--dry-run` 和 `--execute` 开关，说明真在 PRD 跑过写操作
   - **比 #1 #2 严重得多**：直接对生产数据库做了非 PR 的写操作

`git pull --merge` 因 #1 + #2 冲突域 abort，deploy.sh `set -e` 中止。

违反 CLAUDE.md 「生产只读铁律」(5a)。

## 影响范围

`git pull` 失败 ≠ 部署部分完成；**完全没动 pm2/build/DB**。生产仍是合并前状态。
本次同时进行的 `.env.pro` AUDIT_HMAC_SECRET 轮换也**未生效**（pm2 没 reload，
进程内存里还是旧值）。

## 修复方案（按风险递增）

1. **存档 + 丢弃**：
   ```bash
   git diff > ~/prd-handpatch-$(date +%Y%m%d).diff   # 安全网
   git checkout -- <file1> <file2>                    # 丢弃 modified
   ```
   **⚠️ 教训：丢弃前必须验证 git 版本能在生产实际跑通**——否则丢的是"长得丑
   但有用"的 hand patch，老 bug 立刻回来。本次实际踩过：
   `scripts/ops/env-check.sh` 4/29 hand patch 把 `pg_isready` 换成 `nc -z`
   （quoting 写错了，看起来像废代码），实际是因为生产 host 没装
   postgresql-client，git 版本的 `pg_isready` 直接 command not found。
   丢弃后下一次 deploy 的 `pre-deploy-checks` 立刻 fail。

2. **回流为 hotfix PR**：把 hand patch 内容（修正 quoting 等）正式过 PR 合到
   production，再 deploy。最干净但最慢。

3. **stash + pull + pop**：试图保留 hand patch 跨 deploy，但 stash pop 也可能
   产生冲突，且默认隐藏 hand patch 这件事不解决。

## 丢弃 hand patch 前必做的验证

对每个要 checkout 掉的 modified 文件：
1. 看 git 版本里关键命令在生产 host 是否存在（`which <cmd>` 或 `<cmd> --version`）
2. 看 git 版本是否依赖 hand patch 同步存在的某个 sibling 文件
3. 看 hand patch 是不是修了某个 quoting / 路径 bug —— 即使写错也是修一半，
   不要丢
4. 优先级：`hand-patch 意图正确但实现差` → 修对，回流 PR；`hand-patch 完全无关` → 丢

## 预防（按文件类型分别处理）

### 防 Next.js 自动改写 tsconfig.json（框架副作用）

最干净：deploy.sh 在 `[1/8] 更新代码` **之前**加一步无害的 reset：
```bash
git checkout -- frontend/tsconfig.json 2>/dev/null || true
```
理由：tsconfig 是 Next 自己会重写的文件，无须保留生产侧改动；提前 reset 让
`git pull` 一定能 fast-forward。

次优：把 tsconfig.json 加入 `.git/info/exclude`（不污染 production 分支内容），
让 git 长期忽略本地 diff。

### 防人为 hand patch（铁律执行问题）

- **CI 部署前置 check**：`deploy.sh [0/8]` 阶段加 `git status --porcelain` 非空
  fail-fast，明确列出哪些文件被 hand-patched
- **生产 chown 收紧**：常规 srvadmin shell 不能直接写仓库 working tree
- **调试发现的 fix 立刻提 PR**，不允许长期保留生产改动

### 防 ad-hoc 数据脚本

- 数据修复必须走 staging 验证后再到 PRD
- 临时脚本入仓做成 `scripts/ops/` 的 ops 工具，参数化、可审计
- PRD 上禁止 scp + 跑

## Gitea rerun API 不可用

本次部署修复后想触发 run rerun，发现以下端点全部 404：
- `POST /api/v1/repos/{o}/{r}/actions/runs/{id}/rerun`
- `POST /api/v1/repos/{o}/{r}/actions/runs/{id}/rerun-failed`
- `POST /api/v1/repos/{o}/{r}/actions/jobs/{id}/rerun`
- `POST /api/v1/repos/{o}/{r}/actions/runs/{id}/cancel`

当前部署的 Gitea 版本（http://43.130.59.228）没暴露 rerun API。备选：
- Gitea Web UI 上的 Rerun 按钮（最稳）
- 推空 commit 触发新 run（受分支保护影响）
- SSH 服务器直接跑 deploy.sh（绕 CI 审计，仅紧急情况用）

未来需要 rerun 类自动化时，优先 web UI 而非 API。

## 排查模板（下次再遇到生产 git tree 脏）

按文件三类区分，**不要一刀切**：

```bash
# 1. 看每个文件的 mtime 找时间线索
ssh prd 'cd <repo> && stat -c "%y  %n" <files>'
# 2. 看是不是框架自动改写（.next/.next.tmp/dist 引用都是嫌疑）
git diff <file>  # 看 diff 内容形态
# 3. 看 SSH 登录配套时间，定位人为操作来源
ssh prd 'sudo grep "Accepted" /var/log/auth.log | tail -50'
# 4. 分类处理：
#    - 框架副作用 → 直接 checkout，加防护到 deploy.sh
#    - 人为调试 patch → 评估丢弃 vs 回流为 PR
#    - ad-hoc 数据脚本 → 立刻 audit 数据库实际状态，回查影响
```

## 排查方法

1. 看 deploy job 日志找 `error:` 或 `Aborting`
2. SSH 到目标服务器 `git status --short` 看 modified / untracked
3. `git diff <file>` 评估每个 hand patch 的风险
4. 决定丢弃 / 回流 / 临时存档