---
date: 2026-05-06
tags: [ci, gitea-actions, deploy, staging, uat]
---

# production → staging 回灌会重新触发一次 UAT 部署

## 现象

合并 `staging → production`（PR）之后，预期只跑 `deploy-production.yml`，
实际会额外触发一次 `deploy-uat.yml`。

例：Gitea Actions run #1217（workflow=`deploy-uat.yml`, event=`push`,
branch=`staging`, commit title=`Merge branch 'production' into staging`）。
相邻 run 1218/1219 是 PR #234 (`production → staging`) 的 quality-gates / ai-review。

## 根因

`staging → production` 合并完成后，团队（或自动化）会再开一个反向 PR
**`production → staging`** 把 merge commit 同步回 staging，让两边历史对齐
（squash merge 后 hash 不一致，需要回灌）。

回灌完成 = 一次 push 到 staging。`deploy-uat.yml` 的触发条件是：

```yaml
on:
  push:
    branches:
      - staging
```

→ 命中触发，UAT 重跑一次（~6min）。staging 实际内容没变化，纯浪费。

## 关键约定

- 看到"staging→production 合并后 UAT 又跑了"不是 bug，是**两步流程的第二步副作用**：
  1. PR `staging → production` → 触发 `deploy-production.yml`
  2. PR `production → staging`（回灌） → push staging → 触发 `deploy-uat.yml`
- 排查 Gitea Actions run 时，**URL 上的数字是 `run_number` 不是 `id`**：
  - API `GET /api/v1/repos/{owner}/{repo}/actions/runs?limit=N` 返回的 `run_number` 才能对上 URL
  - `tasks/{id}` 路径 404 是因为 id ≠ run_number
- Gitea web 页面 `/actions/runs/{n}` curl 直接抓会拿到 "Not found"（需要 session cookie）；
  改用 API `actions/runs?limit=` 列表过滤 `run_number` 是稳的方案。

## 处理（已实施 2026-05-06）

`deploy-uat.yml` job 级加 `if:` 跳过回灌触发的 push：

```yaml
if: ${{ !contains(github.event.head_commit.message, 'production'' into staging')
        && !contains(github.event.head_commit.message, 'from production into staging') }}
```

匹配两种 merge commit 标题：
- 手动 `git merge`：`Merge branch 'production' into staging`
- Gitea PR merge：`Merge pull request '#N' from <user>/production into staging`

`post-deploy-regression` 通过 `needs: deploy` 自动跟随跳过。

## 边界情况

- 如果未来出现 squash merge `production → staging`，commit 标题不再含上述模式 →
  guard 失效，UAT 仍会重跑。届时可改用 SHA/树指纹比对。
- 如果有人手动改 merge commit message，guard 也会失效。用 PR 自动 merge 比手动 git
  merge 安全。
