构建状态增加平台终态只读核对,修复回调丢失后任务长期卡在构建中

工作流在脚本收尾前失败(拉取/编译/runner 异常)或 GENURL 不可达时,
结束回调会永久丢失,而等待页 30 秒轮询与列表页此前只读本地库,
任务只能等 6 小时本地超时,平台侧已失败也无法及时反映。

- GitHub/Gitea 后端新增只读 get_run(),仅在平台报告终态时返回结论,
  在途、非 200、网络异常、坏 JSON 一律返回 None;不取消、不写平台
- 等待页 30 秒轮询同步核对一次真实状态:20 秒节流、6 秒超时、异常静默
- 我的构建/后台仪表盘改为后台守护线程核对,页面渲染零等待
- 仅做非终态到终态的条件更新,回调抢先到达时不会被覆盖
- 停止/删除纯本地语义、6 小时本地超时兜底等现有逻辑保持不变
This commit is contained in:
naeeo
2026-09-30 12:46:05 +08:00
parent b0219603be
commit d78c071818
5 changed files with 203 additions and 12 deletions
+13
View File
@@ -18,6 +18,14 @@ class DispatchResult:
html_url: str = '' # 平台上的运行详情页地址
@dataclass
class RunInfo:
"""平台单次运行的状态快照。"""
finished: bool = False # 是否已结束(拿到终态结论)
status: str = '' # finished=True 时为 rdgen 内部终态码
html_url: str = '' # 平台上的运行详情页地址
# rdgen 目标平台 -> 工作流文件名(两平台同名)
WORKFLOW_FILES = {
'windows': 'generator-windows.yml',
@@ -53,6 +61,11 @@ class BuildBackend:
def log_url(self, run_id) -> str:
raise NotImplementedError
def get_run(self, run_id, *, timeout=8):
"""只读查询单次运行:已结束返回 RunInfo(finished=True, status=内部终态码);
仍在途、查询失败或不支持时返回 None(调用方按「仍在进行」处理)。"""
return None
# ---- 后台连接测试 ----
def test_connection(self) -> dict:
"""返回 {'ok': bool, 'message': 中文说明},不抛异常。"""
+53 -3
View File
@@ -6,11 +6,25 @@ API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实)
{workflow_run_id, html_url, run_url},不传该参数为 204;
- 鉴权:Authorization: token <PAT>。
注意:本平台不主动查询/取消 Gitea 上的运行。构建状态完全以工作流结束时的
回调(update_github_run)为准,停止/删除只作用于本平台本地记录。
注意:本平台不主动取消 Gitea 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
"""
from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, WORKFLOW_FILES, BuildBackend
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
# 部分 Gitea 版本 run.status 直接返回终态(无 completed+conclusion 包裹)
_GITEA_TERMINAL = {'success', 'failure', 'cancelled', 'skipped', 'timed_out', 'stopped', 'dead'}
# Gitea 结论 -> rdgen 内部终态
_GITEA_CONCLUSION_MAP = {
'success': 'success',
'failure': 'failure',
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
'stopped': 'cancelled',
'dead': 'failure',
}
class GiteaBackend(BuildBackend):
@@ -130,6 +144,42 @@ class GiteaBackend(BuildBackend):
print(f'查询 Gitea 最新运行失败:{e}')
return None
# ---- 只读状态查询(回调丢失时的终态兜底) ----
def get_run(self, run_id, *, timeout=8):
url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}')
try:
resp = ghnet.get(
url, token=app_settings.get_value('GITEA_TOKEN'),
timeout=timeout, flavor='gitea', proxy=self._proxy(),
)
except Exception as e:
print(f'查询 Gitea 状态出错:{e}')
return None
if resp.status_code != 200:
return None
try:
data = resp.json()
except ValueError:
return None
html_url = data.get('html_url') or self.log_url(run_id)
status = (data.get('status') or '').lower()
conclusion = (data.get('conclusion') or '').lower()
if status == 'completed':
return RunInfo(
finished=True,
status=_GITEA_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=html_url,
)
# 部分 Gitea 版本直接在 status 返回终态(无 completed 包裹)
if status in _GITEA_TERMINAL:
return RunInfo(
finished=True,
status=_GITEA_CONCLUSION_MAP.get(status, 'failure'),
html_url=html_url,
)
# queued/in_progress/waiting/blocked 等在途状态返回 None,交给回调与下次核对
return None
# ---- 日志链接 ----
def log_url(self, run_id):
if not run_id:
+40 -3
View File
@@ -1,13 +1,24 @@
"""GitHub Actions 构建后端(迁自 views.py / web_views.py 的原有逻辑,行为保持一致)。
注意:本平台不主动查询/取消 GitHub 上的运行。构建状态完全以工作流结束时的
回调(update_github_run)为准,停止/删除只作用于本平台本地记录。
注意:本平台不主动取消 GitHub 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
"""
from .. import app_settings, ghnet
from .base import BackendError, DispatchResult, WORKFLOW_FILES, BuildBackend
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
API = 'https://api.github.com'
# GitHub conclusion -> rdgen 内部终态;无人值守构建不会有人处理审批,
# action_required/neutral/stale/startup_failure 等异常结论一律按失败呈现
_GH_CONCLUSION_MAP = {
'success': 'success',
'failure': 'failure',
'cancelled': 'cancelled',
'skipped': 'skipped',
'timed_out': 'timed_out',
}
class GitHubBackend(BuildBackend):
name = 'github'
@@ -57,6 +68,32 @@ class GitHubBackend(BuildBackend):
pass
return DispatchResult(run_id=data.get('workflow_run_id'), html_url=data.get('html_url') or '')
# ---- 只读状态查询(回调丢失时的终态兜底) ----
def get_run(self, run_id, *, timeout=8):
user = app_settings.get_value('GHUSER')
repo = app_settings.get_value('REPONAME')
url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}'
try:
resp = ghnet.get(url, token=app_settings.get_value('GHBEARER'), timeout=timeout)
except Exception as e:
print(f'查询 GitHub 状态出错:{e}')
return None
if resp.status_code != 200:
return None
try:
data = resp.json()
except ValueError:
return None
# 只关心终态:在途(queued/in_progress/waiting)返回 None,交给回调与下次核对
if data.get('status') != 'completed':
return None
conclusion = (data.get('conclusion') or 'failure').lower()
return RunInfo(
finished=True,
status=_GH_CONCLUSION_MAP.get(conclusion, 'failure'),
html_url=data.get('html_url') or self.log_url(run_id),
)
# ---- 日志链接 ----
def log_url(self, run_id):
if not run_id: