Files
rdgen/rdgenerator/build_backends/gitea.py
T
naeeo d78c071818 构建状态增加平台终态只读核对,修复回调丢失后任务长期卡在构建中
工作流在脚本收尾前失败(拉取/编译/runner 异常)或 GENURL 不可达时,
结束回调会永久丢失,而等待页 30 秒轮询与列表页此前只读本地库,
任务只能等 6 小时本地超时,平台侧已失败也无法及时反映。

- GitHub/Gitea 后端新增只读 get_run(),仅在平台报告终态时返回结论,
  在途、非 200、网络异常、坏 JSON 一律返回 None;不取消、不写平台
- 等待页 30 秒轮询同步核对一次真实状态:20 秒节流、6 秒超时、异常静默
- 我的构建/后台仪表盘改为后台守护线程核对,页面渲染零等待
- 仅做非终态到终态的条件更新,回调抢先到达时不会被覆盖
- 停止/删除纯本地语义、6 小时本地超时兜底等现有逻辑保持不变
2026-09-30 12:46:05 +08:00

245 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Gitea Actions 构建后端(自建 Gitea 1.23+,建议 1.27+)。
API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实):
- 触发:POST /api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
?return_run_details=true,body {ref, inputs},200 返回
{workflow_run_id, html_url, run_url},不传该参数为 204;
- 鉴权:Authorization: token <PAT>。
注意:本平台不主动取消 Gitea 上的运行,停止/删除只作用于本平台本地记录。
构建状态以工作流结束时的回调(update_github_run)为第一来源;get_run 仅做
只读终态核对,兜底回调丢失(脚本未跑到收尾或 GENURL 不可达)的情况。
"""
from .. import app_settings, ghnet
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):
name = 'gitea'
label = 'Gitea'
# ---- 取值辅助 ----
def _server(self):
return app_settings.get_value('GITEA_SERVER_URL').strip().rstrip('/')
def _owner(self):
return app_settings.get_value('GITEA_OWNER').strip()
def _repo(self):
return app_settings.get_value('GITEA_REPO').strip() or 'rdgen'
def _branch(self):
return app_settings.get_value('GITEA_BRANCH').strip() or 'master'
def _ref(self):
branch = self._branch()
return branch if branch.startswith('refs/') else f'refs/heads/{branch}'
def _proxy(self):
return app_settings.get_value('GITEA_PROXY').strip()
def _api(self, path):
return f'{self._server()}/api/v1{path}'
# ---- 配置 ----
def configured(self):
return bool(self._server() and self._owner() and app_settings.get_value('GITEA_TOKEN').strip())
def warnings(self):
warnings = []
if not self._server():
warnings.append('Gitea 服务器地址未配置,无法触发构建。')
if not self._owner():
warnings.append('Gitea 仓库属主未配置,无法触发构建。')
if not app_settings.get_value('GITEA_TOKEN').strip():
warnings.append('Gitea 访问令牌未配置,无法触发构建。')
warnings.extend(self._shared_warnings())
return warnings
# ---- 工作流选择 ----
def workflow_file_for(self, platform, selfhosted=False):
# Gitea 的 runner 全部为自建注册,无需 sh- 变体,统一用标准工作流
return WORKFLOW_FILES.get(platform, 'generator-windows.yml')
# ---- 触发 ----
def dispatch(self, workflow_file, inputs, *, timeout=20):
if not self.configured():
raise BackendError('Gitea 构建服务未完成配置(服务器地址 / 属主 / 令牌缺失)。', kind='config')
url = self._api(
f'/repos/{self._owner()}/{self._repo()}'
f'/actions/workflows/{workflow_file}/dispatches?return_run_details=true'
)
payload = {'ref': self._ref(), 'inputs': inputs}
try:
resp = ghnet.post(
url, json=payload,
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 resp.status_code in (200, 204):
data = {}
try:
data = resp.json()
except ValueError:
pass
run_id = data.get('workflow_run_id')
html_url = data.get('html_url') or ''
# 旧版 Gitea 或未回传详情时,按工作流最新一条运行兜底取 run id
if not run_id and resp.status_code == 204:
fallback = self._latest_run_id(workflow_file)
if fallback:
run_id, html_url = fallback
return DispatchResult(run_id=run_id, html_url=html_url or self.log_url(run_id))
detail = ''
try:
detail = (resp.text or '')[:200]
except Exception:
pass
if resp.status_code == 404:
msg = f'Gitea 仓库或工作流 {workflow_file} 不存在(请确认仓库已启用 Repository Actions)。'
elif resp.status_code in (401, 403):
msg = 'Gitea 拒绝访问:请检查令牌是否有效且具备该仓库 Actions 读写权限。'
elif resp.status_code == 422:
msg = f'Gitea 校验失败(分支或工作流参数有误):{detail}'
else:
msg = f'Gitea 构建服务拒绝了启动请求(HTTP {resp.status_code}):{detail}'
raise BackendError(msg, kind='rejected')
def _latest_run_id(self, workflow_file, *, timeout=10):
"""204 兜底:查该工作流最新一条运行(仅在刚触发后调用)。"""
url = self._api(
f'/repos/{self._owner()}/{self._repo()}'
f'/actions/workflows/{workflow_file}/runs?limit=1'
)
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()
runs = data.get('workflow_runs') if isinstance(data, dict) else data
if runs:
run = runs[0]
return run.get('id'), run.get('html_url') or ''
except Exception as e:
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:
return ''
return f'{self._server()}/{self._owner()}/{self._repo()}/actions/runs/{run_id}'
# ---- 连接测试 ----
def test_connection(self):
server = self._server()
owner = self._owner()
token = app_settings.get_value('GITEA_TOKEN').strip()
repo = self._repo()
if not server or not owner or not token:
return {'ok': False, 'message': 'Gitea 服务器地址、仓库属主或访问令牌为空,请先保存配置。'}
if not (server.startswith('http://') or server.startswith('https://')):
return {'ok': False, 'message': 'Gitea 服务器地址需以 http:// 或 https:// 开头。'}
try:
resp = ghnet.get(
self._api(f'/repos/{owner}/{repo}'),
token=token, timeout=10, flavor='gitea', proxy=self._proxy(),
)
if resp.status_code == 401:
return {'ok': False, 'message': '令牌无效或已过期(Gitea 返回 401)。'}
if resp.status_code == 404:
return {'ok': False,
'message': f'找不到 Gitea 仓库 {owner}/{repo},或令牌无权访问该仓库。'}
if resp.status_code != 200:
return {'ok': False, 'message': f'Gitea 返回异常状态码:{resp.status_code}。'}
act = ghnet.get(
self._api(f'/repos/{owner}/{repo}/actions/workflows'),
token=token, timeout=10, flavor='gitea', proxy=self._proxy(),
)
if act.status_code != 200:
return {'ok': False,
'message': '可以访问仓库,但无法读取 Actions 工作流;请在仓库设置中启用 Repository Actions。'}
# 版本检查:1.23 起支持 workflow_dispatch;建议 1.27+(return_run_details 与缓存兼容性更好)
version_msg = ''
ver = ghnet.get(
self._api('/version'),
token=token, timeout=10, flavor='gitea', proxy=self._proxy(),
)
if ver.status_code == 200:
raw = (ver.json().get('version') or '').strip()
parts = raw.replace('+dev', '').split('.')
try:
major, minor = int(parts[0]), int(parts[1])
if (major, minor) < (1, 23):
return {'ok': False,
'message': f'Gitea 版本为 {raw},低于 1.23,不支持 workflow_dispatch 远程触发,请先升级。'}
if (major, minor) < (1, 27):
version_msg = f'当前 Gitea 版本为 {raw},可触发构建,但建议升级到 1.27+(运行详情回传与缓存兼容性更好)。'
except (ValueError, IndexError):
pass
msg = f'连接正常:令牌可访问仓库 {owner}/{repo},Actions 已启用。'
if version_msg:
msg += version_msg
return {'ok': True, 'message': msg}
except Exception as e:
return {'ok': False, 'message': f'无法连接 Gitea API:{e}(请检查服务器地址与网络/代理配置)'}