| name | gitcode-ci-diagnose |
| description | Use when a gitcode.com PR's /retest finished and you need to know which checks failed and why. Given a PR number, fetches openeuler-ci-bot comments, parses the check result table, identifies FAILED checks, extracts console URL, fetches console output, and pinpoints failing test cases + actual error messages. Saves 10+ manual API/web fetches per diagnosis. |
GitCode PR CI 诊断
针对 gitcode.com 上的 PR(openeuler/yuanrong-datasystem 或其他 openeuler 仓),自动定位 CI 失败的检查项 + 失败用例 + 根因错误信息。
适用场景
- 用户说「PR #N CI 跑完了」「PR #N 失败了」「帮我看下 CI 结果」
- /retest 触发后等结果,需要快速判断是否本 PR 引入
- 失败 PR 需要写 fix,但不知道根因在哪
不适用:
- GitHub PR(API 完全不同)
- 还在 running 的 CI(需等完成)
- 非 openeuler 仓的 gitcode PR(结构可能不同)
前置条件
- GitCode token 可用(优先级:环境变量
GITCODE_TOKEN / GITCODE_ACCESS_TOKEN → ~/.local/gitcode_token → ~/.gitcode_token),有 repo 权限
- 安装了
curl、python3、jq(可选)
- 装了 MCP
web-reader 或能用 WebFetch(CI console 是公开 URL,无需鉴权)
- 若需抓 SPA 内容(如 openlibing.com codecheck 报告):WSL2 + Windows 双环境,Windows 装了 Edge + Python + selenium(见 Step 5.5)
工作流程
PR 号 → 列评论 → 找 openeuler-ci-bot 带 FAILED 的表 → 解析 console URL
↓
WebFetch console → grep failure markers → 提取失败用例 + 根因
↓
若报告是 SPA(openlibing codecheck / 其他 dashboard)→ Step 5.5 WSL2→Windows Edge
Step 1: 拉取 PR 评论
if [ -n "$GITCODE_TOKEN" ]; then TOKEN="$GITCODE_TOKEN"
elif [ -n "$GITCODE_ACCESS_TOKEN" ]; then TOKEN="$GITCODE_ACCESS_TOKEN"
elif [ -f ~/.local/gitcode_token ]; then TOKEN=$(cat ~/.local/gitcode_token)
elif [ -f ~/.gitcode_token ]; then TOKEN=$(cat ~/.gitcode_token)
else echo "no gitcode token found"; exit 1; fi
curl -s "https://api.gitcode.com/api/v5/repos/openeuler/yuanrong-datasystem/pulls/${PR}/comments?per_page=50" \
--header "Authorization: Bearer $TOKEN" \
--header "PRIVATE-TOKEN: $TOKEN"
注意:
- token 路径与环境变量约定与
ds-pr-review skill 一致(~/.local/gitcode_token + GITCODE_TOKEN/GITCODE_ACCESS_TOKEN),本机若无则回退 ~/.gitcode_token
- 鉴权必须同时传
Authorization: Bearer 和 PRIVATE-TOKEN 两个 header
per_page=50 通常够,大型 PR 可分页
Step 2: 找最新的 CI 结果评论
openeuler-ci-bot 在 CI 跑完时会发一条HTML 表格评论,结构如下:
<table>
<tr><th colspan=2>Check Name</th> <th>Build Result</th> <th>详情</th> <th>Build Details</th></tr>
<tr><td colspan=2>check_code</td> <td>✅<strong>SUCCESS</strong></td> <td></td>
<td rowspan=3><a href=https://ci.openeuler.openatom.cn/.../6474>#6474</a></td></tr>
<tr><td colspan=2>check_package_license</td> <td>✅<strong>SUCCESS</strong></td> <td></td></tr>
<tr><td colspan=2>check_sca</td> <td>✅<strong>SUCCESS</strong></td> <td></td></tr>
<tr><td rowspan=1>x86_64</td> <td>check_build</td>
<td>❌<strong>FAILED</strong></td> <td></td>
<td rowspan=1><a href=https://ci.openeuler.openatom.cn/.../6577/console>#6577</a></td></tr>
<tr><td rowspan=1>aarch64</td> <td>check_build</td>
<td>❌<strong>FAILED</strong></td> <td></td>
<td rowspan=1><a href=https://ci.openeuler.openatom.cn/.../6591/console>#6591</a></td></tr>
<tr><td rowspan=1>openyuanrong</td> <td>check_build</td>
<td>✅<strong>SUCCESS</strong></td> <td></td>
<td rowspan=1><a href=...>#2498</a></td></tr>
</table>
关键解析点:
- HTML entity
✅ = ✅ SUCCESS,❌ = ❌ FAILED
- 同一行
<a href=...N/console> 是该 check 的 console URL(公开可访问,无需鉴权)
- 同一个 PR 可能有多条 CI 结果评论(每次 /retest 一条),取最新一条(按
created_at 排序)
Step 3: Python 解析脚本(一键提取 FAILED checks + console URLs)
import json, sys, re
with open("pr_comments.json") as f:
comments = json.load(f)
ci_comments = [c for c in comments
if c.get("user", {}).get("login") == "openeuler-ci-bot"
and "<table>" in (c.get("body") or "")]
if not ci_comments:
print("CI 还没跑完或评论格式异常")
sys.exit(0)
latest = max(ci_comments, key=lambda c: c.get("created_at", ""))
body = latest["body"]
print(f"=== CI 评论时间: {latest['created_at']} ===\n")
def label_cells(row_html):
"""提取行内纯文本单元格:跳过 &#...; 标记实体和含 <a>/<strong> 的单元格。"""
out = []
for m in re.finditer(r"<td[^>]*>(.*?)</td>", row_html, re.DOTALL):
inner = m.group(1).strip()
if not inner or inner.startswith("&#"):
continue
if "<a " in inner or "<strong>" in inner:
continue
out.append(inner)
return out
print(f"{'Check':36} {'Result':10} {'Console URL'}")
print("-" * 110)
for row in re.finditer(r"<tr>(.*?)</tr>", body, re.DOTALL):
cells = row.group(1)
rm = re.search(r"<strong>(SUCCESS|FAILED)</strong>", cells)
if not rm:
continue
result = rm.group(1)
labels = label_cells(cells)
label = " ".join(labels) if labels else "???"
hm = re.search(r'href=(https?://[^>\s]+)', cells)
url = hm.group(1) if hm else ""
marker = "❌" if result == "FAILED" else "✅"
print(f" {marker} {label:34} {result:10} {url}")
Step 4: WebFetch console URL 拿原始日志
CI console 是 公开 URL(不需要登录),直接 WebFetch 即可:
WebFetch:
url: https://ci.openeuler.openatom.cn/job/multiarch/job/openeuler/job/aarch64/job/yuanrong-datasystem/6591/console
或用 MCP web-reader:
mcp__web-reader__webReader:
url: https://ci.openeuler.openatom.cn/...
return_format: text
retain_images: false
注意:console 输出通常 100KB+,WebFetch 会触发 Output too large 自动持久化到文件。下一步直接读那个文件。
Step 5: 从 console 文本中提取失败根因
console 文本是 escape 过的 JSON 字符串(\n 字面量),需先 unescape 再 grep:
import json, re
with open("console_raw.json") as f:
data = json.load(f)
text = data[0]["text"]
content = json.loads(text)["content"]
patterns = {
"失败用例名": r"\[ FAILED \] (\w+Test\.\w+)",
"断言失败": r"Value of: \w+\s+Actual: (\w+)\s+Expected: (\w+)",
"错误码": r"code: \[([^\]]+)\]",
"RPC 错误": r"\[(RPC cancelled|RPC_RECV_TIMEOUT|RPC_UNAVAILABLE)[^\]]*\]",
"错误位置": r"(File|Line of code)\s*:\s*([^\n]+)",
"测试时长异常": r"\*\*\*Failed\s+(\d+\.\d+)\s+sec",
}
for name, pat in patterns.items():
matches = re.findall(pat, content)
if matches:
unique = list(dict.fromkeys(matches))[:5]
print(f"=== {name} ({len(matches)} 处) ===")
for m in unique:
print(f" {m}")
print()
Step 6: 总结报告(控制 400 字内)
回报结构:
## PR #N CI 诊断报告
**整体结果**:N 项 PASS / M 项 FAILED
### 失败检查项
- ❌ x86_64 check_build #6577(console URL)
- ❌ aarch64 check_build #6591(console URL)
### 失败用例
- `ObjectClientBigBufferTest.EXCLUSIVE_TestPutAndRemoteGetBigData`(72.24s 后失败)
### 根因(按可能性排序)
1. **直接错误**:`[RPC cancelled ×17 / 60s]` 在 `worker_oc_service_get_impl.cpp:1914`
2. **位置**:`worker0->GetObjectRemote->worker1`
3. **断言**:`Value of: false / Expected: true`
4. **错误码**:`brpc_status_util.h:158` 提示走 BRPC 路径
### 与本 PR 关联性
- 改动是否触及失败路径:是/否
- 默认值翻转后哪些测试首次跑 BRPC 模式:列出
- 是否预存在 flaky:grep master CI 历史看同测试是否曾失败
常见坑
1. 评论 HTML 实体
openeuler-ci-bot 用 ✅(✅)和 ❌(❌)HTML 实体,不是 unicode 字符。grep 时注意:
import html
text = html.unescape(raw_body)
2. 多次 /retest 多条评论
每次 /retest 触发一次 CI,会发一条新的表格评论。不要用第一条(可能过时),用 created_at 最新的。
3. Console URL 不需要鉴权
openeuler CI 的 Jenkins console 是公开的,WebFetch 直接拿即可,不要尝试加 token header。
4. Console 文本双层 JSON escape
WebFetch 返回的 text 字段是 JSON string,里面又是 escape 过的 console 文本(\n 是字面 \ + n,不是换行)。必须 json.loads(text)["content"] 取出真正的多行文本,否则 grep 不到换行相关模式。
5. check_code PASS ≠ CI PASS
check_code 只跑 lint/static check,不编译。真正的编译+测试是 check_build(x86_64/aarch64/openyuanrong 三套)。check_code PASS 但 check_build FAILED 是常见组合,不要看到 check_code PASS 就以为 CI 通过。
6. openyuanrong check_build vs x86_64/aarch64 check_build
openyuanrong check_build:项目自带 bazel build + 项目自带测试(宽松)
x86_64/aarch64 check_build:openEuler 发行版构建(rpmbuild + 完整 ST 套件,严格)
项目 PASS 但 multiarch FAIL 的情况,多半是 ST 测试在某种模式下挂(如本 PR 把 BRPC 默认开 → ZMQ 跑通的 ST 在 BRPC 模式下挂)。
7. 同 PR 多次重跑编号
openeuler CI 每次重跑 build ID 会递增(如 #6575 → #6577)。报告里写明 build ID 方便回溯。
Step 5.5: SPA + WAF 报告用 WSL2 → Windows Edge 绕过
适用场景:报告 URL 是 SPA(如 openlibing.com/apps/entryCheckDashCode/...)或被 WAF 拦截(如 api.openlibing.com 返回 418)。
为何 WebFetch/curl 拿不到:
- SPA 只返回 HTML shell + JS bundle,实际数据由 JS 异步加载
- WAF(CloudWAF 等)会拦截
curl 默认 User-Agent,返回 418
- WSL2 内的 selenium 跑不起来(无 GUI,msedgedriver 状态 127 退出)
绕过方案:WSL2 → Windows PowerShell → Windows 上的 Edge
主会话写 Python selenium 脚本到 /tmp/,cp 到 /mnt/d/(Windows 可见),用 PowerShell 调 Windows Python 执行。Edge 在 Windows 上跑,渲染完整 SPA。
完整脚本模板
import sys, time, re
URL = "https://www.openlibing.com/apps/entryCheckDashCode/MR_xxx/yyy?projectId=300024&codeHostingPlatformFlag=gitcode"
from selenium import webdriver
from selenium.webdriver.common.by import By
options = webdriver.EdgeOptions()
options.add_argument("--headless=new")
options.add_argument("--window-size=1920,1080")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Edge(options=options)
try:
driver.set_page_load_timeout(60)
driver.get(URL)
time.sleep(20)
body = driver.find_element(By.TAG_NAME, "body").text
print(f"=== BODY length: {len(body)} ===")
print(body[:8000])
try:
pre_text = driver.find_element(By.TAG_NAME, "pre").text
print("=== PRE (Jenkins console) ===")
print(pre_text[:5000])
except: pass
finally:
driver.quit()
WSL2 调用 Windows 执行
cp /tmp/fetch_spa.py /mnt/d/tmp_fetch_spa.py
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy Bypass -Command \
"& 'D:\Program Files\Python3.13\python.exe' 'D:\tmp_fetch_spa.py'" 2>&1 | head -100
关键细节
- Windows Python 路径:探测
D:\Program Files\Python3.13\python.exe 或 C:\Python3xx\python.exe
- Windows selenium + Edge:Windows 上需
pip install selenium webdriver-manager,Edge 自带 msedgedriver
- headless=new:Edge 109+ 推荐
--headless=new(不是旧 --headless),更兼容现代 SPA
- time.sleep(20):给 SPA 时间 fetch + render。codecheck dashboard ~15s,Jenkins console ~10s
- GBK 乱码处理:Windows 中文 locale 输出 GBK,关键英文部分(规则 ID、文件路径、test name)能看懂;中文段落("未处理"/"已解决")可对照上下文理解
反模式(不要用)
python3 -c "from selenium import webdriver; ..."
curl -H "User-Agent: Mozilla/..." $URL
实战案例(PR #1251 v9 codecheck 缺陷抓取)
通过此技术成功拿到 G.STD.02-CPP 缺陷详情:
- 文件:
src/datasystem/common/rpc/brpc_client_unary_writer_reader.h:178
- 描述:
Use std::string instead of char*
- 行:
responseAttachment_.copy_to(static_cast<char *>(dest) + copied, ...)
- 修复:改
char* → uint8_t* 绕过 false positive
WebFetch + WSL2 selenium 都拿不到,只有 WSL2 → Windows Edge 拿到了真实缺陷内容。
进阶:批量分析多个失败用例
CI 可能挂多个测试。提取所有 [ FAILED ] XxxTest.Yyy 后,按测试名归类:
failed_tests = re.findall(r"\[ FAILED \] (\w+\.\w+)", content)
suites = {}
for t in failed_tests:
suite, case = t.rsplit(".", 1)
suites.setdefault(suite, []).append(case)
for suite, cases in suites.items():
print(f"{suite}: {len(cases)} cases failed")
for c in cases[:3]:
print(f" - {c}")
与其他 skill 协作
- 诊断完 → 修复:用
vibe-review 自检视代码改动
- 修复完 → 重测:用
remote-build 在 compile_32c_05 上跑相关单测验证
- 重测 PASS → 推送:用
gitcode-api 增量 push + /retest
- /retest 后 → 再诊断:回到本 skill
真实案例(yuanrong-datasystem 2026-07-07)
案例 A:PR #1250 全 PASS
openeuler-ci-bot 评论解析结果:
| Check | 结果 | Build ID | Console |
|---|
| check_code | ✅ SUCCESS | #6467(共享) | multiarch/.../trigger/.../6467/console |
| check_package_license | ✅ SUCCESS | (共享 #6467) | — |
| check_sca | ✅ SUCCESS | (共享 #6467) | — |
| x86_64 check_build | ✅ SUCCESS | #6570 | multiarch/.../x86-64/.../6570/console |
| aarch64 check_build | ✅ SUCCESS | #6584 | multiarch/.../aarch64/.../6584/console |
| openyuanrong check_build | ✅ SUCCESS | #2491 | manual-jobs/.../Test_Datasystem_Bazel_arm/2491/console |
| openyuanrong check_build | ✅ SUCCESS | #2566 | (x86 镜像) |
案例 B:PR #1251 部分失败
同一个表解析:
| Check | 结果 | Build ID |
|---|
| check_code | ✅ SUCCESS | #6474 |
| check_package_license | ✅ SUCCESS | — |
| check_sca | ✅ SUCCESS | — |
| x86_64 check_build | ❌ FAILED | #6577 |
| aarch64 check_build | ❌ FAILED | #6591 |
| openyuanrong check_build | ✅ SUCCESS | #2498 |
| openyuanrong check_build | ✅ SUCCESS | #2573 |
对比 A vs B:check_code + openyuanrong check_build 两者都 PASS,但 x86_64/aarch64 check_build 在 B 失败。说明:
- 代码 lint 没问题(check_code PASS)
- 项目自带 bazel build 没问题(openyuanrong PASS)
- 但跑完整 openEuler ST 套件就挂 → ST 测试在新模式下(本例 BRPC 默认开)有问题
Console URL 提取出来后 WebFetch #6591 的 console,发现失败用例 ObjectClientBigBufferTest.EXCLUSIVE_TestPutAndRemoteGetBigData,传输 2GB 数据时 BRPC RPC cancelled。根因:BRPC 默认 max_body_size=64MB,没在 rpc_server.cpp:117 的 ServerOptions 里设大 → 2GB RPC 必然失败。
启示:单纯 check_code PASS 容易误判
典型陷阱:开发者看 PR CI 看到 check_code ✅ 就以为 CI 通过,急着合并。实际上 x86_64/aarch64 check_build 才是真编译+跑测试,需要单独看。本 skill 通过表格解析会强制列出所有 check 的结果,避免这种误判。