🐛 fix(gitea-fix-ci): trace workflow_run CI chains
This commit is contained in:
+104
-185
@@ -1,231 +1,150 @@
|
||||
---
|
||||
name: gitea-fix-ci
|
||||
description: "Use when a user asks to debug or fix failing Gitea Actions, Gitea PR checks, or CI workflow runs for a Gitea-hosted repository. Also triggers on Chinese phrasing such as CI 挂了/流水线红了/构建失败/工作流失败."
|
||||
description: "Diagnose failing, skipped, or missing Gitea/Forgejo Actions and PR checks, including prepare→workflow_run trigger chains, then produce an evidence-backed repair plan before changing code. Use for CI 挂了/流水线红了/构建失败/工作流失败 on Gitea-hosted repositories. Do not use for GitHub/GitLab or external CI, local-only test/lint requests, or cases with neither remote access nor supplied logs."
|
||||
---
|
||||
|
||||
# Gitea Fix CI
|
||||
|
||||
## 概述
|
||||
## 核心约束
|
||||
|
||||
从 pull request 或 workflow run 诊断失败的 Gitea Actions,提取最小可用的失败上下文,
|
||||
然后在改代码之前先给出修复计划。核心原则:CI 日志是证据,不要仅凭红色状态臆测原因。
|
||||
- 把 run、job 和日志当证据;未取证前不猜根因、不改代码。
|
||||
- 先制定最小修复计划,在显式检查点等待授权。
|
||||
- 区分本地修改、push、重跑 workflow、runner/secret 操作;一种授权不包含另一种。
|
||||
- 取证脚本只读取 Gitea Actions,不分类、不修复、不改变远端状态。
|
||||
|
||||
本 skill 不是独立执行器。它指导你在当前工作区中使用随附的取证脚本
|
||||
`scripts/fetch_ci_logs.py`、`tea`、本地 `git` 和 Gitea API。
|
||||
|
||||
取证阶段(step 2-4)优先使用随附脚本,把「探测版本 → 找失败 run → 列 job →
|
||||
下载失败 job 日志」这段易记错 API 路径的逻辑交给它。脚本仅取证到日志,分类、
|
||||
修复计划与改代码仍由你按本文档执行。将 `<skill-dir>` 替换为包含本 `SKILL.md`
|
||||
的目录:
|
||||
随附脚本位于 `scripts/fetch_ci_logs.py`。全局选项必须放在子命令前:
|
||||
|
||||
```bash
|
||||
# 方式一(优先):通过环境变量提供 API token
|
||||
export GITEA_TOKEN=<token>
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py version # 探测实例版本并确认连通性
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py runs --status failure --branch <branch>
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py jobs <run-id> # 标注 failure 的 job
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py logs <job-id> # 日志存临时文件并回显尾部
|
||||
|
||||
# 方式二:显式复用当前仓库的 Git HTTP 凭据
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --use-git-credential version
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --use-git-credential runs --status failure
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py version
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json runs --limit 20
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json jobs <run-id>
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json logs <job-id> --tail 80
|
||||
```
|
||||
|
||||
`base-url`、owner 和 repo 默认从 git remote 推导。认证规则如下:
|
||||
|
||||
- 设置了 `GITEA_TOKEN` 时始终优先使用它,即使同时传入 `--use-git-credential`。
|
||||
- 只有显式传入 `--use-git-credential` 才会非交互调用 `git credential fill`,并将
|
||||
当前仓库对应的用户名和密码用于 HTTP Basic 认证;脚本不会静默读取 Git 凭据。
|
||||
- 未设置 token 且未传入该参数时按匿名方式访问,私有仓库通常会返回 401。
|
||||
- 所有携带凭据的请求必须使用 HTTPS;认证 header 只发送到配置的 Gitea 同源地址,
|
||||
跨源请求和跨源重定向会被拒绝。凭据不会出现在命令参数、日志或异常文本中。
|
||||
|
||||
### 取证路径选择
|
||||
|
||||
在 steps 2–4 沿同一条路径完成 run、job 与日志取证,不要在路径之间无依据跳转:
|
||||
|
||||
| 触发条件 | 取证路径 |
|
||||
| --- | --- |
|
||||
| 随附脚本可运行且 Actions API 可用 | 使用 `fetch_ci_logs.py` 的 `version` → `runs` → `jobs` → `logs` |
|
||||
| 脚本不可用,但 `tea actions` 或 Actions API 可用 | 使用下文对应的 `tea` 命令或 REST 端点 |
|
||||
| Actions API 返回 404,旧版实例仍有 Actions web UI | 使用 legacy web 页面定位 run、失败 job index 与日志 |
|
||||
| 无法远端访问,但用户提供日志 | 以粘贴日志为证据,并明确标注远端 run/job 未核验 |
|
||||
|
||||
## 适用场景
|
||||
|
||||
- Gitea 托管的仓库出现失败的 Gitea Actions 或 PR checks
|
||||
- 用户要求检查某个失败的 workflow run、job 或 CI 状态
|
||||
- 用户要求在 push、更新分支或更新 pull request 之后修复 CI
|
||||
- 本地测试通过,但远端 Gitea Actions 失败
|
||||
|
||||
## 不适用场景
|
||||
|
||||
- 仓库并非托管在 Gitea 或 Forgejo 兼容的基础设施上
|
||||
- 失败属于外部 CI 服务,只是从 Gitea 链接出去
|
||||
- 用户只想在本地跑测试或 lint
|
||||
- 没有凭据、token 或网络访问权限,且用户也未提供失败的日志文本
|
||||
认证优先级为 `GITEA_TOKEN` → 显式 `--use-git-credential` → 匿名访问。只在 HTTPS
|
||||
同源请求中发送认证 header;拒绝跨源请求与跨源重定向。不得把 token、用户名、密码
|
||||
放进 argv、日志或对话。
|
||||
|
||||
## 输入
|
||||
|
||||
- 仓库路径,默认为当前工作区
|
||||
- Gitea base URL 和仓库 owner/name,尽量从 `git remote -v` 获取
|
||||
- Pull request 编号、分支、commit SHA 或 workflow run ID
|
||||
- 认证方式:`tea` 登录 profile、用于 API 请求的 `GITEA_TOKEN`,或通过
|
||||
`--use-git-credential` 显式读取的当前仓库 Git HTTP 凭据
|
||||
- 若无法远端访问,则由用户粘贴的 CI 日志
|
||||
- 当前仓库路径和 remote,默认使用当前工作区与 `origin`
|
||||
- PR 编号、分支、完整 commit SHA、workflow run ID 或 job ID
|
||||
- Gitea base URL 与 owner/repo;优先从 remote 推导
|
||||
- `GITEA_TOKEN`、已配置的 `tea` profile,或显式 Git HTTP 凭据授权
|
||||
- 无法访问远端时,由用户提供的最小 CI 日志
|
||||
|
||||
## 适用边界
|
||||
|
||||
适用于 Gitea/Forgejo 托管的 Actions、PR checks 和 workflow_run 触发链;入口可以是
|
||||
PR、run、job、commit SHA 或用户提供的 CI 日志。
|
||||
|
||||
不适用于 GitHub/GitLab、仅从 Gitea 链出的外部 CI、只想本地跑测试/lint,或既无远端
|
||||
访问也无用户日志的请求。
|
||||
|
||||
## 取证路径
|
||||
|
||||
先选择一条路径,再读取对应文件;不要把互斥路径的细节全部载入:
|
||||
|
||||
| 触发条件 | 必须读取 | 下一步 |
|
||||
| ----------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| 脚本可运行且 Actions API 可用 | [`workflows/actions-api.md`](workflows/actions-api.md) | 用 workflow、事件、状态和时间遍历直接 run 或 `workflow_run` 链 |
|
||||
| 脚本不可用但 `tea actions` 可用 | [`workflows/fallback.md`](workflows/fallback.md) | 使用 tea 定位 run/job/log |
|
||||
| Actions API 返回 404,旧实例仍有 Actions web UI | [`workflows/fallback.md`](workflows/fallback.md) | 从 run 页面选择失败 job index |
|
||||
| 只有用户粘贴的日志 | [`workflows/fallback.md`](workflows/fallback.md) | 标记远端 run/job 未核验 |
|
||||
|
||||
## 流程
|
||||
|
||||
1. **基线本地状态**
|
||||
1. **记录基线**
|
||||
|
||||
- 记录 `git status --short`、当前分支和最新 commit SHA。
|
||||
- 从 `git remote -v` 识别 remote URL 和 owner/repo。
|
||||
- 继续之前先确认 remote 是 Gitea/Forgejo:对照已知的 Gitea 实例核对 host,
|
||||
或探测 `/api/v1/version`。若 remote 是 GitHub、GitLab 或其他服务,按"不适用场景"停止。
|
||||
- 收集 CI 证据期间不要修改文件。
|
||||
运行 `git status --short`,记录当前分支、HEAD 和 `git remote -v`。通过已知实例或
|
||||
`/api/v1/version` 确认 remote 属于 Gitea/Forgejo。取证期间不修改文件。
|
||||
|
||||
2. **验证 Gitea 访问**
|
||||
2. **选择路径并定位 run**
|
||||
|
||||
- 优先运行 `fetch_ci_logs.py version` 探测实例版本并确认脚本能连通实例。脚本从
|
||||
remote 推导 base URL,认证按 `GITEA_TOKEN` → 显式 `--use-git-credential` →
|
||||
匿名的顺序选择。注意:`version` 只读 `/api/v1/version`,成功仅代表连通与鉴权
|
||||
可用,**不代表 Actions API 一定可用**。Actions 端点是否存在需在后续
|
||||
`runs`/`jobs` 步骤中实际验证;旧版 Gitea(见下)会在此才暴露 404。
|
||||
- 选择 `tea` 路径时,先确认已安装并已认证:
|
||||
- `tea login list`
|
||||
- 使用前先运行 `tea actions --help` 确认已安装的 `tea` 版本存在 `actions`
|
||||
子命令;并非所有版本都带这些子命令。
|
||||
- `tea actions runs list --status failure --branch <branch>`
|
||||
- `tea actions runs view <run-id>`
|
||||
- `tea actions runs logs <run-id> --job <job-id>`
|
||||
- `tea pulls view <pr>`
|
||||
- 若 `tea` 不可用,或已安装版本缺少 Actions 命令,则直接使用 Gitea API。
|
||||
- 当 API 行为不明确时,检查 `/api/v1/version` 和 `/swagger.v1.json`。较旧的
|
||||
Gitea(1.21 及更早)在 web UI 中提供 Actions 页面,但不提供 workflow
|
||||
run/job/log 的 API 端点;应对照上报的版本确认可用性,而不是想当然。
|
||||
- 绝不打印 token、用户名或密码。API token 只通过环境变量传递;Git 凭据只通过
|
||||
`git credential fill` 的标准输入/输出在脚本进程内传递,不放入 argv。
|
||||
按上表读取一个 workflow 文件并执行。入口是 PR 时,先读取仓库 workflow 的 `on:`
|
||||
段,判断失败检查是直接触发,还是由 Prepare 等上游 workflow 经 `workflow_run`
|
||||
触发。不得默认所有 run 都保存 PR 分支或 PR head SHA。
|
||||
|
||||
3. **定位失败的 workflow run**
|
||||
区分 `failure`、`cancelled`、`skipped` 和缺失:
|
||||
|
||||
- 若入口是 PR 而非 run ID,先把 PR 解析成 run:取 PR 的 head 分支与 head SHA
|
||||
(`fetch_ci_logs.py` 的 `runs` 支持 `--branch`/`--sha`;手动则
|
||||
`GET /api/v1/repos/<owner>/<repo>/pulls/<pr>` 取 `head.ref`/`head.sha`),
|
||||
再按该 SHA 过滤 runs。PR 页面的 checks 列表可能聚合多个 workflow,逐一定位到
|
||||
具体失败 run,不要假设只有一个。
|
||||
- 优先用脚本:`fetch_ci_logs.py runs --status failure --branch <branch>`
|
||||
(也支持 `--sha`/`--event`/`--limit`),已知 run ID 时用
|
||||
`fetch_ci_logs.py jobs <run-id>` 直接列出各 job 并标注失败项。
|
||||
- 使用 `tea`、API 或 web 路径且已知 run ID 时,直接获取该 run。
|
||||
- 否则按分支、事件、状态或 commit SHA 过滤,列出最近的 workflow runs。
|
||||
- API 模式:
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/runs`
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/runs/<run>`
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/runs/<run>/jobs`
|
||||
- 较旧 Gitea 的 web 回退:
|
||||
- `GET /<owner>/<repo>/actions`
|
||||
- 解析 `/<owner>/<repo>/actions/runs/<run>` 链接和状态标签。
|
||||
- 区别对待 `failure`、`cancelled` 和缺失的必需 checks。被取消(cancelled)的
|
||||
job 可能需要重跑或排查队列,而非改代码。
|
||||
- 有明确 run ID 时直接查该 run。
|
||||
- 直接触发的 PR workflow 优先按完整 PR head SHA 过滤;只有实例已验证
|
||||
`head_branch` 语义时才把 PR 分支作为辅助过滤条件。
|
||||
- `workflow_run` 链先定位上游 run,再判断是否应存在下游 run。
|
||||
- 上游失败或取消时,证据优先在上游日志;下游通常为 `skipped`,也可能缺失。
|
||||
|
||||
4. **获取 job 日志**
|
||||
3. **获取最小日志证据**
|
||||
|
||||
- 优先用脚本:`fetch_ci_logs.py logs <job-id>`。它把日志存到受跟踪源码路径
|
||||
之外的临时文件、只回显尾部若干行,并打印临时文件路径供进一步查看
|
||||
(`--tail N` 调整行数,`--json` 输出结构化结果)。
|
||||
- 使用 API 路径时,对每个失败的 job 下载日志:
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/jobs/<job_id>/logs`
|
||||
- 在较旧 Gitea 的 web 回退(无 job-log API)下,按 UI 的 job index 下载日志:
|
||||
- `GET /<owner>/<repo>/actions/runs/<run>/jobs/<job-index>/logs`
|
||||
- 先打开 run 页面读取每个 job 的状态;取状态为 `failure` 的 job 的 index,
|
||||
不要默认用 index `0`。失败的 job 很少是第一个,盲目用 index `0` 通常会返回
|
||||
某个通过的 job 的日志。只有当 run 页面不暴露每个 job 的状态时,才回退到逐一
|
||||
扫描 index。
|
||||
- 大日志保存到受跟踪源码路径之外的临时文件。
|
||||
- 提取首个可操作的错误块、其上下文命令、job 名、workflow 名、run URL、分支和 SHA。
|
||||
- 若日志缺失,明确报告,而不是编造原因。
|
||||
先列 jobs,再下载失败 job 日志;取消或基础设施场景读取最后执行或被取消的相关
|
||||
job。保存完整日志到受跟踪源码路径之外,只回显首个可操作错误块及必要上下文。
|
||||
记录 workflow、job、run URL、事件、状态、分支和 SHA。日志缺失时明确报告,不编造
|
||||
原因。
|
||||
|
||||
5. **分类失败**
|
||||
4. **分类失败**
|
||||
|
||||
- 代码/测试失败:断言失败、编译错误、lint 错误、类型错误
|
||||
- 环境失败:缺失 secret、runner 镜像、依赖安装、网络、缓存、权限或服务启动
|
||||
- 工作流失败:YAML 非法、语法不支持、触发条件错误、路径错误、分支/ref 假设错误
|
||||
- 基础设施失败:runner 离线、队列卡住、run 被取消、超时
|
||||
- 把日志直接显示的事实与根因假设分开。一个断言、编译错误或心跳中断只能证明其
|
||||
直接症状;在读取相关源码、配置或运行环境前,不把默认值、依赖版本、runner
|
||||
资源等假设写成已确认根因。
|
||||
- **代码/测试失败**:断言、编译、lint 或类型错误
|
||||
- **环境失败**:secret、镜像、依赖、网络、缓存、权限或服务启动
|
||||
- **工作流失败**:YAML、触发条件、路径、ref 或事件假设错误
|
||||
- **基础设施失败**:runner 离线、队列、取消、超时
|
||||
- **上游门禁未通过**:Prepare 等上游 `failure`/`cancelled`,导致下游 `skipped` 或缺失
|
||||
|
||||
6. **制定修复计划**
|
||||
把日志直接显示的事实与根因假设分开。断言、编译错误或心跳中断只证明直接症状;
|
||||
读取相关源码、配置或运行环境前,不把默认值、版本或资源假设写成已确认根因。
|
||||
|
||||
- 用确切的 job/run 标识总结失败证据。
|
||||
- 提出与证据匹配的最小代码或工作流改动;证据尚未区分多个可能根因时,先计划
|
||||
检查对应源码、配置或环境,不预选修复。
|
||||
- 本地验证优先复用失败 workflow/job 的原命令。若只能运行不同命令,明确标为
|
||||
“近似验证”,说明与 CI 命令、环境或依赖的差异,并保留远端复检路径。
|
||||
5. **制定修复计划**
|
||||
|
||||
7. **🔴 CHECKPOINT · 🛑 STOP:等待修复授权**
|
||||
用确切 run/job 标识总结证据,提出与证据匹配的最小修改。优先复用失败 job 的原命令
|
||||
做本地验证;只能运行不同命令时标为“近似验证”,说明差异并保留远端复检路径。
|
||||
|
||||
展示计划后停止。只有用户明确批准具体动作,才进入实施:批准修改本地文件不等于
|
||||
批准 push 或重跑 workflow;批准重跑不等于批准重启/重新注册 runner、修改 secret
|
||||
或其它基础设施状态。授权范围不明确时保持只读取证,不修改代码、workflow 或外部
|
||||
状态。
|
||||
6. **🔴 CHECKPOINT · 🛑 STOP:等待修复授权**
|
||||
|
||||
8. **批准后实施**
|
||||
展示计划后停止。批准修改本地文件不等于批准 push 或重跑;批准重跑不等于批准
|
||||
重启/重新注册 runner、修改 secret 或其它基础设施状态。授权不明确时保持只读取证。
|
||||
|
||||
- 只应用已批准的修复。
|
||||
- 运行最接近复现该失败 job 的本地命令。
|
||||
- 若失败仅涉及工作流,验证工作流文件语法及其引用的路径或脚本。
|
||||
7. **批准后实施**
|
||||
|
||||
9. **复检**
|
||||
只应用已批准的修复,运行最接近 CI 的本地命令。工作流改动需验证语法、触发条件、
|
||||
引用路径和事件字段。
|
||||
|
||||
- 告诉用户需要在 Gitea 中 push 或重跑什么。
|
||||
- 若获准,使用 Gitea API 检查重跑状态。
|
||||
- 最终输出必须区分本地验证与远端 CI 状态。
|
||||
8. **复检**
|
||||
|
||||
区分本地验证与远端状态。说明需要 push 或重跑什么;只有获得相应授权后才执行或
|
||||
查询重跑。
|
||||
|
||||
## 输出约定
|
||||
|
||||
- `Target:` 仓库、分支/SHA、PR 或 run ID
|
||||
- `Failed CI:` workflow、job、状态、run URL 或 API 路径
|
||||
- `Evidence:` 精简的日志片段、分类、已确认事实与尚待验证的根因假设
|
||||
- `Plan:` 提出的检查或修复、本地验证(原命令或标注近似)、远端复检
|
||||
- `Changes:` 批准后改动的文件
|
||||
- `Result:` 已运行的本地检查与剩余的远端状态
|
||||
- `Target:` 仓库、PR、分支/SHA 或 run ID
|
||||
- `Failed CI:` workflow、job、状态与 run URL/API 路径
|
||||
- `Evidence:` 最小日志片段、分类、已确认事实与待验证假设
|
||||
- `Plan:` 最小修复、本地验证方式与远端复检
|
||||
- `Changes:` 批准后修改的文件
|
||||
- `Result:` 已运行检查与剩余远端状态
|
||||
|
||||
## 成功标准
|
||||
|
||||
- 失败分析基于 Gitea Actions 的 run/job 数据或粘贴的日志
|
||||
- 修复计划指明其针对的确切 workflow run 或 job
|
||||
- 计划批准前不发生任何代码或工作流改动
|
||||
- 验证区分本地命令与远端 Gitea Actions 结果
|
||||
- 不回显 token;私有日志只保留可操作的片段,不整段外泄
|
||||
- 分析基于 Gitea run/job/log 或明确标注的粘贴日志
|
||||
- 计划指向确切 workflow run/job 或确切的上游门禁
|
||||
- 计划批准前不修改本地文件或远端状态
|
||||
- 不泄露凭据或整段私有日志
|
||||
- 最终结果区分本地验证、推断关联与远端已核验状态
|
||||
|
||||
## 危险信号(Red Flags)
|
||||
## 危险信号
|
||||
|
||||
出现以下情况说明流程走偏,停下纠正而非继续:
|
||||
|
||||
- **凭红色状态臆测原因**:还没下载 job 日志就断言失败原因或动手改代码。
|
||||
- **把症状写成根因**:仅凭单个断言、编译错误或最后一行日志,就断言具体默认值、
|
||||
依赖版本、配置路径或 runner 资源是根因。
|
||||
- **误读双字段结果**:把 `status`(生命周期)当成结果判定。Gitea Actions 沿用
|
||||
GitHub 兼容的双字段模型——`status=completed` 只表示跑完了,真正的成败在
|
||||
`conclusion`(`failure`/`success`/`cancelled`)。判定失败必须看有效结果,
|
||||
而非 `status=completed` 就当通过。
|
||||
- **盲取 job index `0`**:在 web 回退下不看每个 job 状态就用 index `0` 下载日志;
|
||||
失败的 job 很少是第一个,通常会误取到某个通过 job 的日志。
|
||||
- **把 `version` 成功当作 Actions API 可用**:`version` 只探连通与鉴权,Actions
|
||||
端点可能仍返回 404(旧版 Gitea)。
|
||||
- **把 `cancelled`/基础设施问题当代码 bug 修**:被取消、runner 离线、队列卡住、
|
||||
超时应重跑或排查环境,不是改代码。
|
||||
- **回显 token 或整段私有日志**:只保留可操作的最小片段。
|
||||
- **静默读取或降级传输凭据**:Git 凭据必须由 `--use-git-credential` 显式启用;
|
||||
token 和 Git 凭据都不得通过 HTTP 或跨源重定向发送。
|
||||
- 未下载日志就断言原因或修改代码
|
||||
- 把 `status=completed` 当成功,而不看 `conclusion`
|
||||
- 按 PR 分支或 PR SHA 过滤 `workflow_run` 下游 run
|
||||
- Prepare 失败后继续寻找不存在的“下游失败日志”,忽略 `skipped`/缺失
|
||||
- 在 web 回退中盲取 job index `0`
|
||||
- 把 `version` 成功当成 Actions API 一定可用
|
||||
- 把 `cancelled`、runner 或队列问题当代码 bug
|
||||
- 静默读取凭据、使用 HTTP、跨源发送认证 header
|
||||
- 越权 push、重跑、改 secret 或操作 runner
|
||||
|
||||
## 失败处理
|
||||
|
||||
- 401 且未使用认证时,通过环境提供 `GITEA_TOKEN` 或显式传入
|
||||
`--use-git-credential`;401 且已使用认证时检查凭据是否有效,不要在对话中索要 secret
|
||||
- 403 表示所选凭据缺少仓库或 Actions 读取权限;补足权限,而不是切换到匿名访问
|
||||
- 404 优先核对 owner/repo、端点和 Gitea 版本;旧版本缺少 Actions API 时使用 web 回退
|
||||
- 若 Gitea 版本缺少 Actions API 端点,请用户提供相关日志文本或从浏览器复制的 job 日志
|
||||
- 若失败的 check 归属外部 CI 服务,报告外部 URL 并在证据收集处停止
|
||||
- 若失败仅为基础设施问题,建议重跑/排查 runner,而非改代码
|
||||
- 401:提供 `GITEA_TOKEN` 或显式使用 `--use-git-credential`;已认证仍 401 时检查凭据
|
||||
- 403:补足仓库和 Actions 读取权限,不降级为匿名
|
||||
- 404:核对 owner/repo、endpoint 和版本;旧版 Actions API 转 web 回退
|
||||
- 下游候选无法与上游可靠关联:报告“关联未证实”,使用日志中的 ref/SHA 或用户提供的 run URL
|
||||
- 外部 CI:报告外部 URL 后停止本 skill
|
||||
- 只有基础设施问题:计划重跑或排查 runner,不修改源码
|
||||
|
||||
@@ -466,14 +466,30 @@ def _outcome(item: dict[str, Any]) -> str:
|
||||
return (conclusion or status).lower()
|
||||
|
||||
|
||||
def _workflow_name(run: dict[str, Any]) -> str:
|
||||
workflow = run.get("name") or run.get("workflow_id") or ""
|
||||
if workflow:
|
||||
return str(workflow)
|
||||
path = str(run.get("path") or "")
|
||||
return path.rsplit("@", 1)[0] if path else ""
|
||||
|
||||
|
||||
def _run_row(run: dict[str, Any]) -> dict[str, Any]:
|
||||
workflow = _workflow_name(run)
|
||||
return {
|
||||
"id": run.get("id"),
|
||||
"name": run.get("name") or run.get("workflow_id") or "",
|
||||
"name": workflow,
|
||||
"workflow": workflow,
|
||||
"path": run.get("path") or "",
|
||||
"title": run.get("display_title") or "",
|
||||
"outcome": _outcome(run),
|
||||
"event": run.get("event") or "",
|
||||
"branch": run.get("head_branch") or run.get("branch") or "",
|
||||
"sha": (run.get("head_sha") or run.get("commit_sha") or "")[:12],
|
||||
"sha": run.get("head_sha") or run.get("commit_sha") or "",
|
||||
"started_at": run.get("started_at") or "",
|
||||
"completed_at": run.get("completed_at") or "",
|
||||
"run_number": run.get("run_number"),
|
||||
"run_attempt": run.get("run_attempt"),
|
||||
"url": run.get("html_url") or run.get("url") or "",
|
||||
}
|
||||
|
||||
@@ -483,24 +499,30 @@ def cmd_runs(
|
||||
target: RepoTarget,
|
||||
client: ApiClient,
|
||||
) -> int:
|
||||
if args.sha and not re.fullmatch(r"[0-9a-fA-F]{40}", args.sha):
|
||||
raise ConfigError("--sha requires a full 40-character hexadecimal commit SHA")
|
||||
query: dict[str, str] = {}
|
||||
if args.branch:
|
||||
query["branch"] = args.branch
|
||||
if args.event:
|
||||
query["event"] = args.event
|
||||
if args.status:
|
||||
if args.status and args.status != "all":
|
||||
query["status"] = args.status
|
||||
if args.sha:
|
||||
query["head_sha"] = args.sha
|
||||
if args.limit:
|
||||
query["limit"] = str(args.limit)
|
||||
suffix = "/actions/runs"
|
||||
if args.workflow:
|
||||
workflow = urllib.parse.quote(args.workflow, safe="")
|
||||
suffix = f"/actions/workflows/{workflow}/runs"
|
||||
else:
|
||||
suffix = "/actions/runs"
|
||||
if query:
|
||||
suffix += "?" + urllib.parse.urlencode(query)
|
||||
payload = client.get_json(target.repo_path(suffix))
|
||||
runs = [_run_row(run) for run in _as_list(payload, "workflow_runs")]
|
||||
if args.sha:
|
||||
runs = [row for row in runs if row["sha"].startswith(args.sha[:12])]
|
||||
runs = [row for row in runs if row["sha"].lower() == args.sha.lower()]
|
||||
runs = runs[: args.limit] if args.limit else runs
|
||||
|
||||
if args.json:
|
||||
@@ -511,10 +533,20 @@ def cmd_runs(
|
||||
return 1
|
||||
print(f"{len(runs)} run(s):")
|
||||
for row in runs:
|
||||
number = f" number={row['run_number']}" if row["run_number"] else ""
|
||||
attempt = f" attempt={row['run_attempt']}" if row["run_attempt"] else ""
|
||||
print(
|
||||
f" run {row['id']} [{row['outcome']}] {row['name']} "
|
||||
f"{row['event']} {row['branch']} {row['sha']}"
|
||||
f" run {row['id']} [{row['outcome']}] {row['workflow']} "
|
||||
f"{row['event']} {row['branch']} {row['sha'][:12]}"
|
||||
f"{number}{attempt}"
|
||||
)
|
||||
if row["started_at"] or row["completed_at"]:
|
||||
print(
|
||||
f" started={row['started_at'] or '-'} "
|
||||
f"completed={row['completed_at'] or '-'}"
|
||||
)
|
||||
if row["title"]:
|
||||
print(f" title: {row['title']}")
|
||||
if row["url"]:
|
||||
print(f" {row['url']}")
|
||||
return 0
|
||||
@@ -651,10 +683,18 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
sub.add_parser("version", help="probe /api/v1/version")
|
||||
|
||||
runs = sub.add_parser("runs", help="list workflow runs")
|
||||
runs.add_argument(
|
||||
"--workflow",
|
||||
help="workflow file or ID (for example checks.yml)",
|
||||
)
|
||||
runs.add_argument("--branch")
|
||||
runs.add_argument("--event")
|
||||
runs.add_argument("--status", default="failure")
|
||||
runs.add_argument("--sha")
|
||||
runs.add_argument(
|
||||
"--status",
|
||||
default="failure",
|
||||
help="run outcome/status (default failure; use all to omit this filter)",
|
||||
)
|
||||
runs.add_argument("--sha", help="full triggering commit SHA")
|
||||
runs.add_argument("--limit", type=int, default=20)
|
||||
|
||||
jobs = sub.add_parser("jobs", help="list jobs of a run, flag failed ones")
|
||||
|
||||
@@ -13,5 +13,15 @@
|
||||
"id": "cancelled-infrastructure-not-code-bug",
|
||||
"prompt": "Gitea Actions 页面仍是红色:run 的 status 是 completed,conclusion 是 cancelled,日志最后显示 runner lost heartbeat。请修复这个 CI。",
|
||||
"expected": "以 conclusion 而非 status 判断结果,将其分类为取消或基础设施问题;建议重跑并排查 runner/队列,不编造代码原因、不修改源码;最终明确区分本地验证与尚待远端复检的状态。"
|
||||
},
|
||||
{
|
||||
"id": "workflow-run-chain-checks-failure",
|
||||
"prompt": "Gitea 上 PR #42 的 Prepare 已成功,但 Checks 红了。仓库的 checks.yml 由 workflow_run 触发。请定位正确日志并给出修复计划。",
|
||||
"expected": "先读取 workflow 触发关系并按 PR head SHA 定位 Prepare;不使用 PR 分支或 PR SHA 过滤 workflow_run 下游 run,而是按 checks.yml、event=workflow_run、status=all 和上游完成时间定位候选,再用日志中的实际 ref/SHA 或 PR run URL 核验关联;读取失败 job 日志后形成计划并在修改前暂停。"
|
||||
},
|
||||
{
|
||||
"id": "prepare-gate-failure-skips-downstream",
|
||||
"prompt": "Gitea 上 PR #43 没看到 Checks,Prepare 是 failure;下游 workflow 由 workflow_run 触发且 job 有 success 门禁。请查清楚。",
|
||||
"expected": "把问题分类为上游门禁未通过,读取 Prepare 的失败 job 日志;检查下游是否为 skipped 或缺失,但不继续寻找下游失败日志,也不把缺失误报为未知代码错误;区分已核验事实与关联推断,给出计划后等待授权。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Actions API 取证
|
||||
|
||||
仅在 `fetch_ci_logs.py` 可运行且 Gitea Actions API 可用时读取本文件。
|
||||
|
||||
## CLI 规则
|
||||
|
||||
全局选项放在子命令前:`--remote`、`--base-url`、`--owner`、`--repo`、
|
||||
`--use-git-credential`、`--json`。`logs` 的 `--tail` 与 `--out` 放在 job ID 后。
|
||||
|
||||
```bash
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --remote origin --json runs --limit 20
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json logs <job-id> --tail 80 --out /tmp/job.log
|
||||
```
|
||||
|
||||
`runs` 默认过滤 `failure`;发现触发链时用 `--status all`,查上游门禁结果时也检查
|
||||
`cancelled` 与 `skipped`。`--sha` 使用完整 SHA。`--workflow checks.yml` 通过 workflow
|
||||
专用 endpoint 过滤,并在输出中保留 `path`、时间、run number 与 attempt。
|
||||
|
||||
## 直接触发
|
||||
|
||||
1. 从 `GET /pulls/<pr>` 取得 `head.ref` 与完整 `head.sha`。
|
||||
2. 按目标 workflow、事件和完整 SHA 列 runs。Gitea 的 PR run 可能保存隐藏 PR ref,
|
||||
`head_branch` 不一定等于 `head.ref`;只有在目标实例实测匹配时才追加 `--branch`。
|
||||
3. 用 `path` 确认 workflow,再列 jobs 和失败日志。
|
||||
|
||||
```bash
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json runs \
|
||||
--workflow <workflow.yml> --event pull_request --status all --sha <full-pr-head-sha>
|
||||
```
|
||||
|
||||
## Prepare → workflow_run 链
|
||||
|
||||
先读取仓库 workflow 的 `on:` 段,确认上游 workflow 名与下游文件。
|
||||
|
||||
1. **定位上游**:按 PR 完整 head SHA 定位 Prepare 候选;用 workflow path、事件和时间
|
||||
排除无关 run,记录 run ID、attempt、`conclusion` 与 `completed_at`。同一 SHA 有多个
|
||||
run 或 attempt 时逐个保留,不静默任选。
|
||||
2. **判断门禁**:
|
||||
- `failure`/`cancelled`:列上游 jobs 并读取上游失败日志;分类为“上游门禁未通过”。
|
||||
下游通常会产生 `skipped` run/job,也可能缺失,不要寻找下游失败日志。
|
||||
- `success`:继续查对应下游 workflow。
|
||||
3. **定位下游候选**:按 workflow 文件、`event=workflow_run`、`status=all` 列 runs,选择
|
||||
`started_at` 紧随该上游 attempt 完成时间的候选;重跑上游会生成新一组下游 run。
|
||||
4. **处理缺失**:上游成功却没有下游候选时,核对默认分支上是否存在并启用了下游
|
||||
workflow、`workflow_run.workflows` 名称是否与上游一致,以及事件/条件是否排除了该
|
||||
run。仍无下游时,把“应存在但缺失”作为工作流或 Actions 基础设施证据,不虚构下游
|
||||
日志。
|
||||
5. **核验关联**:公开 Gitea run API 不暴露父 run ID 或事件 payload。下游 API 的
|
||||
`head_branch/head_sha` 属于默认分支 run 本身,不是 PR head。必须用 job 日志中实际
|
||||
checkout/ref/SHA、PR 页给出的 run URL,或 workflow 自己输出的 provenance 核验。
|
||||
未核验前把关联标为推断。
|
||||
|
||||
```bash
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json runs \
|
||||
--workflow prepare.yml --event pull_request --status all --sha <full-pr-head-sha>
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json runs \
|
||||
--workflow checks.yml --event workflow_run --status all --limit 30
|
||||
```
|
||||
|
||||
## Jobs 与日志
|
||||
|
||||
```bash
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json jobs <run-id>
|
||||
python <skill-dir>/scripts/fetch_ci_logs.py --json logs <job-id> --tail 80
|
||||
```
|
||||
|
||||
下载失败 job 日志;取消或基础设施场景读取最后执行或被取消的相关 job。上游门禁场景
|
||||
只取上游证据。完整日志默认写临时文件,或用 `--out` 指向仓库外路径。提取首个可操作
|
||||
错误块,不输出凭据或整段私有日志。
|
||||
|
||||
## 原始 API
|
||||
|
||||
- `/api/v1/version` 只证明实例连通;Actions endpoint 仍需实际调用验证
|
||||
- API 字段或过滤器不明确时读取 `/swagger.v1.json`,不要猜参数语义
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/runs`
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/workflows/<workflow>/runs`
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/runs/<run>/jobs`
|
||||
- `GET /api/v1/repos/<owner>/<repo>/actions/jobs/<job_id>/logs`
|
||||
@@ -0,0 +1,37 @@
|
||||
# 取证回退路径
|
||||
|
||||
仅在脚本不可用、Actions API 返回 404,或只有用户日志时读取本文件。
|
||||
|
||||
## tea actions
|
||||
|
||||
先确认安装、登录和子命令能力;不同 tea 版本不一定提供 Actions:
|
||||
|
||||
```bash
|
||||
tea login list
|
||||
tea actions --help
|
||||
tea pulls view <pr>
|
||||
tea actions runs list --status failure
|
||||
tea actions runs view <run-id>
|
||||
tea actions runs logs <run-id> --job <job-id>
|
||||
```
|
||||
|
||||
仍需先读取 workflow `on:` 段。遇到 `workflow_run` 链时,先查 Prepare,再按下游
|
||||
workflow 和时间定位候选;不要用 PR 分支过滤下游 run。
|
||||
|
||||
## Legacy web
|
||||
|
||||
Gitea 1.21 及更早可能有 Actions web UI,但没有 run/job/log API:
|
||||
|
||||
1. 打开 `/<owner>/<repo>/actions`,找到目标 run。
|
||||
2. 打开 `/<owner>/<repo>/actions/runs/<run>`,读取每个 job 的状态。
|
||||
3. 选择状态为 `failure` 的 job index,再下载
|
||||
`/<owner>/<repo>/actions/runs/<run>/jobs/<job-index>/logs`。
|
||||
4. 不要默认 index `0`;只有页面不暴露 job 状态时才逐一扫描。
|
||||
|
||||
Prepare 失败时读取 Prepare 页和其失败 job;下游可能显示 `skipped` 或根本不出现在
|
||||
PR checks 中,不要把它误报为“找不到失败 run”。
|
||||
|
||||
## 用户粘贴日志
|
||||
|
||||
把日志标记为用户提供的证据,并明确:远端 run/job、workflow、SHA 与状态均未核验。
|
||||
只引用首个可操作错误块;日志不含 run/job 身份时,不把推测写成事实。
|
||||
Reference in New Issue
Block a user