---
date: 2026-04-28
title: 四层环境（local/dev/uat/production）落地踩坑沉淀
tags: [deploy, ci, prisma, nginx, gitea, ports, learnings]
---

# 四层环境落地踩坑沉淀

把单层 CI/CD 扩到 L1/L2/L3/L4，dev 与 UAT 共用 43.153.69.73 服务器，期间踩了 6 个坑。

---

## ERR-1：Prisma migration 在共用机器全新 dev 库的 P3009 失败

### 表象
新 dev 库跑 `prisma migrate deploy` 卡在 `20260320000731_cleanup_deprecated_models`：
```
Error: P3009
ERROR: current transaction is aborted, commands ignored until end of transaction block
```

### 根因
那条迁移做 `ALTER TYPE Evaluation360Status ...` 等清理，**前提是早期迁移已经创建了 evaluation_360 表**。但 `_prisma_migrations` 表里只有 0311 之后的几条记录——前 60+ 条迁移**根本没跑过**。空数据库 + 缺前置 schema → `ALTER TABLE` 找不到表 → 整个事务 abort → 后续所有语句 ignored。

实际是 `prisma migrate deploy` 跟"新 dev 环境"配合不当——deploy.sh 的 `dev` env 沿用了 UAT 的迁移流程。

### 修复
**dev 环境用 `prisma db push`，不用 migration history**：
```bash
# 1. 干净重置 dev 库
sudo docker stop ffws-dev-postgres && sudo docker rm ffws-dev-postgres
sudo docker volume rm ffws-dev_postgres_data
DEPLOY_ENV=dev bash scripts/deploy/deploy.sh up

# 2. 用 db push 一次性同步 schema（不走 migration history）
cd backend && npm run db:push

# 3. seed
npm run db:seed

# 4. deploy.sh 跳过 migrate 步骤
DEPLOY_ENV=dev bash scripts/deploy/deploy.sh deploy --skip-migrate
```

CI 同步实践：`deploy-dev.yml` 改成 `git pull → db:push → deploy.sh deploy --skip-migrate`，dev 永远跟 schema 头部同步，不再被历史 migration 绊住。

### 预防
- **dev 环境的设计原则**：数据可丢、schema 跟 prisma/schema 头部对齐 → 用 `db push`
- **uat/production**：数据要保留 → 必须用 `migrate deploy`
- 这两条流程在 `deploy.sh` 里要分开，dev 不要复用 prod 的迁移路径

---

## ERR-2：Prisma `migrate resolve` 找不到 DATABASE_URL

### 表象
```
$ npx prisma migrate resolve --rolled-back <name>
Error: PrismaConfigEnvError: Missing required environment variable: DATABASE_URL
```

### 根因
项目用了 `prisma.config.js`，prisma 的 CLI 默认不再自动读取 `.env`（与 prisma 6.x 行为变化有关）。但 npm scripts 里都用 `dotenv -e .env -- prisma ...` 包了一层。直接 `npx prisma migrate resolve` 绕过了 dotenv。

### 修复
```bash
npx dotenv -e .env -- npx prisma migrate resolve --rolled-back <name>
```
或直接用 npm script：`npm run prisma:migrate:resolve -- --rolled-back <name>`。

### 预防
所有 prisma 子命令在本项目都必须经 `dotenv` 包裹。可加 npm script 别名：
```json
"prisma:migrate:resolve": "dotenv -e .env -- prisma migrate resolve"
```

---

## ERR-3：dev 与 UAT 共用机器的端口大撞车

### 表象
首次 `deploy.sh dev up` 报：
```
Error response from daemon: Bind for 0.0.0.0:7081 failed: port is already allocated
```

### 根因
- `deploy.sh` 的 dev env 默认端口配在 3000-3091（设计给开发者本地机器用）
- UAT 在共用机器上已占用 3000(grafana) / 7012(uat-grafana) / 7081(uat-redis-commander) / 7090-7097(uat-minio + ragflow) 等
- `.env.dev` 我从 `.env.uat` 复制改基础端口（POSTGRES/BACKEND/FRONTEND/REDIS）但漏改了次要端口（GRAFANA_PORT / PROMETHEUS_PORT / RAGFLOW_* / TEI_PORT 等），全部继承 UAT 的 7xxx → 全撞

### 修复
1. 改 `scripts/deploy/deploy.sh` 的 `[dev_*_port]` 全部从 3xxx 移到 5xxx 段
2. `sed` 把 `.env.dev` 里所有 `7xxx → 5xxx` 平移
3. 重新 deploy

最终方案：dev 用 5000-5099，UAT 用 7000-7099，production 用 6000-6099 — 三段不重叠。

### 预防
共用服务器跑两套 stack 时，**.env 里要 audit 所有 PORT= 字段**，不能只改名字带 PORT 的几个，还要查所有形如 `*_API_PORT` `*_HOST_PORT` 的变体。

---

## ERR-4：Gitea Actions 默认 `pipefail` 让 grep 无匹配 fail 整个 step

### 表象
新 CI job `migration-file-count` 跑：
```bash
NEW_MIGRATIONS=$(... | grep -E '^backend/prisma/migrations/...' | awk -F/ '{print $4}' | sort -u | wc -l)
echo "New migration directories: $NEW_MIGRATIONS"
```
本地跑 0 个 migration 时 `wc -l` 返回 0，但 CI 上整个 step exit 1，连 echo 都没打印。

### 根因
**Gitea Actions 跟 GitHub Actions 一致，bash 默认带 `-e -o pipefail`**：grep 在没匹配时返回 1，pipefail 让整个管道返回 1，set -e 让整个 step 失败——echo 那行根本到不了。

### 修复
```bash
CHANGED=$(git diff ... 2>/dev/null || true)
MIG_FILES=$(printf '%s\n' "$CHANGED" | grep -E '...' || true)
if [ -z "$MIG_FILES" ]; then
  COUNT=0
else
  COUNT=$(printf '%s\n' "$MIG_FILES" | awk -F/ '{print $4}' | sort -u | wc -l)
fi
```
关键模式：每个可能 fail 的 grep 后面都加 `|| true`，分支处理"无结果"情况。

### 预防
CI yaml 里写 bash pipeline 默认要假设 `-eo pipefail`，**不能依赖 GNU/Linux shell 默认的"宽容"语义**。本地测试通过 ≠ CI 通过。

---

## ERR-5：CI runner 上 `nest build` / `tsc --noEmit` OOM

### 表象
```
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
exit status 134
```

### 根因
nest build 和 frontend `tsc --noEmit` 在大型 monorepo 上需要 ~2GB+ heap，Node 默认上限 ~1.7GB，OOM。

### 修复
job 级别加 `env: NODE_OPTIONS: '--max-old-space-size=4096'`：
```yaml
build-check:
  runs-on: ubuntu-latest
  env:
    NODE_OPTIONS: '--max-old-space-size=4096'
```
**注意** lint 和 build 是两个 job，要分别加。

### 预防
任何在 CI 跑 nest/next/tsc 的项目都默认提到 4GB。Heap 限制和 runner 物理内存（通常 7GB）无关——Node 自身保守。

---

## ERR-6：Gitea Actions CI runner 上 `prisma generate` 没跑导致 type 不全

### 表象
```
src/core/compute/automation/automation.controller.ts:18:10 - error TS2305:
  Module '"@prisma/client"' has no exported member 'AutomationTaskType'.
```
本地 `tsc` 通过，CI 上 fail——本地 node_modules 有项目特定的 prisma client 类型，CI 没有。

### 根因
`@prisma/client` 是个 stub，真正的类型由 `prisma generate` 生成（基于项目 schema）写到 `node_modules/.prisma/client/`。CI 上每次 `npm install` 后默认会触发 postinstall 跑 generate，但本项目可能跳过或使用了 `--ignore-scripts`。

### 修复
在 CI 的 install 之后显式跑：
```yaml
- name: Generate Prisma Client
  run: cd backend && DATABASE_URL='postgresql://dummy:dummy@localhost:5432/dummy' npx prisma generate
```
DATABASE_URL 用 dummy 即可——`prisma generate` 不连数据库，只读 schema。

### 预防
任何用 prisma 的 CI job 在 `tsc` 或 `build` 之前必须显式跑 `prisma generate`，不要依赖 postinstall。

---

## ERR-7：`scripts/ops/gitea-pr-merge.py` 不兼容自定义 SSH 端口

### 表象
```
$ python3 scripts/ops/gitea-pr-merge.py --pr 164
PR #164 读取失败: HTTP 404
{"errors":["user redirect does not exist [name: 2222]"]}
```

### 根因
脚本 line 78 用正则提取 owner/repo：
```python
m = re.search(rf"{re.escape(GITEA_HOST)}[:/]([^/]+/[^/.]+)", url)
```
当 git remote 是 `ssh://git@43.130.59.228:2222/FFAIWorkspace/workspace.git` 时，`:2222/FFAIWorkspace` 被匹配，owner 变成 `2222`，repo 变成 `FFAIWorkspace`，404。

### 绕行
直接 curl Gitea API 手动跑 dance（read PR → relax 保护规则 → sleep 30s → merge → restore）。

### 根因修复（待提 PR）
```python
m = re.search(rf"{re.escape(GITEA_HOST)}(?::\d+)?[:/]([^/]+/[^/.]+)", url)
```

### 预防
项目 Gitea 用自定义 SSH 端口（2222 而非 22），未来写解析 git URL 的脚本要先考虑 URL 多形态：
- `https://host/o/r`、`http://host/o/r`
- `ssh://git@host/o/r.git`、`ssh://git@host:port/o/r.git`
- `git@host:o/r.git`

---

## 元经验：rebase 大跨度 PR 的策略

PR feature/workflow-optimization 落后 develop 76 个 commit。直接 force-push 触发冲突全表显示像核爆，真正手解只 4 个文件（AGENTS.md / CLAUDE.md / .agents/skills/README.md / deploy-ops/SKILL.md，分发副本 sync 脚本重生）。

**遇到大量冲突时先 audit 共同改动文件**：
```bash
comm -12 <(git diff --name-only origin/develop...HEAD | sort) \
         <(git diff --name-only HEAD...origin/develop | sort)
```
如果数量小（< 10），直接 rebase 解；如果 > 30，考虑 cherry-pick 关键 commit 重新做。
