---
date: 2026-05-10
author: Chentao Jia
status: draft
related:
  - .learnings/2026-05-10-gitea-1.26-upgrade-and-act-runner-compat-saga.md
  - PR #280（concurrency 已落地）
  - PR #283（待开 — HostKeyAlgorithms 防御性配置）
---

# CI/CD 工作流审计报告

## 1. 范围

`.gitea/workflows/` 下 7 个 workflow + 触发它们的环境矩阵。**仅审计当前形态、列改造选项**，不直接动 yml；落地按用户勾选拆 PR 实施（`.gitea/workflows/**` 是 CLAUDE.md 标记的高风险路径，必须**单独 PR**）。

## 2. 环境矩阵

| 环境 | 触发 | 主机 | 用户 | 部署路径 | 域名 | 部署脚本 | Schema 同步 |
|---|---|---|---|---|---|---|---|
| **L2 test** | push `develop` | 170.106.161.71（独立 test 机） | `ubuntu` | `/srv/apps/ffworkspace-test` | `https://ffworkspace.test.faradayfuturecn.com` | `deploy.sh test deploy --skip-migrate` | `db push` |
| **L3 UAT (FFAI)** | push `staging` | 43.153.69.73 | `ubuntu` | `/srv/apps/ffworkspace-test` | (UAT 域名) | `deploy.sh uat deploy` | `migrate deploy` |
| **L3 UAT (AIxC)** | push `staging` | 52.234.29.56 | `itadminaixc` | `/srv/apps/aixcworkspace` | (AIxC UAT 域名) | `sg docker -c 'deploy.sh uat deploy'` | `migrate deploy` |
| **L4 Prod (FFAI)** | push `production` | 43.130.6.44 | `srvadmin` | `/srv/apps/ffworkspace` | (Prod 域名) | `deploy.sh production deploy` | `migrate deploy` |
| **L4 Prod (AIxC)** | push `production` | 23.101.202.65 | `itadminaixc` | `/srv/apps/aixcworkspace` | (AIxC Prod 域名) | `sg docker -c 'deploy.sh production deploy'` | `migrate deploy` |

> Test/UAT/Prod 各 push 一个分支即触发部署。UAT 和 Prod 各自把 FFAI + AIxC 两个目标在**单 job 内并发后台 SSH**（节省 ~6min vs 串行）。

## 3. Workflow 一览

| 文件 | 触发 | runs-on | jobs | 关键 secret | 状态 |
|---|---|---|---|---|---|
| [ai-review.yml](.gitea/workflows/ai-review.yml) | PR → develop/staging/production；workflow_dispatch | ubuntu-latest | ai-review | `AI_REVIEW_GITEA_TOKEN` | **dry-run 观察期**（`AI_REVIEW_DRY_RUN=1`） |
| [quality-gates.yml](.gitea/workflows/quality-gates.yml) | PR → develop/staging/production；push develop | ubuntu-latest（多）+ uat-with-docker（backend-integration） | verify-agent-assets / build-check / migration-file-count / contract-check / env-coverage-check / backend-integration | （runner 内置） | **重型 job 仅 base=develop 跑**（`if:` 限定） |
| [deploy-test.yml](.gitea/workflows/deploy-test.yml) | push develop；workflow_dispatch | ubuntu-latest | deploy / post-deploy-health | `FFWORKSPACE_TEST_CI_KEY_170_106_161_71` | concurrency=cancel-in-progress |
| [deploy-uat.yml](.gitea/workflows/deploy-uat.yml) | push staging | ubuntu-latest（deploy）+ uat-with-docker（regression） | deploy / post-deploy-regression | `DEPLOY_SSH_KEY`（共用） | concurrency=cancel-in-progress |
| [deploy-production.yml](.gitea/workflows/deploy-production.yml) | push production | ubuntu-latest | pre-deploy-checks / deploy / post-deploy-smoke | `DEPLOY_SSH_KEY`（共用） | concurrency=cancel-in-progress=**false**（保护已开始的 deploy） |
| [nightly-snapshot-check.yml](.gitea/workflows/nightly-snapshot-check.yml) | workflow_dispatch（schedule 注释） | uat-with-docker | snapshot-data-quality | `SNAPSHOT_S3_URL` / `SNAPSHOT_DECRYPT_KEY` | **未启用**（脱敏快照链路待打通） |
| [weekly-retro.yml](.gitea/workflows/weekly-retro.yml) | schedule cron `3 1 * * 1`（UTC）；workflow_dispatch | ubuntu-latest | weekly-retro | `WEEKLY_RETRO_TOKEN` | schedule 在本 Gitea 实例**未生产验证**（首次手动跑过即可标稳） |

## 4. 已经做对的（不动）

最近几周累积下来的几条好习惯，单独标出避免在改造时被误"优化"掉：

- **concurrency 治理**：3 个 deploy*.yml 都配了 concurrency group（PR #280，Gitea 1.26.1 解锁）。test/uat 用 `cancel-in-progress=true`（节流），prod 用 `false`（不打断已开始的 deploy）。
- **silent fail-open 防御**：[.gitea/workflows/README.md](.gitea/workflows/README.md) 红线 + [quality-gates.yml:108-122](.gitea/workflows/quality-gates.yml#L108-L122) 显式 `git rev-parse --verify origin/$BASE_REF` 校验。
- **共享测试容器 flock 互斥**：`backend-integration` 跟 `post-deploy-regression` 用 `/tmp/ffoa-test-containers.lock` 串行（Gitea 1.25.x 的 `concurrency` 不可靠的绕行方案，详见 [quality-gates.yml:216-230](.gitea/workflows/quality-gates.yml#L216-L230)）。
- **重型 job 限定 base=develop**：避免 PR→staging/production 重跑 L2 已过的 build/contract/integration（节省 ~10-15min/PR）。
- **L0c 复用 backend container**：`backend-integration` Phase 3 跑 L0c 响应快照对比（[quality-gates.yml:285-296](.gitea/workflows/quality-gates.yml#L285-L296)），零增量成本。

## 5. 改造选项（分档）

### A. 必修（learning 已点名 / 已知缺陷）

#### A1. 3 个 deploy*.yml 加 `HostKeyAlgorithms` 防御性 ssh 选项 (= 待开的 PR #283)

**问题**：[2026-05-10 升级 saga learning](.learnings/2026-05-10-gitea-1.26-upgrade-and-act-runner-compat-saga.md) 教训 6 已记录——runner 机 ssh client 默认 offer `sk-*`（FIDO 安全密钥）host key 类型，部分目标 sshd 不支持，negotiate 失败 → ssh `CLOSE_WAIT` 状态进程不退 → CI step 卡死、log 黑洞。当前是 runner 机本地改了 `~/.ssh/config` 临时绕行，跨 runner 不可移植。

**改造**：把 yml 里现有 ssh 调用都补上：

```yaml
ssh -i ~/.ssh/deploy_key \
  -o StrictHostKeyChecking=accept-new \
  -o HostKeyAlgorithms=ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa \
  -o PubkeyAcceptedAlgorithms=ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256,ssh-rsa \
  ubuntu@"$DEPLOY_HOST" bash -s
```

**影响范围**：deploy-test.yml × 2 处、deploy-uat.yml × 2 处、deploy-production.yml × 3 处（含 pre-deploy-checks）。

**验证**：改一处后 push 一个空 commit（仅文档/注释改动），看 deploy job CI log 完整且 step 正常退出（不卡）。

**风险**：低。defensive，向前兼容。

---

### B. 建议（明显改善但不紧急）

#### B1. `deploy-uat.yml` 缺 post-deploy 公网 smoke

**事实**：
- [deploy-test.yml:66-83](.gitea/workflows/deploy-test.yml#L66-L83)：post-deploy-health（公网 curl `/` 200 + `/api/v1/health` healthy + on-server smoke）
- [deploy-production.yml:93-111](.gitea/workflows/deploy-production.yml#L93-L111)：post-deploy-smoke（on-server smoke-test.sh）
- [deploy-uat.yml](.gitea/workflows/deploy-uat.yml)：只有 post-deploy-regression（跑 L1 全量集成测试，**用 ffoa-test 测试容器，跟 UAT 服务本身没关系**）

**问题**：UAT 部署 ssh 退码是 0 但服务实际未起来时（PM2 reload 失败但 deploy.sh 没正确传染退码），post-deploy-regression 不会发现——它跑在 runner 机的 test 容器里。

**改造**：补一个 `post-deploy-uat-smoke` job，模仿 deploy-test 那段公网 curl + on-server smoke。两个目标（FFAI / AIxC）各跑一遍。

**风险**：低。

#### B2. secret 命名一致性

**现状**：
- deploy-test 用 `FFWORKSPACE_TEST_CI_KEY_170_106_161_71`（命名规范：业务 + 用途 + IP，机器换 IP 时需同步重命名）
- deploy-uat / deploy-production 都用 `DEPLOY_SSH_KEY`，同一个 secret 给 4 台不同机不同 user 共用

**风险**：
- secret rotation 困难——一台机泄露要换 4 台机的 authorized_keys
- 看 yml 不知道 secret 对应哪个目标，故障排查多绕一层
- 跟 deploy-test 命名风格不一致

**改造**（可选两档）：
- 轻：保留 `DEPLOY_SSH_KEY` 通用 alias，文档加一句"对应这 4 台机的同一个 key"
- 重：拆 4 个 secret（FFAI_UAT_KEY / AIXC_UAT_KEY / FFAI_PROD_KEY / AIXC_PROD_KEY），yml 显式按目标取 secret

**风险**：中（拆 secret 需要在 4 台机分别生成新 key + Gitea 配 secret + 旧 key 撤销，操作多但可逆）。

#### B3. 显式 `timeout-minutes`

**问题**：当前**没有任何 job 配置 timeout**。Gitea Actions 默认 360 分钟（6 小时）；ssh 进程 CLOSE_WAIT 不退（教训 6 那种情况）会一直占 runner，卡 6 小时才释放。

**改造**：每个 job 按预期时长 ×2 配 timeout。建议初值：

| job | 实测/预期 | 建议 timeout |
|---|---|---|
| ai-review | ≤5min | `10` |
| verify-agent-assets / migration-file-count / env-coverage-check | ≤2min | `5` |
| build-check | 5-15min（含 cache hit/miss） | `25` |
| contract-check | ≤5min | `10` |
| backend-integration | 10-30min（jest 全量） | `45` |
| deploy（任意） | 5-10min | `20` |
| post-deploy-regression | 10-30min | `45` |
| post-deploy-health/smoke | ≤2min | `5` |
| weekly-retro | ≤5min | `10` |
| nightly-snapshot-check | 视脱敏快照大小 | `60` |

**风险**：低。

#### B4. 显式 `permissions:` 字段（最小权限）

**问题**：Gitea Actions 默认 `GITEA_TOKEN` 权限较开放。涉及写操作的 workflow 应显式声明：

- `ai-review.yml`：`pull-requests: write`（评论 PR）+ `statuses: write`（写 PR check status）
- `weekly-retro.yml`：`issues: write`（创建/更新 issue）
- 其他纯读：显式 `contents: read`

**风险**：低。

#### B5. SSH setup block 抽公共

**重复点**：
- [deploy-test.yml:43-46](.gitea/workflows/deploy-test.yml#L43-L46) + [deploy-test.yml:91-95](.gitea/workflows/deploy-test.yml#L91-L95)（×2）
- [deploy-uat.yml:35-39](.gitea/workflows/deploy-uat.yml#L35-L39)（×1，但 ssh-keyscan 是 2 个目标 IP）
- [deploy-production.yml:33-37](.gitea/workflows/deploy-production.yml#L33-L37) + [deploy-production.yml:51-55](.gitea/workflows/deploy-production.yml#L51-L55) + [deploy-production.yml:106-109](.gitea/workflows/deploy-production.yml#L106-L109)（×3）

每处都是同一段 5 行：mkdir + echo key + chmod + ssh-keyscan。如果 A1（HostKeyAlgorithms）落地，每处都要再加一行 ssh -o，重复维护成本翻倍。

**改造**：用 Gitea Actions 的 composite action（`.gitea/actions/setup-deploy-ssh/action.yml`），两行调用：

```yaml
- uses: ./.gitea/actions/setup-deploy-ssh
  with:
    ssh_key: ${{ secrets.DEPLOY_SSH_KEY }}
    hosts: '170.106.161.71'
```

**风险**：低-中（composite action 在 Gitea Actions 上语法支持需先验证；先做 1 个 deploy yml 试点再推广）。

---

### C. 可选（要做策略决策）

#### C1. ai-review 是否退出 dry-run 观察期？

**现状**：[ai-review.yml:63](.gitea/workflows/ai-review.yml#L63) `AI_REVIEW_DRY_RUN: '1'`（只打日志、不写 Gitea）。注释说"观察 1-2 周看 review 质量、调 prompt"。
**决策**：累计了多少 dry-run 输出？质量是否到位？退出观察期 = 删 `AI_REVIEW_DRY_RUN`，可能还要把 ai-review 加进 Gitea PR 的 required checks。

#### C2. `post-deploy-regression` 接 L2 E2E

**现状**：[deploy-uat.yml:86](.gitea/workflows/deploy-uat.yml#L86) 注释 `TODO: L2 E2E 需 Playwright MCP 服务可用；当前先跑 L1 + L1c`。
**决策**：Playwright MCP 在 self-hosted runner 上的方案是否准备好？如未准备好，这条暂时不动。

#### C3. `nightly-snapshot-check` 启用 schedule

**现状**：[nightly-snapshot-check.yml:17-18](.gitea/workflows/nightly-snapshot-check.yml#L17-L18) schedule 注释掉。前置条件未具备：脱敏快照流程 + `snapshot-loader.sh` + 两个 secret。
**决策**：脱敏快照流程是否在路线图上？没规划就先冻结这个 workflow（甚至可考虑先标 `if: false` 或挪到 `_disabled/` 避免 workflow_dispatch 误触发）。

#### C4. `weekly-retro` schedule 端到端验证

**现状**：[weekly-retro.yml:18](.gitea/workflows/weekly-retro.yml#L18) 注释说 schedule trigger 在本 Gitea 实例尚未在生产验证过。`workflow_dispatch` 已可手动验证整链路。
**决策**：是否已用 workflow_dispatch 跑通过一次端到端？跑通后这条标"已验证"，注释可去掉。

#### C5. 文档脱节核对

**现状**：[docs/ops/four-layer-rollout-runbook.md](docs/ops/four-layer-rollout-runbook.md) 描述四层 rollout 流程；[docs/standards/05-development-workflow.md](docs/standards/05-development-workflow.md) + [docs/standards/08-release-management.md](docs/standards/08-release-management.md) 描述分支模型和发布管理。
**未核对**：这些文档跟现行 yml（含 PR #280 concurrency / 测试容器互斥 / 共用 secret 等）是否一致。
**决策**：是否要做一次 doc-vs-yml 对齐审计。

#### C6. quality-gates.yml 跨 PR 的 lint 已被合并掉是否合理？

**现状**：[quality-gates.yml:48-51](.gitea/workflows/quality-gates.yml#L48-L51) 注释"原 lint-and-type-check 合并进 build-check（nest build / next build 自身跑 tsc）"。
**决策**：累计的 PR 红/绿模式里，是否出现过类型错误漏判（`next build` 在某些 lint 规则下不会报）？如否则留着不动。

---

## 6. 推荐落地顺序

按 PR 拆分（每个独立的 yml 改动 = 单独 PR，符合 CLAUDE.md 高风险路径规则）：

| 顺序 | 项 | PR 大小 | 依赖 |
|---|---|---|---|
| 1 | A1（HostKeyAlgorithms） | 小，3 yml × ~7 处 | 无 |
| 2 | B3（timeout-minutes） | 小-中 | 无 |
| 3 | B4（permissions） | 小 | 无 |
| 4 | B1（uat post-deploy smoke） | 小-中 | A1 落地后做更稳（防 ssh 卡） |
| 5 | B5（composite action 抽公共） | 中 | A1 后做（要重写的 SSH 块已稳定） |
| 6 | B2（secret 拆分） | 中（含 4 台机操作） | 用户决策 |
| 后续 | C1-C6 | 各看情况 | 用户决策 |

## 7. 待用户决策

请按上面 A/B/C 列勾选要落地的项。我会按勾选顺序拆 PR、逐项实施。每个 PR 流程：

1. 改 yml
2. push 到 `chore/ci-workflow-audit-XX` 子分支（每项一个）
3. 开 PR、走 CI 验证（A1 那种 ssh 配置改动可在 deploy-test.yml 上构造空 commit 验证）
4. squash merge 到 develop
