---
date: 2026-05-11
tags: [ai-review, ci, bash, pipefail]
---

# ai-review-runner.sh 首次 review（无旧评论）pipefail 把 grep -oE 的 no-match 引爆

## 现象

`.gitea/workflows/ai-review.yml` 在 PR #320（ai-review 转正后第一个 PR）上挂掉。Job log：

```
[info] PR: ... → develop (diff 11912 bytes)
[info] 查找上一轮 state header（增量 review 去重）
##[error]Process completed with exit code 1.
```

无任何报错文本，但 step exit=1。

## 根因

`scripts/ops/ai-review-runner.sh` 头部 `set -euo pipefail`。第 67-72 行去拉上一轮 review 的 state header：

```bash
PREV_STATE_B64=$(echo "$PREV_COMMENTS_JSON" | jq -r '...' \
  | grep -oE '<!-- ai-review-state-b64: [A-Za-z0-9+/=]+ -->' | tail -1 \
  | sed -E 's/.../\1/')
```

首次 review 时 PR 上没有任何旧 ai-review 评论 → grep 无匹配 → exit 1 → 因 `pipefail` 整条管道 fail → 因 `set -e` 命令替换 `$()` 子 shell 退 1 → 外层赋值上下文继承 → 脚本退 1。

为什么之前没暴露：

- 此逻辑 PR #318 新加，那之前没有 state header
- PR #318 之后 ai-review 全在 dry-run，required check 不阻断；step failure 不计入合并门禁
- 转正后第一个 PR（#320）首次撞上、首次硬阻断

## 解决

把无匹配会 exit 1 的 `grep -oE` 包成 `{ grep ... || true; }`，no-match 时返回 0 不毒化 pipefail：

```bash
| { grep -oE '<!-- ai-review-state-b64: [A-Za-z0-9+/=]+ -->' || true; } | tail -1 \
```

## 复用经验

`set -euo pipefail` 脚本里凡是依赖 grep / awk 过滤"找不到也合理"的输入，必须显式吞掉非零退出码：

- `cmd | { grep PAT || true; } | next` ✅
- `cmd | grep PAT | next` ❌（no-match → pipefail → die）
- `cmd | (grep PAT || true) | next` ✅（子 shell 也行）
- `cmd | grep -c PAT` ✅（-c 改语义，永远输出数字 exit 0）

排查方法：脚本静默 exit 1 + 最后一行 echo 后面跟着的是含 grep/awk/jq 的管道 → 99% 是 pipefail 没接住 no-match。

## 关联

- PR #318（feat(ai-review): #259 工作流优化套件）引入 state header 逻辑
- 工单 #171 转正 ai-review 时（PR #320）首次暴露
