# AI Review 启用过程踩到的坑（Gitea + claude CLI + bash/jq）

**日期**: 2026-04-29
**上下文**: 启用 `.gitea/workflows/ai-review.yml` 的 dry-run 观察期，过程中踩了 4 个坑。

---

## 1. claude CLI 必须以 act_runner 用户登录，auth 才能被 runner 读到

**现象**: 直接 `ssh ubuntu@host` 然后跑 `claude` 登录，auth 落在 `/home/ubuntu/.claude/`，但 act_runner 进程读不到（它的 HOME 是 `/var/lib/act_runner`）。

**正确做法**:
```bash
sudo -u act_runner -H -s /bin/bash -c 'cd /var/lib/act_runner && /usr/local/bin/claude'
```
- `-u act_runner` 切到 runner 用户
- `-H` 把 HOME 设成 runner 的 HOME（关键）
- `-s /bin/bash -c` 给个 shell 容器

**验证 auth 落对位置**:
```bash
sudo ls /var/lib/act_runner/.claude/.credentials.json
sudo -u act_runner -H bash -c '/usr/local/bin/claude -p "reply with just OK"'
# 应该看到 OK
```

**适用范围**: 所有需要让 act_runner 进程访问的工具（不只是 claude），都要装 + 登录到 act_runner 的 HOME。

---

## 2. Gitea 保留 `GITEA_*` 前缀的 secret 名，不让用

**现象**: PUT `/api/v1/repos/.../actions/secrets/GITEA_API_TOKEN_FOR_AI` 返回 400 `"invalid variable or secret name"`。

**原因**: Gitea Actions 把 `GITEA_*` 和 `GITHUB_*` 留作内置 env var 命名空间，secret 名禁用这两个前缀。

**修法**: 改名 `AI_REVIEW_GITEA_TOKEN` 即可。workflow yml 里 env 字段用任意名字消费 secret 都行（`env: GITEA_API_TOKEN: ${{ secrets.AI_REVIEW_GITEA_TOKEN }}`），脚本不用改。

---

## 3. Gitea API token 不能创建新 token，必须 basic auth

**现象**: `POST /api/v1/users/{name}/tokens` 用 `Authorization: token xxx` 调用返回 403 `"doer should be the site admin or be same as the contextUser"`，即使 token 属于同名用户。

**原因**: Gitea 安全策略——禁止 token 自我繁殖（防止 token 泄露后无限刷新权限）。创建 token 必须用密码 basic auth。

**实战取舍**: AI 没有 root 密码所以创建不了新 token。要么：
- 让用户去 web UI 手动建（http://host/-/user/settings/applications）
- 复用现有 token（接受 audit trail 不分用户）—— v1 这么干就行

---

## 4. bash 双引号 + jq 字符串里的反引号转义会爆

**现象**: ai-review-runner.sh 跑出来报：
```
jq: error: Invalid escape at line 1, column 4 (while parsing '"\`"')
```

**根因**: 多层转义。原代码：
```bash
jq -r ".findings[] | ... \"(\\\`\(.file)...\\\`)\" ..."
```
bash 解析双引号字符串时把 `\\\`` 处理成 `\` + 反引号，jq 拿到 `"\` "` 视为非法转义（jq 字符串只认 `\n \t \" \\ \uXXXX` 这几个标准 JSON 转义，没有 `\``）。

**修法**: jq 表达式用单引号包（bash 不解析里面的内容），变量用 `--arg`：
```bash
jq -r --arg sev "$sev" '
  .findings[]
  | select(.severity == $sev)
  | "- **[\(.category)]** \(if .file then "(`\(.file)\(if .line then ":\(.line)" else "" end)`) " else "" end)\(.message)"
'
```
反引号在 jq 字符串里就是普通字符，不用转义。

**通用经验**: shell 嵌套 jq 时，**永远用单引号包 jq 表达式，参数走 --arg / --argjson**，避免双层转义地狱。

---

## 5. Gitea Actions same-repo PR 用 PR HEAD 的 workflow 定义

**意外发现**: PR #168 修改 `.gitea/workflows/ai-review.yml` 启用 trigger，**这个 PR 自己就触发了 ai-review**（base 上的 develop 此时还是 workflow_dispatch-only 版本）。

**含义**: 同仓库 PR 改 workflow 文件能在自身 PR 上立刻生效，不用先合并再开新 PR 测试。**但** fork PR 出于安全不会跑 fork 端的 workflow——这个特性只对 same-repo PR 有效。

**用途**: 以后改 workflow 可以直接在 PR 里测，省一轮 merge → 新 PR 的循环。

---

## 6. 仓库有破损的 submodule 配置，所有 checkout 后置步骤都报警

**现象**: 每个用 `actions/checkout@v4` 的 job 在 Post Checkout 阶段报：
```
fatal: No url found for submodule path 'apps/outline' in .gitmodules
::warning::The process '/usr/bin/git' failed with exit code 128
```

**根因**: 仓库历史里 `apps/outline` 注册成 submodule（gitlink 模式），但 `.gitmodules` 没对应配置。可能是早期 vendoring 残留。

**影响**: 仅警告，不影响 job 成功。但每次 CI 日志都难看。

**根治**: 需要决定 `apps/outline` 是要真做 submodule（补 .gitmodules）还是当普通目录（`git rm --cached apps/outline` 删 gitlink）。本次 session 没改。
