接口¶
MCP 的三个入口;REST 与 CLI 同构。
MCP 适配层通过 Streamable HTTP 暴露,默认端点是 http://127.0.0.1:8001/mcp。
启动 MCP 服务¶
# 默认 0.0.0.0:8001;端口可用 GHAT_MCP_PORT 调整
rhosocial-github-actions-tracker serve mcp
# 仅本机使用时,建议显式绑定 loopback
rhosocial-github-actions-tracker serve mcp --host 127.0.0.1 --port 8001
GHAT_API_KEY 留空是允许的:此时 MCP 不启用 Bearer 校验,启动日志会给出告警。
公网部署必须提供该 key,这由部署者自行保证;本服务不会因为 key 为空而拒绝启动。
MCP 工具与 REST、CLI 共用同一套业务核心。首次接入建议先调用 ci_status、list_repos
验证连通性;trigger_workflow / rerun_run 会消耗 CI 额度,确认无误后再使用。
配置 OpenCode(V2)¶
配置写在全局 ~/.config/opencode/opencode.jsonc(Windows 为
%USERPROFILE%\.config\opencode\opencode.jsonc),不要写进项目配置。键名是 mcp.<服务器名>:
没有 servers 这一层,写成 mcp.servers.<名字> 会被当成一台名叫 "servers" 的服务器,
静默连不上:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"github-actions-tracker": {
"type": "remote",
"url": "http://127.0.0.1:8001/mcp",
"enabled": true,
"oauth": false,
"headers": {
"Authorization": "Bearer {env:GHAT_API_KEY}"
}
}
}
}
oauth: false:本服务用静态 Bearer,不做 OAuth;关掉自动探测,避免 key 打错时 OpenCode 去开浏览器走一遍发现流程。{env:GHAT_API_KEY}:加载时从进程环境展开。OpenCode 进程必须能读到该变量;从桌面 图标启动时读不到 shell 里临时设置的变量,此时可把令牌直接写进全局配置(该文件不入库)。- 四家主流 agent(Claude Code、Codex、OpenCode、Command Code)的完整配置与作用域模型见 编码 Agent 接入 MCP。
验证¶
opencode mcp list
显示 github-actions-tracker connected 即接通。异常时先用 opencode mcp debug github-actions-tracker 查看 HTTP 与
鉴权细节,或在 TUI 里用 /mcps 重连。
鉴权¶
MCP 与 REST 共用 GHAT_API_KEY。设置了该变量时,/mcp 要求
Authorization: Bearer <key>,与上面的客户端配置一致;比较是常量时间的,错误的令牌
由 SDK 以 401 拒绝。该 key 代表 operator,持有 ghat:read、ghat:write
与未使用的 ghat:admin 全部 scope,请像保护 REST key 一样保护它。
GHAT_API_KEY 为空时,MCP 和 REST 都不鉴权,启动日志会分别打印
MCP bearer verification is disabled / REST API authentication is disabled 告警。
这是受支持的部署方式(本机、内网、或由前置网关鉴权),但公网直连时必须由部署者提供
key。CLI 不使用这个 key,credential、serve webhook 等命令在 key 为空时照常运行。
serve mcp 默认绑定 0.0.0.0;仅本机使用时请显式使用 --host 127.0.0.1。
用提示词驱动¶
工具名与参数由 MCP 提供,agent 自己会挑;用户只需要把意图说清楚,给仓库名、分支或提交号、 工作流名这几样,比说"看看 CI"有效得多。下面两段是可直接复制的开场提示。
中文:
用本服务看一下 rhosocial/python-activerecord 在 main 上最新一次 test.yml 的结果。
如果还在跑,等到它结束再告诉我;失败了就指出是哪个 job、哪个 step,并把关键错误行贴出来。
用本服务一次性看一下 python-activerecord 系列 11 个仓库各自最新一次运行的状态,
汇总成一张表:仓库 / 结论 / 分支 / 耗时,失败的要标出来。
用本服务统计一下 python-activerecord 系列里,过去三次运行中哪些仓库连续失败,
以及每个失败仓库最近一次失败是哪个 job、哪个 step。
我刚推了 3 个后端仓库(mysql / postgres / mariadb)的 fix/column-constraint-gates 分支,
用本服务分别查一下各自最新一次的运行状态,汇总成一张表:仓库 / 结论 / 耗时 / 失败 job。
用本服务触发 rhosocial/python-activerecord-postgres 的 test.yml(ref: main),
然后跟踪到结束。失败了先别重跑,把失败原因给我。
English:
Use the tracker to check the latest test.yml run for rhosocial/python-activerecord on main.
If it is still running, wait until it finishes. If it failed, tell me which job and step
failed and show the key error lines.
I just pushed fix/column-constraint-gates to three backend repos (mysql, postgres, mariadb).
Ask the tracker for the latest run in each and give me one table: repo / conclusion / duration /
failing job.
Ask the tracker for the newest run of all eleven python-activerecord repositories in one go,
then give me only the ones that are red, with the failing job and step for each.
Use the tracker to dispatch test.yml on rhosocial/python-activerecord-postgres (ref: main) and
track it to completion. If it fails, do not re-run: report the failure first.
写提示词时的三条约定:
- 别让 agent 轮询。说"等到结束",agent 会调
wait_for_run;说"每 10 秒查一次",它会把 上下文烧在重复调用上。 - 触发类请求要带 ref,并且明确"只跑一次"。
trigger_workflow不是幂等的,含糊的答复会让 agent 重复派发,直接多烧一次 CI。 - 失败时先要原因,别先说"重跑"。先
get_job_log拿到错误行,再决定重跑还是改代码。
任务跟踪¶
一次典型的跟踪是几个工具接力,agent 通常不需要你指挥具体调哪个:
| 阶段 | 工具 | 说明 |
|---|---|---|
| 概览 | recent_runs |
一批仓库各自最近 N 次运行的摘要,一次调用回答"这些仓库现在什么状态"。上限 20 个仓库 × 10 条;某个仓库失败只在原位报错,不影响其他仓库 |
| 定位 | find_runs |
单个仓库按分支/提交/PR/工作流/状态查;每次都读 GitHub,所以能补上漏收的 webhook |
| 等待 | wait_for_run |
单次阻塞默认 45 秒,上限由 GHAT_WAIT_MAX_SECONDS 决定(默认 300 秒)。返回 timed_out=true 表示还在跑,不是失败,再调一次即可 |
| 归因 | get_run → get_job → get_job_log |
get_run 给 job 名册与 failed;get_job 给该 job 的每个 step 与 has_log;get_job_log 读那一个 step 的文本 |
选择建议:跨多仓库问"谁红了"用 recent_runs;盯一个仓库的某次提交或某条 PR 用 find_runs。一次投递的写入在 pool.transaction() 里,失败则整体回滚;连接由请求上下文解析,不写模型类属性。
两者都只给摘要。要 job 名册再调 get_run,要某个 job 的 step 与日志再调 get_job /
get_job_log——这是刻意的分层:每层只返回自己那一层的对象,避免一次调用把上下文灌满。
recent_runs 是批量读:仓库与 workflow 各一条查询,每个仓库一条 run 查询("每个仓库取最新 N 条"
没法用一条 IN 查询表达),15 个仓库约 16 条 SQL、1 秒内。它不加载 jobs/steps——摘要用不到。
某个仓库本地一条 run 都没有时会去 GitHub 确认一次,并把"确实没有"记 5 分钟,否则一个空仓库会让
每次批量调用都多花几秒。
# 概览:15 个仓库一次问完(实测 3.9 秒,纯读库)
recent_runs(repositories=[...], limit=1)
# 单仓库定位:某分支的最新一次
find_runs(repository="rhosocial/python-activerecord", branch="main", latest=True)
列表在哪个键下¶
每个工具按内容命名自己的列表键,且空结果与非空结果用同一个键:
| 工具 | 列表位置 | 空结果也长这样 |
|---|---|---|
find_runs |
items[] |
{"items": [], "repository": "..."} |
recent_runs |
repositories[].runs[] |
每个仓库条目带 runs: [] 或 error |
list_repos |
列表模式 items[];单仓库模式 workflows[] |
items: [] / workflows: [] |
get_run |
jobs[](名册,不含 steps) |
jobs: [] |
get_job |
steps[] |
steps: [] |
find_runs 曾经在"有结果"时返回 items、"无结果"时返回 runs——按 runs 读的调用方
只会拿到 undefined,且 hint 里没有任何提示。现在两条路径都是 items,并且 hint 明说
"Results are under items[]"。
触发与查询都可以用其中任一形式:显示名(Changelog Check)、文件名(changelog-check.yml)、
完整路径(.github/workflows/changelog-check.yml)或数字 id。名字比较忽略大小写与首尾空格,
因为 GitHub 会原样保留作者写的大小写。
名字对不上时,错误信息会列出该仓库全部可用工作流,形如 名字 (路径)。按列出的原文再传一次,
不要猜拼写——workflow_not_found 的含义是"没有这一条",不是"再试试别的写法"。
三种"触发不了"是不同的错误码,处理方式也不同:
| 错误码 | 含义 | 下一步 |
|---|---|---|
workflow_not_found |
本地没有匹配的工作流 | 看错误里列出的 名字 (路径),原文照传 |
workflow_not_dispatchable |
工作流存在,但没声明 on: workflow_dispatch |
没有产生 run,别重试;推一次提交,或给 workflow 加该触发器 |
trigger_no_write_capability |
绑定凭据没有 Actions 写权限 | 补权限后重新探测;这是触发前唯一的门 |
重跑另有一组,与令牌无关,不要照着 credential_* 去换 token:
| 错误码 | 含义 | 下一步 |
|---|---|---|
run_in_progress |
这次 run 还在跑,GitHub 拒绝重跑(403 "This workflow is already running") | 用 wait_for_run 等它结束,或 cancel_run 取消后再重跑;换令牌没有用 |
run_state_conflict |
这次 run 的当前状态不允许该操作(例如对已结束的 run 取消) | get_run 看当前状态,别原地重试 |
rerun_run 的响应直接带 run_attempt、previous_attempt、advanced:advanced 为 true
就是"这次是第 N 次 attempt",不必再去比 GitHub;为 false 说明 GitHub 还没反映新增的 attempt,
稍后用 get_run 确认即可,不要连续重跑。
failed_only=true 走 GitHub 的 rerun-failed-jobs:只重跑最新 attempt 里失败的 job
(GitHub 也会重跑它们依赖的 job,reran_jobs 只列本服务选中的那些)。两个前置条件在本地先查:
run 未完成、或最新 attempt 没有失败 job 时不会发请求,直接报 invalid_argument——
那两种情况下 GitHub 会回 403,读起来像权限问题,而这里不是。默认 false,且 false 就是全部
job:说“重跑”的人不会因为某个默认值得到更窄的那一个。
秒数花在哪¶
duration_seconds 是这次 attempt 的墙上时间——GitHub 报的那个窗口,里面包含等 runner
的排队。每次读 run(get_run / find_runs / recent_runs)都另给三个数:
| 字段 | 回答什么 |
|---|---|
job_span_seconds |
第一个开始的 job 到最后一个结束的 job。等待预算该用的就是这个;二十秒就失败的那种 job 同样结束这段区间,那是真实的结束 |
queue_seconds |
这次 attempt 在开始之前等了多久。"这个 workflow 慢"与"runner 忙"需要的应对不同 |
succeeded_job_span_seconds |
只算通过的 job 的同一区间,用来对绿色 workflow 估一个等待 |
null 表示"还没有 job 可量"(job 未同步),不是 0。trigger_workflow 与超时的
wait_for_run 用同一套数字发布 typical_duration_seconds / observed(p50 / p90)——
调用方等的那个数和 run 报出来的那个数是同一次测量。
时间戳¶
所有对外时间戳都是时区敏感(timezone-aware)的,并且一律以 UTC 表示,ISO-8601 带
显式 +00:00 偏移——是"把偏移固定成 UTC",不是"去掉时区"。数据库用 timestamptz 存 UTC,
视图层统一归一化;服务不猜访问者时区、也不做本地化,那是调用方展示层的事。
之前同一个响应里会同时出现 +08:00(数据库按连接时区渲染)与 +00:00(GitHub 载荷自带偏移),
读起来像两种时间;现在只剩一种。
读某一次 attempt¶
get_run 有 attempt 参数。不传就读最新一次(GitHub 对 run id 的默认语义);传了则每次都
向 GitHub 读这一次(/runs/{id}/attempts/{n}),与本地行比对:缺了补、状态或时间戳不一致就
更正,然后才返回。旧构建留下的脏行、投递丢失留下的陈旧行,都会在被问到时修好——仍然是
"问到了才更新",只是问的那一刻一定回源。
返回里的 attempt 是这次读的序号,is_latest 说明它是不是当前那次;读旧 attempt 时 jobs[]
是当时存下的 job(GitHub 的 job 端点只返回最新 attempt 的,不能拿来冒充),hint 会明说
"这是第 N 次,共 M 次,当前是第 M 次"。
序号超出范围返回 attempt_not_found(run 存在,只是没有这个序号)——它与 run_not_found
不同:前者去核对序号,后者去核对 run id。
step 日志:GitHub 只提供一小时,取回来就永久保存¶
GitHub 有两个日志来源,本项目都用:
| 来源 | 内容 | 可得性 |
|---|---|---|
GET /actions/jobs/{id}/logs |
一个 job 的合并文本 | 90 天内随时可读,存进 ghat_job_logs |
GET /actions/runs/{id}/logs |
zip:合并文件 + <job>/<step 号>_<step 名>.txt + system.txt |
按 step 拆分的文件只在 run 结束后约一小时内由 GitHub 提供,之后同一归档只剩合并文件,边界不可恢复 |
一小时是取回(采集)的截止时间,不是保存期限。 一旦某个 step 的文本被取回并写进
ghat_step_logs,它就是本服务的普通数据:与 GitHub 的保留期无关,读多少次、隔多久读都一样。
截止时间唯一决定的是"还能不能再补采",不影响"已经采到的能不能读"。
三条边界,一次说清:
| 问题 | 答案 |
|---|---|
| 多久之内必须取回? | run 结束后 GHAT_LOG_CAPTURE_WINDOW_MINUTES(默认 60 分钟);过了只能读该 job 的合并日志 |
| 取回之后能存多久? | 没有时间上限。ghat_step_logs / ghat_job_logs 与其它表一样长期保留,读路径不做任何时效判断 |
| 什么会让已存的日志消失? | 只有运维显式清理:db cleanup --logs-before <时长>(默认不动),或 db reset / db drop;仓库退订也会删掉该仓库的数据 |
因此 step 日志是采集,不是随时可读:
- 采集发生在
workflow_runcompleted 投递到达时,以及截止时间之前第一次有人读 step 日志时; 已采集的不会重复抓。不在workflow_job完成时抓:归档是 run 级的,run 结束前GET /actions/runs/{id}/logs一律 404(实测一个 12 job 的矩阵,每个 job 完成都 404); - 归档文件是异步写入的,run 刚结束时可能只到了几份。读取路径会为此重试: 每次重新拉归档,间隔 60 秒,最多 6 次;404/5xx 也算"还没好",同样重试; webhook 只做单次尝试,不占用投递时间;
- 存进
ghat_step_logs,与 step 一对一、允许缺失;system.txt不入库; - 没赶上采集就如实返回缺失(
reason是capture_closed/not_a_step_archive等), 绝不拿另一个 step 的文本、也不从合并文本里猜边界; - 一个 job 的 step 全都没有日志时,调用方读该 job 的合并日志自行分析——这是设计上的退路。
get_job的capture块描述的是采集进度:{expected_logs, captured_logs, state, window_closes_at}。
两个计数是日志文件数,不是 step 数:skipped 的 step、以及从未开始的 step,在 GitHub
侧同样没有文件,所以 expected_logs 会小于 steps 的长度——这个差额不是漏采。
partial 表示归档仍在写入,用 refresh_logs=true 再等一次;closed 表示 GitHub 已不再
提供这个 run 的 step 分片,缺的补不回来了(已采到的照常可读);window_closes_at 就是那个
截止时间点。
对应到工具:
get_job(job_id) 每个 step 带 has_log / log_lines / failed
get_job(job_id, refresh_logs=true) 再采集一次(归档还在写入时最多等 6 分钟)
get_job_log(job_id, step=N) 第 N 个 step 自己的日志文件
get_job_log(job_id) 失败 job 的失败 step;否则整个 job 的合并文本
get_job_log(job_id, whole_job=true) 明确要合并文本
get_job_log(job_id, failed_steps_only=true) 一次拿全部失败 step 的文本(批量便利)
get_run_failed_logs(run_id) 整个 run 里失败 job 的失败 step,按文本分组
search_job_log(job_id, query, step=N) 在第 N 个 step 里搜;不带 step 搜合并文本
search_job_log 与 get_job_log 用同一个 step 参数:返回里的 source(job/step)与
step 说明行号属于哪个作用域,照它把 from_line 传回去即可。step 没采集到时返回
reason,不是 no_match——没文档和没匹配是两件事。
不指定作用域时,失败的 job 从失败的 step 读起。 回答里带 scoped_to_failed: true——
自行缩小了范围的回答不能读起来像一份完整文档;whole_job=true 明确要合并文本。失败 step
当初没采集到时返回合并文本并附 scope_note 说明原因,而不是给一个"step 里什么都没有"的
空回答。这个选择只读 step 的结论,从不读日志文本。
N 个 job 打印同一条失败,就是一个组。 get_run_failed_logs(run_id) 按逐字节相同的文本
分组:原文只给一份,外加打印它的 steps(job、step 号、名称、结论)。矩阵里十二个 job 因
同一个原因失败是常态,这个工具一次调用回答"一个原因还是多个"。分组故意是精确的——判定
"两段不同文本意思相同"属于解释,项目改了输出的那天就会判错。没采集到的失败 step 列在
steps_without_logs 里,绝不顶替。
日志读取的几个参数:tail(尾部 N 行)、grep(带上下文找错误)、from_line/to_line
(显式区间)。窗口被截断时响应里带 truncated、next_from_line、next_to_line,
可以按行号续读。
默认窗口就是最后 200 行,不猜内容。 本服务是通用 CI 跟踪器,不知道某个项目会打印
pytest 摘要、##[error] 标记,还是 runner 的收尾日志;按这些特征去"找重点",对任何别的项目
都是错的。所以窗口只做寻址:
- 不传参数 → 文档最后 200 行;
tail=N→ 最后 N 行;from_line/to_line→ 显式区间;grep→ 调用方给的正则(非法时退化为字面量),带context行上下文。
search_job_log 默认给最后那些匹配配上下文(prefer="tail"):日志是边跑边追加的,
结论在末尾、初始化噪音在开头;全部匹配位置照样报告,prefer="head" 只把文本挪到另一端。
响应里的相关字段:
| 字段 | 含义 |
|---|---|
returned_lines |
实际返回的行数。grep 选出的行不连续,last_line - first_line + 1 会算多 |
truncated |
选区之外还有行;配合 first_line / last_line 续读 |
matches |
用了 grep 时命中的行号 |
empty_reason |
内容为空时说明是哪个条件筛空的:grep_no_match / window_outside_document / log_empty / tail_not_positive / filters_selected_nothing |
next |
针对该原因的下一步建议(换短 pattern、ignore_case、改用 tail 等) |
total_lines 是整份日志的行数,returned_lines 是本窗口的行数。两者一起看:
total_lines: 3213 而 returned_lines: 0 不是"这个 job 没输出",而是"筛选条件没匹配到"——
empty_reason 会明说是哪个条件。
failed_steps_only 读的是已采集的失败 step(结论属于 failure / cancelled /
timed_out / startup_failure / action_required / stale 的那些),不解析日志内容去找
##[error];没有任何失败 step 时返回 reason: no_failed_steps,有失败 step 但都没采集到
时返回采集的原因。conclusion 为 null 的 step 不算失败——那是 GitHub 元数据还没填全。
需要在自己的日志格式里精确定位时用 grep(合并文本)或 search_job_log(可按 step):
get_job_log(repository=..., job_id=..., grep="FAILED", context=2)
search_job_log(repository=..., job_id=..., query="ModuleNotFound", step=6)
长任务(这套 CI 是十几分钟的矩阵)有两种处理方式,二选一:
- 让 agent 反复调
wait_for_run(默认每次 45 秒,timed_out就再来一次); - 或者先用
find_runs确认 run 存在,把 run id 记下来,过一会儿再查。
wait_for_run 单次时长由调用方传 timeout_seconds 决定,上限是 GHAT_WAIT_MAX_SECONDS。
把它调大可以一次等更久,但客户端自己的执行超时必须相应放宽,否则请求会先被客户端掐断。
OpenCode 的 mcp.timeout.execution 默认 12 小时,通常不用改。
等待时长不要猜。 该 workflow 最近的已结束 run 已经给出了答案:
trigger_workflow/rerun_run返回typical_duration_seconds: {samples, p50_seconds, p90_seconds};wait_for_run超时时返回observed(同样三项)和suggested_timeout_seconds(p90 向上取整, 夹在[10, GHAT_WAIT_MAX_SECONDS]),hint 会写清"再等约 N 秒";- p90 超过单次上限时,hint 会明确说"这是单次上限,需要不止一次等待";
- 新订阅或新 workflow 没有历史,这两个字段是
null——此时没有可依据的时长,不要编一个。
样本取的是所有已终结的 run(成功的和失败的):问题问的是"多久结束",二十秒就失败的一次 也是真实的结束时间,只统计成功会让常失败的工作流被低估。
常见问题¶
| 现象 | 原因 | 处理 |
|---|---|---|
opencode mcp list 显示 needs authentication |
没配 oauth: false,OpenCode 去做 OAuth 发现 |
补上 oauth: false,本服务只支持静态 Bearer |
connected 但每次调用都 401 |
客户端 header 里的 key 与服务端 GHAT_API_KEY 不一致 |
两边用同一个值;从桌面图标启动的 OpenCode 读不到 shell 临时变量,可写进全局配置 |
连接被拒绝 / fetch failed |
8001 没有发布到宿主,或绑到了 127.0.0.1 |
看 docker compose ps 的 PORTS 列;需要从别的主机访问时把 GHAT_MCP_PORT_HOST 打开并确认防火墙 |
容器 Up 但所有调用 500、/readyz 503 或 500 |
数据库连不上:GHAT_DB_HOST 是动态 DNS,IP 变了以后容器里缓存的旧地址失效。日志里是 Failed to connect to PostgreSQL: ... server closed the connection unexpectedly |
先 docker compose ps 看是否 (healthy)(compose 已给 web/mcp 配 /readyz 健康检查);再在宿主上用同一账号 Test-NetConnection <host> -Port <port> 对比解析结果;重启 web 和 mcp 让它们重新解析,不需要重建镜像 |
容器 (unhealthy) |
同上,或数据库确实宕了 | docker logs github-actions-tracker-mcp-1 --tail 30;数据库恢复后 docker restart 即可 |
日志里成片 Failed to acquire connection within 30.0 seconds |
连接池耗尽:Pool stats 里 in_use≈max_size 是并发饱和;available=0, in_use=0 是旧版本的许可泄漏 |
前者调大 GHAT_POOL_MAX(长轮询也占连接);后者重启容器,并升级到含 a55ebbd9 的镜像。详见连接池 |
docker compose pull 之后 up -d / docker start / docker rm -f 全部卡住 |
本机 Docker Desktop 的容器运行时被卡住(与上文单文件 bind mount 那条不同,这条与挂载无关,pull 完新镜像后出现) |
退出并重启 Docker Desktop,再跑一次 up -d;不需要动 compose 文件 |
webhook 日志里出现过 ClientDisconnect 的 ASGI traceback |
GitHub 在发送中途放弃(它自己超时),旧版本把它当成未处理异常打了整段堆栈 | 含本次修复的镜像只记一行 client disconnected before the body arrived;GitHub 会重投,或下一次 find_runs 补齐,无需处理 |
init 退出码 1、web/mcp 停在 Created |
新镜像一启动就失败(典型是 import 环:ImportError: cannot import name ... from partially initialized module)。up -d 只会说 init 失败,web/mcp 根本不会启动 |
先看 docker logs github-actions-tracker-init-1 --tail 40 拿真实异常;修好重建镜像再 up -d。ps 里的 Created 不是"在跑",是"没起来" |
工具报 Error executing tool ... |
这是预期的失败形态:MCP 层 isError: true,文本是 JSON 信封 |
读信封里的 code / hint / action_url。repository_not_subscribed 要运营方加仓库,不是你换参数能解决的 |
repository_not_subscribed |
该仓库没订阅 | 按 action_url 加订阅,或在容器里 repo subscribe --name owner/repo --credential pat/work |
wait_for_run 一直 timed_out |
任务比单次上限长 | 继续调;或调大 GHAT_WAIT_MAX_SECONDS 并同步放宽客户端执行超时 |
trigger_* 被拒 |
令牌没有 Actions 写权限 | 看 ci_status 的 can_trigger 与 trigger_scope;权限问题由运营方处理 |
| 触发后不确定是否成功 | trigger_workflow 非幂等,响应不明确 |
先 find_runs 核对,不要重试——重试会起第二次运行 |
| 日志看起来"就这么多" | 窗口被截断(truncated: true) |
用 next_from_line / next_to_line 续读,或换 grep 定位 |
get_job 里某个 step has_log: false |
该 step 的文本没取回来:GitHub 提供按 step 拆分文件的时间窗(约一小时)已过,或当时归档里就没有分片 | 读同一个 job 的合并文本(get_job_log(job_id=...))自己定位;已采集到的 step 不受影响,照常可读 |
| 怀疑线上还是旧代码 | version 是源码常量,区分不了构建 |
curl -k https://<域名>:18080/readyz 看 build.revision / build.number,与 git rev-parse HEAD 对比;null 表示这个镜像没盖章(自建/本地构建),不是旧版本 |
search_job_log(step=N) 返回 reason 而不是匹配 |
这个 step 没有采集到日志,所以没搜过 | 改搜整 job(去掉 step),或用 get_job_log(job_id=...) 读合并文本 |
什么条件下能触发¶
跟踪(recent_runs / find_runs / get_run / get_job / get_job_log / webhook 入库)没有额外条件:
任何已订阅仓库的 run 都会进库。
主动触发(trigger_workflow / rerun_run)只有一个门:仓库已订阅,且绑定给它的凭据
带 Actions 写权限。没有白名单、没有每小时配额、没有操作日志表——本服务以"单操作者部署,
fine-grained PAT 的 scope 本身就是授权"为前提,触发动作一定来自人的明确指令。
代价必须清楚:拿到这个 MCP 服务的人可以触发 publish.yml 这类发版 workflow,也能在任意
分支烧 CI 分钟。要收紧,就收紧 PAT 的 scope,或不要把服务暴露给不该触发的人。
trigger_workflow 不是幂等的:响应不明确时先 find_runs 核对,重试会起第二次运行。