From c8f41a34c0379b76963f37c590e806665402d84a Mon Sep 17 00:00:00 2001 From: naeeo Date: Wed, 30 Sep 2026 01:04:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=97=E8=A1=A8=E9=A1=B5?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=85=A2=E5=B9=B6=E6=94=AF=E6=8C=81=20Gitea?= =?UTF-8?q?=201.27=20=E4=B8=80=E9=94=AE=E5=81=9C=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 状态同步加 20 秒节流、超时缩短到 8 秒;Gitea 改用 runs 列表接口一次批量取回全部任务状态,在途任务并发补查作业,消除逐任务串行 API 请求导致的后台/我的构建/详情页慢。 2. 停止时先区分 404:运行已删除则本地标记取消;Gitea 1.27 无取消 API 时,若后台配置了 Gitea 网页账号密码则模拟网页登录取消,否则回退手动链接提示。 3. 后台 Gitea 连接设置新增网页登录账号/密码两个可选项。 --- rdgenerator/app_settings.py | 18 +++ rdgenerator/build_backends/base.py | 8 + rdgenerator/build_backends/gitea.py | 237 +++++++++++++++++++++++++--- rdgenerator/forms.py | 12 ++ rdgenerator/views.py | 84 +++++++++- rdgenerator/web_views.py | 25 +-- 6 files changed, 349 insertions(+), 35 deletions(-) diff --git a/rdgenerator/app_settings.py b/rdgenerator/app_settings.py index e832d9a..efbad24 100644 --- a/rdgenerator/app_settings.py +++ b/rdgenerator/app_settings.py @@ -169,6 +169,24 @@ SETTING_DEFS = [ 'sensitive': True, 'help': '在 Gitea 个人设置中生成的 API 令牌,需对构建仓库授予 Actions 读写权限。页面仅显示保存状态,不会回显明文。', }, + { + 'key': 'GITEA_WEB_USERNAME', + 'env': 'GITEA_WEB_USERNAME', + 'default': '', + 'label': 'Gitea 网页登录账号(可选)', + 'group': 'gitea', + 'sensitive': False, + 'help': '仅 Gitea 1.27 及更早版本需要:这些版本没有取消运行的 API,填账号后本平台可模拟网页登录一键停止构建。留空则停止时给出运行页链接手动 Cancel;升级到 Gitea 1.28+ 后无需填写。建议使用专用账号并授予构建仓库写权限,该账号不能启用两步验证。', + }, + { + 'key': 'GITEA_WEB_PASSWORD', + 'env': 'GITEA_WEB_PASSWORD', + 'default': '', + 'label': 'Gitea 网页登录密码(可选)', + 'group': 'gitea', + 'sensitive': True, + 'help': '与上面的网页登录账号配套,仅用于 Gitea 1.27 及更早版本的一键停止。页面仅显示保存状态,不会回显明文。', + }, { 'key': 'GITEA_PROXY', 'env': 'GITEA_PROXY', diff --git a/rdgenerator/build_backends/base.py b/rdgenerator/build_backends/base.py index fe4fea2..48ca2c7 100644 --- a/rdgenerator/build_backends/base.py +++ b/rdgenerator/build_backends/base.py @@ -61,6 +61,14 @@ class BuildBackend: """查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。""" raise NotImplementedError + def get_runs_batch(self, run_ids, *, timeout=12): + """批量查询多个运行,返回 {str(run_id): RunInfo}。 + + 列表页一次刷新多个在途任务时使用;平台不支持或查询失败返回 None, + 调用方退化为逐个 get_run。返回字典中缺失的 id 由调用方逐个兜底。 + """ + return None + def cancel_run(self, run_id, *, timeout=15): """请求平台取消一次运行;成功无返回,已结束时抛 BackendError(kind='finished')。""" raise NotImplementedError diff --git a/rdgenerator/build_backends/gitea.py b/rdgenerator/build_backends/gitea.py index e2cd815..ae56ae6 100644 --- a/rdgenerator/build_backends/gitea.py +++ b/rdgenerator/build_backends/gitea.py @@ -8,6 +8,8 @@ API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实) status/conclusion 字段仿 GitHub; - 鉴权:Authorization: token 。 """ +import re + from .. import app_settings, ghnet from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend @@ -149,11 +151,10 @@ class GiteaBackend(BuildBackend): # ---- 状态查询 ---- def get_run(self, run_id, *, timeout=12): url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}') + token = app_settings.get_value('GITEA_TOKEN') + proxy = self._proxy() try: - resp = ghnet.get( - url, token=app_settings.get_value('GITEA_TOKEN'), - timeout=timeout, flavor='gitea', proxy=self._proxy(), - ) + resp = ghnet.get(url, token=token, timeout=timeout, flavor='gitea', proxy=proxy) except Exception as e: print(f'查询 Gitea 状态出错:{e}') return None @@ -176,23 +177,27 @@ class GiteaBackend(BuildBackend): 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) + # 仅在途运行才需要 jobs:识别「前置作业失败但父运行长期挂起」的 Gitea 已知行为 + jobs_url = self._api( + f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}/jobs' + ) + try: + jobs_resp = ghnet.get( + jobs_url, token=token, timeout=timeout, flavor='gitea', proxy=proxy) + except Exception as e: + print(f'查询 Gitea 作业状态出错:{e}') + jobs_resp = None + if jobs_resp is not None: + bad = self._bad_job_conclusion(jobs_resp) + 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') + @staticmethod + def _bad_job_conclusion(resp): + """从 jobs 响应中判定终态:有非 cleanup 作业失败/取消/超时时返回对应状态,否则 None。""" 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() @@ -205,12 +210,98 @@ class GiteaBackend(BuildBackend): if conclusion in _JOB_BAD_CONCLUSIONS: return _CONCLUSION_MAP.get(conclusion, 'failure') except Exception as e: - print(f'查询 Gitea 作业状态出错:{e}') + print(f'解析 Gitea 作业状态出错:{e}') return None + # ---- 状态批量查询(列表页用,一次请求取回全部运行) ---- + def get_runs_batch(self, run_ids, *, timeout=12): + """一次 runs 列表请求批量同步状态,避免每个在途任务各发两个请求。 + + 返回 {str(run_id): RunInfo};列表拿不到时返回 None 让调用方退化逐个查询。 + 列表中仍在途的运行,再查 jobs 以识别「前置作业失败但父运行挂起」的僵死; + 列表中不存在的 id(已删除或超出最近窗口)不放进返回字典。 + """ + want = {str(i) for i in run_ids if i} + if not want: + return {} + url = self._api( + f'/repos/{self._owner()}/{self._repo()}/actions/runs?limit=50' + ) + 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() + items = data.get('workflow_runs') if isinstance(data, dict) else data + except ValueError: + return None + if not items: + return {} + + result = {} + active = [] # (id, html_url) + for item in items: + rid = item.get('id') + if rid is None or str(rid) not in want: + continue + html_url = item.get('html_url') or self.log_url(rid) + status = (item.get('status') or '').lower() + conclusion = (item.get('conclusion') or '').lower() + if status == 'completed' or status in _GITEA_TERMINAL: + result[str(rid)] = RunInfo( + finished=True, + status=_CONCLUSION_MAP.get(conclusion or status, 'failure'), + html_url=html_url, + ) + else: + active.append((rid, html_url)) + + # 在途运行查 jobs 识别 Gitea 的作业级失败挂起(通常在途任务很少) + if active: + token = app_settings.get_value('GITEA_TOKEN') + proxy = self._proxy() + owner_repo = f'{self._owner()}/{self._repo()}' + + def _probe(rid_html): + rid, html_url = rid_html + jobs_url = self._api( + f'/repos/{owner_repo}/actions/runs/{rid}/jobs' + ) + try: + jobs_resp = ghnet.get( + jobs_url, token=token, timeout=timeout, + flavor='gitea', proxy=proxy, + ) + except Exception: + return str(rid), RunInfo(False, 'in_progress', html_url) + bad = self._bad_job_conclusion(jobs_resp) + if bad: + return str(rid), RunInfo(True, bad, html_url) + return str(rid), RunInfo(False, 'in_progress', html_url) + + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=min(8, len(active))) as pool: + for key, info in pool.map(_probe, active): + result[key] = info + return result + # ---- 取消运行 ---- def cancel_run(self, run_id, *, timeout=15): - """请求平台取消运行。成功无返回;409 表示运行已结束(调用方应改为拉取真实状态)。""" + """请求平台取消运行。 + + - 2xx:已接受取消; + - 409:运行已结束(调用方应改为拉取真实状态); + - 运行在平台上已不存在(如已被手工删除):kind='missing'; + - 旧版 Gitea(≤1.27)无取消 API:配置了 Web 账密则模拟网页登录取消, + 否则 kind='unsupported',引导用户去运行页手动 Cancel。 + """ url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}/cancel') try: resp = ghnet.post( @@ -224,9 +315,17 @@ class GiteaBackend(BuildBackend): if resp.status_code == 409: raise BackendError('该构建运行已经结束,无法取消。', kind='finished') if resp.status_code == 404: + exists = self._run_exists(run_id, timeout=timeout) + if exists is False: + raise BackendError( + '该运行在 Gitea 上已不存在(可能已被删除)。', kind='missing') + # 运行存在但 cancel 端点 404:Gitea ≤ 1.27 没有取消 API + if self._cancel_via_web(run_id, timeout=timeout): + return raise BackendError( - '当前 Gitea 版本(1.27 及更早)未开放取消运行的 API,' - '请在 Gitea 的运行页面手动点击 Cancel 停止;升级到 Gitea 1.28+ 后可在此一键停止。', + '当前 Gitea 版本(1.27 及更早)未开放取消运行的 API。' + '可在后台「Gitea 连接」填写 Gitea 网页登录账号以启用一键停止,' + '或在 Gitea 的运行页面手动点击 Cancel 停止;升级到 Gitea 1.28+ 后可直接一键停止。', kind='unsupported', ) detail = '' @@ -236,6 +335,102 @@ class GiteaBackend(BuildBackend): pass raise BackendError(f'Gitea 拒绝了取消请求(HTTP {resp.status_code}):{detail}') + def _run_exists(self, run_id, *, timeout=10): + """GET run:存在 True / 404 False / 无法确定 None。""" + 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: + return None + if resp.status_code == 200: + return True + if resp.status_code == 404: + return False + return None + + def _cancel_via_web(self, run_id, *, timeout=20): + """Gitea ≤1.27 无取消 API 时,用网页会话方式取消(需后台配置 Web 账密)。 + + 模拟浏览器:登录拿会话 → 打开运行页拿 CSRF → POST 网页取消路由。 + 注意:部分 1.27 实例的登录页本身不输出 CSRF(登录 POST 豁免), + 但取消操作需要登录后页面里的令牌;未配置账密、登录失败 + (含开启两步验证)时返回 False。 + """ + import requests + + username = app_settings.get_value('GITEA_WEB_USERNAME').strip() + password = app_settings.get_value('GITEA_WEB_PASSWORD') + if not username or not password: + return False + server = self._server() + owner, repo = self._owner(), self._repo() + proxies = ghnet.proxies(self._proxy()) + sess = requests.Session() + sess.proxies.update(proxies) + ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 rdgen' + try: + # 1. 登录页(建立会话;CSRF 若页面有则带上,没有也无妨——该实例登录豁免) + r = sess.get(f'{server}/user/login', timeout=timeout, + headers={'User-Agent': ua}, allow_redirects=True) + csrf = self._extract_csrf(r.text) + login_data = {'user_name': username, 'password': password} + login_headers = {'User-Agent': ua, 'Referer': f'{server}/user/login'} + if csrf: + login_data['_csrf'] = csrf + login_headers['X-Csrf-Token'] = csrf + # 2. 提交登录(Gitea 用 user_name 字段;成功后 302 离开 /user/login) + r = sess.post( + f'{server}/user/login', data=login_data, + headers=login_headers, timeout=timeout, allow_redirects=True, + ) + if '/user/login' in r.url or 'name="password"' in r.text: + print('Gitea 网页取消:登录失败(账号密码错误或启用了两步验证)') + return False + # 3. 打开运行页取 CSRF(登录后页面才输出令牌) + run_page = f'{server}/{owner}/{repo}/actions/runs/{run_id}' + r = sess.get(run_page, timeout=timeout, headers={'User-Agent': ua}) + if r.status_code != 200: + print(f'Gitea 网页取消:运行页 HTTP {r.status_code}(账号可能无该仓库访问权限)') + return False + csrf = self._extract_csrf(r.text) + if not csrf: + print('Gitea 网页取消:登录后运行页仍未找到 CSRF 令牌(账号可能无该仓库写权限)') + return False + # 4. POST 网页取消路由(302/303 回运行页即成功) + r = sess.post( + f'{run_page}/cancel', + data={'_csrf': csrf}, + headers={'User-Agent': ua, 'Referer': run_page, + 'X-Csrf-Token': csrf}, + timeout=timeout, allow_redirects=False, + ) + if r.status_code in (200, 302, 303): + return True + print(f'Gitea 网页取消:取消请求被拒绝 HTTP {r.status_code}') + return False + except Exception as e: + print(f'Gitea 网页取消出错:{e}') + return False + + @staticmethod + def _extract_csrf(html): + """从 Gitea 页面提取 CSRF:优先 meta,其次隐藏域,最后前端 JSON 配置。""" + if not html: + return None + for pattern in ( + r'name="_csrf"\s+content="([^"]+)"', + r'content="([^"]+)"\s+name="_csrf"', + r'name="_csrf"\s+value="([^"]+)"', + r'"(?:csrfToken|_csrf)"\s*:\s*"([^"]+)"', + ): + m = re.search(pattern, html) + if m: + return m.group(1) + return None + # ---- 日志链接 ---- def log_url(self, run_id): if not run_id: diff --git a/rdgenerator/forms.py b/rdgenerator/forms.py index b7ed4d6..70c413f 100644 --- a/rdgenerator/forms.py +++ b/rdgenerator/forms.py @@ -307,6 +307,18 @@ class GitHubSettingsForm(forms.Form): required=False, ) clear_gitea_token = forms.BooleanField(label="清除后台保存的令牌", required=False) + gitea_web_username = forms.CharField( + label="Gitea 网页登录账号(可选)", + max_length=100, + required=False, + widget=forms.TextInput(attrs={'placeholder': '仅 Gitea 1.27 及更早版本一键停止时需要'}), + ) + gitea_web_password = forms.CharField( + label="Gitea 网页登录密码(可选)", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_gitea_web_password = forms.BooleanField(label="清除后台保存的网页登录密码", required=False) gitea_proxy = forms.CharField( label="Gitea 访问代理", widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '内网直连通常留空,如 http://host:port'}), diff --git a/rdgenerator/views.py b/rdgenerator/views.py index e6a125e..97718f0 100644 --- a/rdgenerator/views.py +++ b/rdgenerator/views.py @@ -2,6 +2,7 @@ import io import json import os import re +import time import uuid from datetime import timedelta from pathlib import Path @@ -34,25 +35,52 @@ from .models import GithubRun, STATUS_LABELS, STATUS_BADGE TERMINAL_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped') # 正常构建约 30~45 分钟;超过该时长平台仍显示进行中的,视为 runner 离线/作业僵死 STALE_BUILD_HOURS = 6 +# 列表页状态同步节流:同一在途任务两次主动查询的最小间隔,避免每次刷页面都串行请求平台 +REFRESH_THROTTLE_SECONDS = 20 +# 列表/仪表盘同步用短超时,平台抖动时不能拖垮页面 +REFRESH_TIMEOUT = 8 +_refresh_stamps = {} -def refresh_active_run(gh_run): +def _refresh_due(run_pk): + now = time.monotonic() + ts = _refresh_stamps.get(run_pk, 0) + if now - ts >= REFRESH_THROTTLE_SECONDS: + # 发起前先占位,避免并发请求堆积 + _refresh_stamps[run_pk] = now + return True + return False + + +def refresh_active_run(gh_run, *, force=False, timeout=REFRESH_TIMEOUT): """向构建平台同步一次任务状态,状态落库后返回 True;网络异常或仍在进行则 False。 同时兜底两种平台侧的僵死: - Gitea 前置作业失败后可复用工作流父作业/无匹配标签的作业长期挂起; - 自建 runner 离线导致运行一直 in_progress(超过 STALE_BUILD_HOURS 判超时)。 + + force=False 时按 REFRESH_THROTTLE_SECONDS 节流(列表/仪表盘用); + force=True 跳过节流立即查询(详情页、停止操作后用),但同样刷新节流戳, + 使紧接着的列表打开不再重复请求。 """ if gh_run.is_finished() or not gh_run.github_run_id: return False + if not force and not _refresh_due(gh_run.pk): + return False + _refresh_stamps[gh_run.pk] = time.monotonic() backend = build_backends.get_backend(gh_run.backend or 'github') try: - info = backend.get_run(gh_run.github_run_id) + info = backend.get_run(gh_run.github_run_id, timeout=timeout) except Exception as e: print(f"查询构建状态出错:{e}") return False if info is None: return False + return _apply_run_info(gh_run, info) + + +def _apply_run_info(gh_run, info): + """把平台返回的状态落库;终态与 6 小时僵死兜底均在此。返回是否有变化。""" 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' @@ -63,6 +91,56 @@ def refresh_active_run(gh_run): return False +def refresh_active_runs(gh_runs, *, force=False): + """并发同步一批在途任务(列表页/仪表盘),整页只承受一次平台往返的耗时。 + + Gitea 优先用 runs 列表接口一次取回全部状态(在途任务再补 jobs 探测); + 其他后端或批量接口不可用时退化为并发逐个查询。 + """ + due = [] + for run in gh_runs: + if run.is_finished() or not run.github_run_id: + continue + if _refresh_due(run.pk) or force: + _refresh_stamps[run.pk] = time.monotonic() + due.append(run) + if not due: + return + + # 按后端分组批量 + groups = {} + for run in due: + groups.setdefault(run.backend or 'github', []).append(run) + for backend_name, runs in groups.items(): + backend = build_backends.get_backend(backend_name) + get_runs_batch = getattr(backend, 'get_runs_batch', None) + infos = None + if get_runs_batch is not None: + try: + infos = get_runs_batch( + [r.github_run_id for r in runs], timeout=REFRESH_TIMEOUT) + except Exception as e: + print(f"批量查询构建状态出错:{e}") + infos = None + if infos is None: + from concurrent.futures import ThreadPoolExecutor + workers = min(8, len(runs)) + with ThreadPoolExecutor(max_workers=workers) as pool: + list(pool.map(lambda r: refresh_active_run(r, force=force), runs)) + continue + missing = [run for run in runs if infos.get(str(run.github_run_id)) is None] + if missing: + # 列表窗口里没有的运行(已删除或超出最近 50 条):并发逐个查询兜底 + from concurrent.futures import ThreadPoolExecutor + workers = min(8, len(missing)) + with ThreadPoolExecutor(max_workers=workers) as pool: + list(pool.map(lambda r: refresh_active_run(r, force=True), missing)) + for run in runs: + info = infos.get(str(run.github_run_id)) + if info is not None: + _apply_run_info(run, info) + + def _webhook_denied(request): """配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。""" secret = app_settings.get_value('WEBHOOK_SECRET') @@ -424,7 +502,7 @@ def _get_run_status(uuid_val): 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 '' - refresh_active_run(gh_run) + refresh_active_run(gh_run, force=True, timeout=12) return { "found": True, diff --git a/rdgenerator/web_views.py b/rdgenerator/web_views.py index c2aae03..fa32dba 100644 --- a/rdgenerator/web_views.py +++ b/rdgenerator/web_views.py @@ -32,11 +32,9 @@ def staff_required(view_func): @login_required def my_builds(request): 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) + # 列表打开时并发同步在途任务的真实状态(带短节流,避免频繁刷页面拖慢) + from .views import refresh_active_runs + refresh_active_runs(builds) return render(request, 'my_builds.html', {'builds': builds}) @@ -82,11 +80,16 @@ def stop_build(request, run_id): if e.kind == 'finished': # 平台侧已结束:立即拉一次真实状态,避免本地仍显示构建中 from .views import refresh_active_run - refresh_active_run(run) + refresh_active_run(run, force=True) if run.is_finished(): messages.info(request, f"构建平台上该任务实际已结束({run.status_label()}),列表状态已同步。") else: messages.error(request, str(e)) + elif e.kind == 'missing': + # 平台上运行已被删除:本地直接标记取消,避免永远卡在构建中 + run.status = 'cancelled' + run.save(update_fields=['status', 'updated_at']) + messages.info(request, "Gitea 上该运行已不存在(可能已被删除),已在本平台标记为已取消。") elif e.kind == 'unsupported': # 平台版本不支持远程取消(如 Gitea ≤ 1.27):给出运行页,本地状态保持构建中 log_link = backend.log_url(run.github_run_id) @@ -167,11 +170,9 @@ def delete_token(request, key): @staff_required def dashboard(request): 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) + # 打开仪表盘时并发同步在途任务的真实状态(带短节流) + from .views import refresh_active_runs + refresh_active_runs(builds) stats = { 'total': GithubRun.objects.count(), 'in_progress': GithubRun.objects.exclude( @@ -317,6 +318,7 @@ _PLAIN_FIELDS = [ ('GITEA_OWNER', 'gitea_owner'), ('GITEA_REPO', 'gitea_repo'), ('GITEA_BRANCH', 'gitea_branch'), + ('GITEA_WEB_USERNAME', 'gitea_web_username'), ('GHUSER', 'ghuser'), ('REPONAME', 'reponame'), ('GHBRANCH', 'ghbranch'), @@ -326,6 +328,7 @@ _PLAIN_FIELDS = [ # 密钥配置项:设置键 -> (输入字段名, 清除复选框字段名) _SECRET_FIELDS = [ ('GITEA_TOKEN', ('gitea_token', 'clear_gitea_token')), + ('GITEA_WEB_PASSWORD', ('gitea_web_password', 'clear_gitea_web_password')), ('GITEA_PROXY', ('gitea_proxy', 'clear_gitea_proxy')), ('GHBEARER', ('ghbearer', 'clear_ghbearer')), ('ZIP_PASSWORD', ('zip_password', 'clear_zip_password')),