工作流在脚本收尾前失败(拉取/编译/runner 异常)或 GENURL 不可达时, 结束回调会永久丢失,而等待页 30 秒轮询与列表页此前只读本地库, 任务只能等 6 小时本地超时,平台侧已失败也无法及时反映。 - GitHub/Gitea 后端新增只读 get_run(),仅在平台报告终态时返回结论, 在途、非 200、网络异常、坏 JSON 一律返回 None;不取消、不写平台 - 等待页 30 秒轮询同步核对一次真实状态:20 秒节流、6 秒超时、异常静默 - 我的构建/后台仪表盘改为后台守护线程核对,页面渲染零等待 - 仅做非终态到终态的条件更新,回调抢先到达时不会被覆盖 - 停止/删除纯本地语义、6 小时本地超时兜底等现有逻辑保持不变
130 lines
5.9 KiB
Python
130 lines
5.9 KiB
Python
"""GitHub Actions 构建后端(迁自 views.py / web_views.py 的原有逻辑,行为保持一致)。
|
||
|
||
注意:本平台不主动取消 GitHub 上的运行,停止/删除只作用于本平台本地记录。
|
||
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
|
||
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
|
||
"""
|
||
from .. import app_settings, ghnet
|
||
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'
|
||
label = 'GitHub'
|
||
|
||
# ---- 配置 ----
|
||
def configured(self):
|
||
return bool(app_settings.get_value('GHUSER').strip()) \
|
||
and bool(app_settings.get_value('GHBEARER').strip())
|
||
|
||
def warnings(self):
|
||
warnings = []
|
||
if not app_settings.get_value('GHUSER').strip():
|
||
warnings.append('GitHub 用户名未配置,无法触发构建。')
|
||
if not app_settings.get_value('GHBEARER').strip():
|
||
warnings.append('GitHub 访问令牌未配置,无法触发构建。')
|
||
warnings.extend(self._shared_warnings(include_sh_secret=True))
|
||
return warnings
|
||
|
||
# ---- 工作流选择(windows selfhosted 走 sh-generator-windows.yml) ----
|
||
def workflow_file_for(self, platform, selfhosted=False):
|
||
if selfhosted and platform in ('windows',):
|
||
return 'sh-generator-windows.yml'
|
||
return WORKFLOW_FILES.get(platform, 'generator-windows.yml')
|
||
|
||
# ---- 触发 ----
|
||
def dispatch(self, workflow_file, inputs, *, timeout=20):
|
||
user = app_settings.get_value('GHUSER')
|
||
repo = app_settings.get_value('REPONAME')
|
||
url = f'{API}/repos/{user}/{repo}/actions/workflows/{workflow_file}/dispatches'
|
||
payload = {
|
||
'ref': app_settings.get_value('GHBRANCH'),
|
||
'inputs': inputs,
|
||
'return_run_details': True,
|
||
}
|
||
token = app_settings.get_value('GHBEARER')
|
||
try:
|
||
resp = ghnet.post(url, json=payload, token=token, timeout=timeout)
|
||
except Exception as e:
|
||
raise BackendError(f'无法连接 GitHub 构建服务:{e}', kind='network')
|
||
if resp.status_code not in (200, 204):
|
||
raise BackendError('GitHub 构建服务拒绝了启动请求,请检查服务配置或稍后重试。', kind='rejected')
|
||
data = {}
|
||
try:
|
||
data = resp.json()
|
||
except ValueError:
|
||
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:
|
||
return ''
|
||
return (
|
||
f"https://github.com/{app_settings.get_value('GHUSER')}"
|
||
f"/{app_settings.get_value('REPONAME')}/actions/runs/{run_id}"
|
||
)
|
||
|
||
# ---- 连接测试 ----
|
||
def test_connection(self):
|
||
user = app_settings.get_value('GHUSER').strip()
|
||
repo = app_settings.get_value('REPONAME').strip()
|
||
token = app_settings.get_value('GHBEARER').strip()
|
||
if not user or not token:
|
||
return {'ok': False, 'message': 'GitHub 用户名或访问令牌为空,请先保存配置。'}
|
||
try:
|
||
resp = ghnet.get(f'{API}/repos/{user}/{repo}', token=token, timeout=10)
|
||
if resp.status_code == 401:
|
||
return {'ok': False, 'message': '令牌无效或已过期(GitHub 返回 401)。'}
|
||
if resp.status_code == 404:
|
||
return {'ok': False,
|
||
'message': f'找不到仓库 {user}/{repo},或令牌无权访问该仓库(请确认仓库名与令牌的 Repository access)。'}
|
||
if resp.status_code != 200:
|
||
return {'ok': False, 'message': f'GitHub 返回异常状态码:{resp.status_code}。'}
|
||
act = ghnet.get(f'{API}/repos/{user}/{repo}/actions/workflows', token=token, timeout=10)
|
||
if act.status_code == 200:
|
||
return {'ok': True, 'message': f'连接正常:令牌可访问仓库 {user}/{repo},且具备 Actions 权限。'}
|
||
return {'ok': False,
|
||
'message': '可以访问仓库,但令牌缺少 Actions 权限;请编辑令牌,授予 Actions 与 Workflows 的读写权限。'}
|
||
except Exception as e:
|
||
return {'ok': False,
|
||
'message': f'无法连接 GitHub API:{e}(如服务器在国内网络,请在下方配置代理后重试)'}
|