feat: 同步构建真实状态并支持在列表停止在途任务

- Gitea 运行未结束时探测作业级结论,修复前置作业失败后可复用工作流父作业/等待标签作业长期挂起导致状态永远构建中

- 打开我的构建/管理仪表盘时主动向构建平台同步在途任务状态;创建超 6 小时仍在途的任务判超时

- 构建中任务列表增加停止按钮(Gitea 需 1.28+ 取消 API,旧版本提示去运行页手动 Cancel),终态任务保留删除

- GitHub/Gitea 后端统一 cancel_run 接口
This commit is contained in:
naeeo
2026-09-29 23:45:25 +08:00
parent 7168c71d34
commit 4fd930aa0b
10 changed files with 199 additions and 13 deletions
+71 -1
View File
@@ -12,7 +12,9 @@ from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
# Gitea 未结束状态(blocked=等待审批,waiting=等待 runner)
_GITEA_ACTIVE = {'queued', 'in_progress', 'waiting', 'blocked', 'requested', 'pending'}
_GITEA_ACTIVE = {'queued', 'in_progress', 'waiting', 'blocked', 'requested', 'pending', 'running'}
# Gitea 原生终态(部分版本 run.status 直接返回结论,不走 completed+conclusion)
_GITEA_TERMINAL = {'success', 'failure', 'cancelled', 'skipped', 'timed_out', 'stopped', 'dead'}
# Gitea 结论 -> rdgen 内部状态
_CONCLUSION_MAP = {
'success': 'success',
@@ -20,7 +22,11 @@ _CONCLUSION_MAP = {
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
'stopped': 'cancelled',
'dead': 'failure',
}
# 作业出现这些结论时,整个运行最终必然失败(cleanup 等收尾作业除外)
_JOB_BAD_CONCLUSIONS = {'failure', 'cancelled', 'timed_out', 'dead'}
class GiteaBackend(BuildBackend):
@@ -163,9 +169,73 @@ class GiteaBackend(BuildBackend):
status=_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=html_url,
)
# 部分 Gitea 版本直接在 status 返回终态(无 completed 包裹)
if status in _GITEA_TERMINAL:
return RunInfo(
finished=True,
status=_CONCLUSION_MAP.get(status, 'failure'),
html_url=html_url,
)
# Gitea 已知行为:前置作业失败后,可复用工作流的父作业/等待匹配标签的
# 后续作业可能长期停留在 in_progress/waiting,运行永远不进入 completed。
# 这里检查作业级结论:只要非收尾作业已失败,即可判定整个运行失败。
bad = self._detect_failed_job(run_id)
if bad:
return RunInfo(finished=True, status=bad, html_url=html_url)
# blocked(等待审批)等也算进行中
return RunInfo(finished=False, status=status or 'in_progress', html_url=html_url)
def _detect_failed_job(self, run_id, *, timeout=10):
"""检查运行下各作业:有非 cleanup 作业失败/取消/超时时返回对应终态,否则 None。"""
url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}/jobs')
try:
resp = ghnet.get(
url, token=app_settings.get_value('GITEA_TOKEN'),
timeout=timeout, flavor='gitea', proxy=self._proxy(),
)
if resp.status_code != 200:
return None
data = resp.json()
jobs = data.get('jobs', data if isinstance(data, list) else [])
for job in jobs:
name = (job.get('name') or '').lower()
if name.startswith('cleanup'):
continue
conclusion = (job.get('conclusion') or '').lower()
if conclusion in _JOB_BAD_CONCLUSIONS:
return _CONCLUSION_MAP.get(conclusion, 'failure')
except Exception as e:
print(f'查询 Gitea 作业状态出错:{e}')
return None
# ---- 取消运行 ----
def cancel_run(self, run_id, *, timeout=15):
"""请求平台取消运行。成功无返回;409 表示运行已结束(调用方应改为拉取真实状态)。"""
url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}/cancel')
try:
resp = ghnet.post(
url, token=app_settings.get_value('GITEA_TOKEN'),
timeout=timeout, flavor='gitea', proxy=self._proxy(),
)
except Exception as e:
raise BackendError(f'无法连接 Gitea 构建服务:{e}', kind='network')
if 200 <= resp.status_code < 300:
return
if resp.status_code == 409:
raise BackendError('该构建运行已经结束,无法取消。', kind='finished')
if resp.status_code == 404:
raise BackendError(
'当前 Gitea 版本(1.27 及更早)未开放取消运行的 API,'
'请在 Gitea 的运行页面手动点击 Cancel 停止;升级到 Gitea 1.28+ 后可在此一键停止。',
kind='unsupported',
)
detail = ''
try:
detail = (resp.text or '')[:200]
except Exception:
pass
raise BackendError(f'Gitea 拒绝了取消请求(HTTP {resp.status_code}):{detail}')
# ---- 日志链接 ----
def log_url(self, run_id):
if not run_id: