---
date: 2026-05-09
type: tooling
tags: [gitea, ci, api, debugging]
---

# Gitea `/actions/runs` 三个非显而易见的 API 行为

写 `scripts/ops/weekly-review.py` 拉 CI 数据时一次踩中三个，调试 ~15 min 才定位。

## 三个坑

### 1. 时间字段不是 `created_at`，是 `started_at`

普通资源（PR、issue、commit）都有 `created_at`。`/actions/runs` 的 run 对象**没有 `created_at` 字段**，时间在 `started_at`（启动时刻）和 `completed_at`（结束时刻）。

代码里习惯性写 `r.get("created_at")` 会拿到 `None`，所有窗口过滤直接 silent 0 命中——脚本不报错，只是输出 `Actions: 0 runs`。

```python
# 错（静默 0 命中）
ts = _safe_parse(r.get("created_at"))

# 对
ts = _safe_parse(r.get("started_at")) or _safe_parse(r.get("completed_at"))
```

### 2. 状态在 `conclusion`，不是 `status`

`status` 字段对所有完成的 run **永远是 `"completed"`**，没有 `success`/`failure` 这种值。真实结果在 `conclusion`：`success` / `failure` / `cancelled` / `skipped`。

```python
# 错（永远没有 failure）
if r.get("status") == "failure": ...

# 对
if (r.get("conclusion") or "").lower() == "failure": ...
```

这跟 GitHub Actions API 的字段语义一致，但 Gitea 文档不强调，脚本作者很容易先按直觉写。

### 3. `limit` 参数被忽略，单次返回全量

PR/issue/branches 端点都遵守 `?limit=50&page=N` 分页协议。`/actions/runs` **不遵守**——`?limit=20` 也会一次返回全部 1360 条 run（实测）。

后果：

- 用现成的 paginate helper 调它会**重复拿同一份全量**，max_pages 次循环，浪费带宽
- 数据量大的仓库一次 HTTP 体积可能很大，要单调用 + 客户端过滤

```python
# 错（paginate 会循环空转）
raw = paginate(api, "/actions/runs", {})

# 对（单调用，客户端按时间过滤）
code, txt = api.get("/actions/runs", {"limit": 50})
data = json.loads(txt)
raw = data.get("workflow_runs", [])
in_window = [r for r in raw if _safe_parse(r.get("started_at")) >= cutoff]
```

## 调试线索

发现的关键时刻：脚本输出 `Actions: 0 runs, 0 failed`，但**同一 token 同一 endpoint** 用 `curl` 能拿到 1360 条 run。这种"curl 通、脚本通但数据空"组合永远先打 `print(raw[0].keys())` 看真实字段名，比读文档快。

## 推荐

写 Gitea `/actions/*` 相关脚本时三件套先确认：

1. 用 `curl ... | jq '.workflow_runs[0] | keys'` 把字段名打印一遍
2. 拉一份全量看 `Counter(r.get('status') for r in runs)` 和 `Counter(r.get('conclusion') for r in runs)` 的真实分布
3. 不要复用通用 paginate；为这个端点单独写一次性调用

涉及代码：`scripts/ops/weekly-review.py:collect_actions`。
