feat: 同步构建真实状态并支持在列表停止在途任务
- Gitea 运行未结束时探测作业级结论,修复前置作业失败后可复用工作流父作业/等待标签作业长期挂起导致状态永远构建中 - 打开我的构建/管理仪表盘时主动向构建平台同步在途任务状态;创建超 6 小时仍在途的任务判超时 - 构建中任务列表增加停止按钮(Gitea 需 1.28+ 取消 API,旧版本提示去运行页手动 Cancel),终态任务保留删除 - GitHub/Gitea 后端统一 cancel_run 接口
This commit is contained in:
@@ -20,6 +20,7 @@ urlpatterns = [
|
|||||||
path('', views.generator_view, name='generator'),
|
path('', views.generator_view, name='generator'),
|
||||||
path('my-builds/', web_views.my_builds, name='my_builds'),
|
path('my-builds/', web_views.my_builds, name='my_builds'),
|
||||||
path('my-builds/<int:run_id>/retry/', web_views.retry_build, name='retry_build'),
|
path('my-builds/<int:run_id>/retry/', web_views.retry_build, name='retry_build'),
|
||||||
|
path('my-builds/<int:run_id>/stop/', web_views.stop_build, name='stop_build'),
|
||||||
path('my-builds/<int:run_id>/delete/', web_views.delete_my_build, name='delete_my_build'),
|
path('my-builds/<int:run_id>/delete/', web_views.delete_my_build, name='delete_my_build'),
|
||||||
path('configs/', web_views.saved_configs, name='saved_configs'),
|
path('configs/', web_views.saved_configs, name='saved_configs'),
|
||||||
path('configs/save/', web_views.save_config, name='save_config'),
|
path('configs/save/', web_views.save_config, name='save_config'),
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ class BuildBackend:
|
|||||||
"""查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。"""
|
"""查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def cancel_run(self, run_id, *, timeout=15):
|
||||||
|
"""请求平台取消一次运行;成功无返回,已结束时抛 BackendError(kind='finished')。"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def log_url(self, run_id) -> str:
|
def log_url(self, run_id) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ from .. import app_settings, ghnet
|
|||||||
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
|
from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend
|
||||||
|
|
||||||
# Gitea 未结束状态(blocked=等待审批,waiting=等待 runner)
|
# 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 内部状态
|
# Gitea 结论 -> rdgen 内部状态
|
||||||
_CONCLUSION_MAP = {
|
_CONCLUSION_MAP = {
|
||||||
'success': 'success',
|
'success': 'success',
|
||||||
@@ -20,7 +22,11 @@ _CONCLUSION_MAP = {
|
|||||||
'cancelled': 'cancelled',
|
'cancelled': 'cancelled',
|
||||||
'skipped': 'skipped',
|
'skipped': 'skipped',
|
||||||
'timed_out': 'timed_out',
|
'timed_out': 'timed_out',
|
||||||
|
'stopped': 'cancelled',
|
||||||
|
'dead': 'failure',
|
||||||
}
|
}
|
||||||
|
# 作业出现这些结论时,整个运行最终必然失败(cleanup 等收尾作业除外)
|
||||||
|
_JOB_BAD_CONCLUSIONS = {'failure', 'cancelled', 'timed_out', 'dead'}
|
||||||
|
|
||||||
|
|
||||||
class GiteaBackend(BuildBackend):
|
class GiteaBackend(BuildBackend):
|
||||||
@@ -163,9 +169,73 @@ class GiteaBackend(BuildBackend):
|
|||||||
status=_CONCLUSION_MAP.get(conclusion, 'failure'),
|
status=_CONCLUSION_MAP.get(conclusion, 'failure'),
|
||||||
html_url=html_url,
|
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(等待审批)等也算进行中
|
# blocked(等待审批)等也算进行中
|
||||||
return RunInfo(finished=False, status=status or 'in_progress', html_url=html_url)
|
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):
|
def log_url(self, run_id):
|
||||||
if not run_id:
|
if not run_id:
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ class GitHubBackend(BuildBackend):
|
|||||||
return RunInfo(finished=False, status=data.get('status') or 'in_progress',
|
return RunInfo(finished=False, status=data.get('status') or 'in_progress',
|
||||||
html_url=data.get('html_url') or self.log_url(run_id))
|
html_url=data.get('html_url') or self.log_url(run_id))
|
||||||
|
|
||||||
|
# ---- 取消运行 ----
|
||||||
|
def cancel_run(self, run_id, *, timeout=15):
|
||||||
|
user = app_settings.get_value('GHUSER')
|
||||||
|
repo = app_settings.get_value('REPONAME')
|
||||||
|
url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}/cancel'
|
||||||
|
try:
|
||||||
|
resp = ghnet.post(url, token=app_settings.get_value('GHBEARER'), timeout=timeout)
|
||||||
|
except Exception as e:
|
||||||
|
raise BackendError(f'无法连接 GitHub 构建服务:{e}', kind='network')
|
||||||
|
if 200 <= resp.status_code < 300:
|
||||||
|
return
|
||||||
|
if resp.status_code == 409:
|
||||||
|
raise BackendError('该构建运行已经结束,无法取消。', kind='finished')
|
||||||
|
raise BackendError(f'GitHub 拒绝了取消请求(HTTP {resp.status_code})。')
|
||||||
|
|
||||||
# ---- 日志链接 ----
|
# ---- 日志链接 ----
|
||||||
def log_url(self, run_id):
|
def log_url(self, run_id):
|
||||||
if not run_id:
|
if not run_id:
|
||||||
|
|||||||
@@ -367,6 +367,8 @@ input[type="file"].form-control::-webkit-file-upload-button {
|
|||||||
.btn-secondary:hover { border-color: var(--primary-border); color: var(--primary); }
|
.btn-secondary:hover { border-color: var(--primary-border); color: var(--primary); }
|
||||||
.btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; }
|
.btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; }
|
||||||
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
|
.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
|
||||||
|
.btn-warning { background: #fff; color: var(--warning); border-color: #f3d9a6; }
|
||||||
|
.btn-warning:hover { background: var(--warning); color: #fff; border-color: var(--warning); }
|
||||||
.btn-sm { min-height: 30px; padding: 0 12px; font-size: 12.5px; border-radius: 6px; }
|
.btn-sm { min-height: 30px; padding: 0 12px; font-size: 12.5px; border-radius: 6px; }
|
||||||
.btn-sm .icon { font-size: 13px; }
|
.btn-sm .icon { font-size: 13px; }
|
||||||
.btn-block { width: 100%; }
|
.btn-block { width: 100%; }
|
||||||
|
|||||||
@@ -76,6 +76,9 @@
|
|||||||
<symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<symbol id="i-trash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>
|
<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="i-stop" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="6" y="6" width="12" height="12" rx="2"/>
|
||||||
|
</symbol>
|
||||||
<symbol id="i-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
<symbol id="i-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<circle cx="12" cy="12" r="10"/><path d="m8.5 12 2.5 2.5 5-5"/>
|
<circle cx="12" cy="12" r="10"/><path d="m8.5 12 2.5 2.5 5-5"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|||||||
@@ -51,6 +51,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if not run.is_finished %}
|
||||||
|
<form method="post" class="inline-form" action="{% url 'stop_build' run.id %}"
|
||||||
|
onsubmit="return confirm('确定停止构建任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?停止后可在列表中使用「重试」重新发起。');">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-warning btn-sm">
|
||||||
|
<svg class="icon"><use href="#i-stop"/></svg> 停止
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
{% if run.is_finished %}
|
{% if run.is_finished %}
|
||||||
<form method="post" class="inline-form" action="{% url 'delete_my_build' run.id %}"
|
<form method="post" class="inline-form" action="{% url 'delete_my_build' run.id %}"
|
||||||
onsubmit="return confirm('确定删除任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?将同时删除服务器上的安装包文件,且不可恢复。');">
|
onsubmit="return confirm('确定删除任务「{{ run.filename|default:run.short_uuid|escapejs }}」吗?将同时删除服务器上的安装包文件,且不可恢复。');">
|
||||||
|
|||||||
+35
-8
@@ -3,6 +3,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
@@ -22,12 +23,45 @@ from django.http import (
|
|||||||
Http404,
|
Http404,
|
||||||
)
|
)
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.utils import timezone
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
from . import app_settings, build_backends, versions as rdeskVersions
|
from . import app_settings, build_backends, versions as rdeskVersions
|
||||||
from .forms import GenerateForm
|
from .forms import GenerateForm
|
||||||
from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
|
from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
|
||||||
|
|
||||||
|
# 终态状态集合
|
||||||
|
TERMINAL_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped')
|
||||||
|
# 正常构建约 30~45 分钟;超过该时长平台仍显示进行中的,视为 runner 离线/作业僵死
|
||||||
|
STALE_BUILD_HOURS = 6
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_active_run(gh_run):
|
||||||
|
"""向构建平台同步一次任务状态,状态落库后返回 True;网络异常或仍在进行则 False。
|
||||||
|
|
||||||
|
同时兜底两种平台侧的僵死:
|
||||||
|
- Gitea 前置作业失败后可复用工作流父作业/无匹配标签的作业长期挂起;
|
||||||
|
- 自建 runner 离线导致运行一直 in_progress(超过 STALE_BUILD_HOURS 判超时)。
|
||||||
|
"""
|
||||||
|
if gh_run.is_finished() or not gh_run.github_run_id:
|
||||||
|
return False
|
||||||
|
backend = build_backends.get_backend(gh_run.backend or 'github')
|
||||||
|
try:
|
||||||
|
info = backend.get_run(gh_run.github_run_id)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"查询构建状态出错:{e}")
|
||||||
|
return False
|
||||||
|
if info is None:
|
||||||
|
return False
|
||||||
|
new_status = info.status if info.finished else None
|
||||||
|
if new_status is None and timezone.now() - gh_run.created_at > timedelta(hours=STALE_BUILD_HOURS):
|
||||||
|
new_status = 'timed_out'
|
||||||
|
if new_status and new_status != gh_run.status:
|
||||||
|
gh_run.status = new_status
|
||||||
|
gh_run.save(update_fields=['status', 'updated_at'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _webhook_denied(request):
|
def _webhook_denied(request):
|
||||||
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
|
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
|
||||||
@@ -390,14 +424,7 @@ def _get_run_status(uuid_val):
|
|||||||
backend = build_backends.get_backend(gh_run.backend or 'github')
|
backend = build_backends.get_backend(gh_run.backend or 'github')
|
||||||
github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else ''
|
github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else ''
|
||||||
|
|
||||||
if gh_run.status not in ['success', 'failure', 'cancelled', 'timed_out', 'skipped'] and gh_run.github_run_id:
|
refresh_active_run(gh_run)
|
||||||
try:
|
|
||||||
info = backend.get_run(gh_run.github_run_id)
|
|
||||||
if info is not None and info.finished:
|
|
||||||
gh_run.status = info.status
|
|
||||||
gh_run.save()
|
|
||||||
except Exception as e:
|
|
||||||
print(f"查询构建状态出错:{e}")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"found": True,
|
"found": True,
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ def staff_required(view_func):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def my_builds(request):
|
def my_builds(request):
|
||||||
builds = GithubRun.objects.filter(created_by=request.user)
|
builds = list(GithubRun.objects.filter(created_by=request.user))
|
||||||
|
# 列表打开时向构建平台同步在途任务的真实状态(Gitea 可能不主动回调终态)
|
||||||
|
from .views import refresh_active_run
|
||||||
|
for run in builds:
|
||||||
|
if not run.is_finished():
|
||||||
|
refresh_active_run(run)
|
||||||
return render(request, 'my_builds.html', {'builds': builds})
|
return render(request, 'my_builds.html', {'builds': builds})
|
||||||
|
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ def delete_my_build(request, run_id):
|
|||||||
"""用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。"""
|
"""用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。"""
|
||||||
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
|
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
|
||||||
if not run.is_finished():
|
if not run.is_finished():
|
||||||
messages.error(request, "该任务仍在进行中,请等待结束(或先在 GitHub Actions 中取消)后再删除。")
|
messages.error(request, "该任务仍在进行中,请先点「停止」取消构建(或等待结束)后再删除。")
|
||||||
else:
|
else:
|
||||||
_remove_run_artifacts(run)
|
_remove_run_artifacts(run)
|
||||||
run.delete()
|
run.delete()
|
||||||
@@ -56,6 +61,49 @@ def delete_my_build(request, run_id):
|
|||||||
return redirect('my_builds')
|
return redirect('my_builds')
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def stop_build(request, run_id):
|
||||||
|
"""停止自己的在途构建:调用构建平台取消运行,并把任务标记为已取消。"""
|
||||||
|
run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user)
|
||||||
|
if run.is_finished():
|
||||||
|
messages.info(request, "该任务已经结束,无需停止。")
|
||||||
|
return redirect('my_builds')
|
||||||
|
if not run.github_run_id:
|
||||||
|
messages.error(request, "该任务尚未获得构建平台的运行编号,暂时无法停止,请稍后重试。")
|
||||||
|
return redirect('my_builds')
|
||||||
|
|
||||||
|
from . import build_backends
|
||||||
|
from .build_backends.base import BackendError
|
||||||
|
backend = build_backends.get_backend(run.backend or 'github')
|
||||||
|
try:
|
||||||
|
backend.cancel_run(run.github_run_id)
|
||||||
|
except BackendError as e:
|
||||||
|
if e.kind == 'finished':
|
||||||
|
# 平台侧已结束:立即拉一次真实状态,避免本地仍显示构建中
|
||||||
|
from .views import refresh_active_run
|
||||||
|
refresh_active_run(run)
|
||||||
|
if run.is_finished():
|
||||||
|
messages.info(request, f"构建平台上该任务实际已结束({run.status_label()}),列表状态已同步。")
|
||||||
|
else:
|
||||||
|
messages.error(request, str(e))
|
||||||
|
elif e.kind == 'unsupported':
|
||||||
|
# 平台版本不支持远程取消(如 Gitea ≤ 1.27):给出运行页,本地状态保持构建中
|
||||||
|
log_link = backend.log_url(run.github_run_id)
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
f"无法远程停止:{e.message} 运行页面:{log_link}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.error(request, f"停止失败:{e.message}")
|
||||||
|
return redirect('my_builds')
|
||||||
|
|
||||||
|
run.status = 'cancelled'
|
||||||
|
run.save(update_fields=['status', 'updated_at'])
|
||||||
|
messages.success(request, f"已请求{run.backend_label}停止构建任务「{run.filename or run.short_uuid()}」。")
|
||||||
|
return redirect('my_builds')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_POST
|
@require_POST
|
||||||
def retry_build(request, run_id):
|
def retry_build(request, run_id):
|
||||||
@@ -118,7 +166,12 @@ def delete_token(request, key):
|
|||||||
|
|
||||||
@staff_required
|
@staff_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
builds = GithubRun.objects.select_related('created_by').all()[:100]
|
builds = list(GithubRun.objects.select_related('created_by').all()[:100])
|
||||||
|
# 打开仪表盘时同步在途任务的真实状态
|
||||||
|
from .views import refresh_active_run
|
||||||
|
for run in builds:
|
||||||
|
if not run.is_finished():
|
||||||
|
refresh_active_run(run)
|
||||||
stats = {
|
stats = {
|
||||||
'total': GithubRun.objects.count(),
|
'total': GithubRun.objects.count(),
|
||||||
'in_progress': GithubRun.objects.exclude(
|
'in_progress': GithubRun.objects.exclude(
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ action)。
|
|||||||
### 1. Gitea 服务器与仓库
|
### 1. Gitea 服务器与仓库
|
||||||
|
|
||||||
1. 部署 Gitea **1.27 或更高版本**(最低 1.23;1.27 随附 act_runner 2.0,对
|
1. 部署 Gitea **1.27 或更高版本**(最低 1.23;1.27 随附 act_runner 2.0,对
|
||||||
artifact/cache 与第三方 action 的兼容性最好)。
|
artifact/cache 与第三方 action 的兼容性最好)。注意:列表页的一键「停止」依赖
|
||||||
|
Gitea 1.28+ 的取消运行 API;1.27 及更早版本点击停止会给出运行页链接,需到
|
||||||
|
Gitea 界面手动 Cancel。
|
||||||
2. 把本仓库整体推送到 Gitea(保留 `.gitea/` 目录与默认分支名,后台填写的
|
2. 把本仓库整体推送到 Gitea(保留 `.gitea/` 目录与默认分支名,后台填写的
|
||||||
GITEA_BRANCH 必须与实际分支一致)。
|
GITEA_BRANCH 必须与实际分支一致)。
|
||||||
3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。
|
3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。
|
||||||
|
|||||||
Reference in New Issue
Block a user