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

# Gitea Actions URL 里的 run 编号是 display number，不是 API id

## 现象

用户给的 URL：

```
http://43.130.59.228/FFAIWorkspace/workspace/actions/runs/1329/jobs/5
```

直觉操作是把 `1329` 当 API id：

```bash
curl -H "Authorization: token $GITEA_API_TOKEN" \
  "http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/actions/runs/1329"
# 返回的是 display_number=956 的另一个 run（API id=1329）！
```

调查内容跟用户说的完全对不上 —— display number 1329 实际对应 **API id=1756**，而 API id=1329 对应的 display number 是 956（一次几十天前的成功 run）。

## 根因

Gitea Actions 维护**两套独立编号**：

- **`id`**：API 内部主键，全局递增
- **`run_number`** / **display number**：仓库范围内 PR/push 触发递增

URL 里出现的是 `run_number`，但 REST API 端点 `/actions/runs/{id}` 接受的是 `id`。两套编号在仓库内**完全不重合**（API id 总是远大于 run_number），所以混用不会报错，会静默命中另一个 run。

## 正确做法

无 "by run_number" 的查询接口，必须先列出来做映射：

```bash
GITEA_TOKEN="$GITEA_API_TOKEN"
TARGET_RUN_NUMBER=1329

# list 最近 N 个 run，找 display number 对应的 API id
curl -sS -H "Authorization: token $GITEA_TOKEN" \
  "http://43.130.59.228/api/v1/repos/FFAIWorkspace/workspace/actions/runs?limit=30" \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
target = $TARGET_RUN_NUMBER
for r in d.get('workflow_runs', d.get('runs', [])):
    if r.get('run_number') == target:
        print(f\"API id = {r['id']}\")
        break
"
```

拿到真实 API id 后再用 `/actions/runs/{api_id}` 和 `/actions/runs/{api_id}/jobs` / `/actions/jobs/{job_id}/logs`。

## 验证陷阱

最坑的是 list 接口返回的 `html_url` 字段也是 display number 形式（`/actions/runs/956/...`），但底层 `id` 是 API id。看 list 输出时如果只看 html_url，会以为"用户给的 URL 跟我查的是同一个 run"，实际不是。

**自检**：拉到 run 数据后，先打 `display_title` 跟用户给的描述对一下；状态（`success` vs `failure`）和时间戳 `started_at` 也对一下。三个字段同时对得上，才是同一个 run。

## 适用范围

- 所有 Gitea Actions 调试场景
- GitHub Actions **没有这个问题** —— GitHub 的 URL 用的就是 API id，所以从 GH 切到 Gitea 第一次踩这个坑是必然的

## 关联

- 本次踩坑场景：调查 backend-integration OOM (ERR-20260509-001)
- CLAUDE.md「Gitea 平台配置」段没提这个坑，但实际开发高频用到
