diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e0704c..cf6603f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,11 +4,25 @@ on: push: tags: - "v*" + workflow_dispatch: + inputs: + release_tag: + description: Existing formal tag to publish, for example v2.1.0 + required: true + default: v2.1.0 + type: string + +concurrency: + group: formal-release-${{ inputs.release_tag || github.ref_name }} + cancel-in-progress: false permissions: contents: write packages: write +env: + RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }} + jobs: test: name: Test Python ${{ matrix.python-version }} @@ -19,12 +33,14 @@ jobs: python-version: ["3.9", "3.11", "3.13"] steps: - uses: actions/checkout@v7 + with: + ref: ${{ env.RELEASE_TAG }} - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - - name: Verify version tag + - name: Verify formal version tag shell: bash - run: test "${GITHUB_REF_NAME}" = "v$(tr -d '\r\n' < VERSION)" + run: test "${RELEASE_TAG}" = "v$(tr -d '\r\n' < VERSION)" - name: Compile Python sources run: python -m py_compile vpngate_manager.py vpn_utils.py proxy_server.py snapshot_utils.py - name: Validate installation script @@ -34,36 +50,53 @@ jobs: - name: Run unit tests run: python -m unittest discover -s tests -v - release: - name: Build Linux release archives + docker-smoke: + name: Smoke test Docker ${{ matrix.slug }} needs: test runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + slug: amd64 + - platform: linux/386 + slug: 386 + - platform: linux/arm64 + slug: arm64 + - platform: linux/arm/v7 + slug: armv7 steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 with: - python-version: "3.11" - - name: Build architecture-labelled archives - run: python scripts/build_release_archives.py --output-dir dist - - name: Create GitHub formal release - env: - GH_TOKEN: ${{ github.token }} + ref: ${{ env.RELEASE_TAG }} + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - name: Build architecture image for smoke test + uses: docker/build-push-action@v7 + with: + context: . + load: true + platforms: ${{ matrix.platform }} + build-args: BUILD_VERSION=${{ env.RELEASE_TAG }} + tags: aimilivpn-smoke:${{ matrix.slug }} + cache-from: type=gha,scope=smoke-${{ matrix.slug }} + cache-to: type=gha,mode=max,scope=smoke-${{ matrix.slug }} + - name: Verify application imports in image shell: bash run: | - title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版" - if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then - gh release upload "${GITHUB_REF_NAME}" dist/* --clobber --repo "${GITHUB_REPOSITORY}" - gh release edit "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md - else - gh release create "${GITHUB_REF_NAME}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag - fi + docker run --rm --platform "${{ matrix.platform }}" \ + "aimilivpn-smoke:${{ matrix.slug }}" \ + python3 -c 'import vpngate_manager as app; print(app.APP_VERSION)' docker: - name: Build multi-architecture Docker image - needs: test + name: Publish multi-architecture Docker image + needs: docker-smoke runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + with: + ref: ${{ env.RELEASE_TAG }} - uses: docker/setup-qemu-action@v4 - uses: docker/setup-buildx-action@v4 - uses: docker/login-action@v4 @@ -75,11 +108,12 @@ jobs: id: version shell: bash run: | - version="${GITHUB_REF_NAME#v}" + version="${RELEASE_TAG#v}" minor="$(printf '%s' "${version}" | cut -d. -f1,2)" echo "version=${version}" >> "${GITHUB_OUTPUT}" echo "minor=${minor}" >> "${GITHUB_OUTPUT}" - - uses: docker/build-push-action@v7 + - name: Build and publish Docker manifest + uses: docker/build-push-action@v7 with: context: . push: true @@ -93,3 +127,28 @@ jobs: cache-to: type=gha,mode=max provenance: mode=max sbom: true + + release: + name: Publish universal Python source release + needs: docker + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ env.RELEASE_TAG }} + - uses: actions/setup-python@v7 + with: + python-version: "3.11" + - name: Build universal Linux source archive + run: python scripts/build_release_archives.py --output-dir dist + - name: Create GitHub formal release + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版" + if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then + gh release upload "${RELEASE_TAG}" dist/* --clobber --repo "${GITHUB_REPOSITORY}" + gh release edit "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md + else + gh release create "${RELEASE_TAG}" dist/* --repo "${GITHUB_REPOSITORY}" --title "${title}" --notes-file RELEASE_NOTES.md --verify-tag diff --git a/Dockerfile b/Dockerfile index c07dbba..17d2d9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,7 @@ COPY mirror ./mirror ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ + DEPLOYMENT_MODE=docker \ VPNGATE_DATA_DIR=/data \ UI_HOST=0.0.0.0 \ UI_PORT=8787 \ diff --git a/README.md b/README.md index 258b020..9f2ad61 100644 --- a/README.md +++ b/README.md @@ -27,23 +27,23 @@ V2.1 是项目启用正式版本标志后的首个稳定版本。仓库、安装 - **国家筛选**:支持带国旗和节点数量的实时多选筛选,选择范围保存到本机,并作用于手动更新和后台周期同步。 - **节点操作**:恢复单节点“检测”按钮,补齐收藏、检测、连接和断开状态逻辑。 - **镜像同步**:GitHub Pages 每 15 分钟同步并校验官方节点快照,官方 API 被屏蔽时自动回退。 -- **Web 更新检测**:页面顶部显示 `V2.1 正式版`,可直接检查 GitHub 最新稳定 Release;只展示 `main` 和正式版下载入口。 -- **正式发布链路**:GitHub 标签自动运行 Python 兼容测试、构建四类 Linux 发行包、生成 SHA-256 校验文件并发布多架构 Docker 镜像。 +- **Web 更新检测**:页面顶部显示 `V2.1 正式版`,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示正确更新命令。 +- **正式发布链路**:GitHub 标签或手动重跑会依次执行 Python 兼容测试、四架构 Docker 冒烟测试、GHCR 镜像发布,全部成功后才发布通用源码包与 SHA-256 校验文件。 #### 系统与架构兼容性 -| 类型 | 正式支持范围 | GitHub 发行标识 | +| 类型 | 正式支持范围 | 安装或镜像标识 | | --- | --- | --- | -| Linux x64 | Intel/AMD 64 位 VPS | `linux-amd64` | -| Linux x86 | Intel/AMD 32 位系统 | `linux-386` | -| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | `linux-arm64` | -| Linux ARM32 | ARMv7 设备 | `linux-armv7` | +| Linux x64 | Intel/AMD 64 位 VPS | 通用 Python 源码 / Docker `linux/amd64` | +| Linux x86 | Intel/AMD 32 位系统 | 通用 Python 源码 / Docker `linux/386` | +| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | 通用 Python 源码 / Docker `linux/arm64` | +| Linux ARM32 | ARMv7 设备 | 通用 Python 源码 / Docker `linux/arm/v7` | | Linux 发行版 | Debian、Ubuntu、CentOS、RHEL、Rocky、AlmaLinux、Fedora、Oracle Linux、Amazon Linux、Alpine | 使用同一正式核心 | | Docker | Linux 主机上的 amd64、386、arm64、arm/v7 | GHCR 多架构镜像 | > AimiliVPN 依赖 Linux 的 TUN、OpenVPN、iptables 和策略路由,因此不发布虚假的 Windows/macOS 原生兼容包。Windows 或 macOS 只能作为代理客户端使用,不能直接运行完整网关;Docker Desktop 同样不等同于具备宿主机 TUN 能力的 Linux 服务器。 -项目由纯 Python 标准库组成,不需要为 CPU 编译不同的 Python 二进制。GitHub Actions 会为每种架构生成经过相同测试的正式发行包,并实际构建对应架构的 Docker 镜像。 +项目由纯 Python 标准库组成,不需要为 CPU 编译不同的 Python 二进制。GitHub Release 只提供一个通用 Linux 源码包;GHCR 才会实际构建并发布四种 CPU 架构的 Docker 镜像。 --- @@ -82,21 +82,26 @@ bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/ma [Releases 页面](https://github.com/baoweise-bot/aimili-vpngate/releases/latest)提供以下文件: -- `aimilivpn-v2.1.0-linux-amd64.tar.gz`:x64 / x86_64。 -- `aimilivpn-v2.1.0-linux-386.tar.gz`:x86 32 位。 -- `aimilivpn-v2.1.0-linux-arm64.tar.gz`:ARM64 / AArch64。 -- `aimilivpn-v2.1.0-linux-armv7.tar.gz`:ARMv7 32 位。 -- `sha256sums.txt`:所有发行包的 SHA-256 校验值。 +- `aimilivpn-v2.1.0-linux-source.tar.gz`:适用于支持 Python 3 和项目系统依赖的 Linux x64、x86、ARM64、ARMv7 主机。 +- `sha256sums.txt`:源码包的 SHA-256 校验值。 #### 方法三:Docker / Docker Compose Docker 镜像地址:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`。仓库中的 [`compose.yaml`](./compose.yaml) 已配置主机网络、`NET_ADMIN` 和 TUN 设备: ```bash +docker compose pull docker compose up -d docker logs -f aimilivpn ``` +无法访问 GHCR 或需要自行审查构建过程时,也可以在 VPS 的仓库目录本地构建: + +```bash +docker compose build +docker compose up -d +``` + 也可以直接运行: ```bash @@ -105,12 +110,13 @@ docker run -d \ --restart unless-stopped \ --network host \ --cap-add NET_ADMIN \ + --cap-add NET_RAW \ --device /dev/net/tun:/dev/net/tun \ -v aimilivpn-data:/data \ ghcr.io/baoweise-bot/aimili-vpngate:2.1 ``` -> Docker 方式只支持具备 `/dev/net/tun` 的 Linux 主机。管理页面默认端口为 `8787`,本机 HTTP/SOCKS5 代理默认端口为 `7928`。 +> Docker 方式只支持具备 `/dev/net/tun` 的 Linux 主机,并需要 `NET_ADMIN`、`NET_RAW` 能力。管理页面默认端口为 `8787`,本机 HTTP/SOCKS5 代理默认端口为 `7928`。容器检测到新版本后会提示重新拉取镜像,不会在容器内执行 `git pull`。 --- @@ -157,7 +163,7 @@ docker run -d \ ### 🛠️ 核心功能与操作说明 * **合并操作面板**:将“更新节点”与“立即检测补齐”合并,一键触发多线程拉取与测速。 -* **正式版更新检测**:Web 顶部版本菜单可以检查 GitHub 最新稳定 Release,并提供 `main` 主分支和正式版下载入口。 +* **正式版更新检测**:Web 顶部版本菜单可以检查 GitHub 最新稳定 Release;源码部署提示 `ml update`,Docker 部署提示重新拉取并启动镜像。 * **多国家发现范围**:节点表可实时勾选多个国家;点击“更新节点”后保存范围并影响后台周期拉取。 * **延迟来源区分**:实测延迟正常显示,官方 Ping 回退值使用弱化样式并标注为预估。 * **网关状态面板**: @@ -253,6 +259,17 @@ bash <(curl -Ls https://raw.githubusercontent.com/baoweise-bot/aimili-vpngate/ma > 💡 **Quick Note**: Once installed, copy the printed URL from the terminal to access the Web UI. Type the `ml` command in the terminal to summon the interactive CLI management console. +#### Docker / Docker Compose + +GitHub publishes prebuilt images for `linux/amd64`, `linux/386`, `linux/arm64`, and `linux/arm/v7` under `ghcr.io/baoweise-bot/aimili-vpngate:2.1`: + +```bash +docker compose pull +docker compose up -d +``` + +To build natively on the VPS instead, run `docker compose build` before `docker compose up -d`. Docker requires a Linux host with `/dev/net/tun`, `NET_ADMIN`, and `NET_RAW` support. + --- ### 💡 Quick Start Guide diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 22e5587..080a9e1 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,11 +8,12 @@ V2.1 是仅从 `main` 主分支发布的首个正式版本标志。 - 修复节点获取缓慢、连接断开和切换失败时误伤现有连接的问题。 - 恢复节点延迟列,区分本机实测值与 VPNGate 官方预估值。 - 加入国旗、实时多选国家筛选、国家范围持久化和单节点测试。 -- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并只保留 `main` 主分支入口。 +- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示对应更新命令。 - `install.sh` 和 `ml update` 统一只更新 `origin/main`。 -- GitHub Release 提供 Linux `amd64`、`386`、`arm64`、`armv7` 发行包与 SHA-256 校验文件。 -- GHCR 提供相同四种架构的 Docker 镜像。 +- GitHub Release 提供一个适用于 Linux `amd64`、`386`、`arm64`、`armv7` 的通用 Python 源码包与 SHA-256 校验文件。 +- GHCR 提供经过逐架构冒烟测试的 `amd64`、`386`、`arm64`、`arm/v7` Docker 镜像,并同时发布 `2.1.0`、`2.1`、`latest` 标签。 +- Docker 用户默认拉取 GitHub 预构建镜像,也可以使用仓库中的 Dockerfile 在 VPS 本地构建。 ## 兼容范围 -应用依赖 Linux TUN、OpenVPN、iptables 和策略路由,因此正式支持 Linux 主机。Docker 也必须运行在具备 `/dev/net/tun` 的 Linux 主机上,并授予 `NET_ADMIN` 能力。 +应用依赖 Linux TUN、OpenVPN、iptables 和策略路由,因此正式支持 Linux 主机。Docker 也必须运行在具备 `/dev/net/tun` 的 Linux 主机上,并授予 `NET_ADMIN` 与 `NET_RAW` 能力。 diff --git a/compose.yaml b/compose.yaml index 32008fc..7420622 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,13 +1,21 @@ services: aimilivpn: image: ghcr.io/baoweise-bot/aimili-vpngate:2.1 + build: + context: . + args: + BUILD_VERSION: local container_name: aimilivpn network_mode: host + cap_drop: + - ALL cap_add: - NET_ADMIN + - NET_RAW devices: - /dev/net/tun:/dev/net/tun environment: + DEPLOYMENT_MODE: docker VPNGATE_DATA_DIR: /data UI_HOST: "0.0.0.0" UI_PORT: "8787" @@ -16,6 +24,8 @@ services: volumes: - aimilivpn-data:/data init: true + security_opt: + - no-new-privileges:true restart: unless-stopped volumes: diff --git a/install.sh b/install.sh index 359fcf9..16cac59 100644 --- a/install.sh +++ b/install.sh @@ -137,6 +137,7 @@ WorkingDirectory=${INSTALL_DIR} ExecStart=/usr/bin/python3 vpngate_manager.py Restart=always RestartSec=5 +Environment=DEPLOYMENT_MODE=source EnvironmentFile=-/etc/default/aimilivpn [Install] diff --git a/scripts/build_release_archives.py b/scripts/build_release_archives.py index ccba93f..4b8e01a 100644 --- a/scripts/build_release_archives.py +++ b/scripts/build_release_archives.py @@ -3,21 +3,13 @@ from __future__ import annotations import argparse import hashlib -import json import shutil import tarfile import tempfile from pathlib import Path - -TARGETS = { - "amd64": {"cpu": "x86_64 / x64", "docker_platform": "linux/amd64"}, - "386": {"cpu": "x86 / i386 32-bit", "docker_platform": "linux/386"}, - "arm64": {"cpu": "ARM64 / AArch64", "docker_platform": "linux/arm64"}, - "armv7": {"cpu": "ARMv7 32-bit", "docker_platform": "linux/arm/v7"}, -} - RELEASE_FILES = [ + ".dockerignore", "VERSION", "README.md", "RELEASE_NOTES.md", @@ -40,7 +32,7 @@ def sha256(path: Path) -> str: return digest.hexdigest() -def build_archives(root: Path, output_dir: Path) -> list[Path]: +def build_archive(root: Path, output_dir: Path) -> Path: version = (root / "VERSION").read_text(encoding="utf-8").strip() version_parts = version.split(".") if len(version_parts) not in (2, 3) or any(not part.isdigit() for part in version_parts): @@ -53,63 +45,38 @@ def build_archives(root: Path, output_dir: Path) -> list[Path]: raise FileNotFoundError("发行文件缺失: mirror") output_dir.mkdir(parents=True, exist_ok=True) - archives: list[Path] = [] + for stale_archive in output_dir.glob("aimilivpn-v*-linux-*.tar.gz"): + stale_archive.unlink() + checksum_path = output_dir / "sha256sums.txt" + checksum_path.unlink(missing_ok=True) + with tempfile.TemporaryDirectory(prefix="aimilivpn-release-") as temp_name: temp_root = Path(temp_name) - for architecture, metadata in TARGETS.items(): - package_name = f"aimilivpn-v{version}-linux-{architecture}" - package_root = temp_root / package_name - package_root.mkdir() + package_name = f"aimilivpn-v{version}-linux-source" + package_root = temp_root / package_name + package_root.mkdir() - for name in RELEASE_FILES: - shutil.copy2(root / name, package_root / name) - shutil.copytree(root / "mirror", package_root / "mirror") - build_info = { - "product": "AimiliVPN", - "version": version, - "release": f"V{'.'.join(version.split('.')[:2])} 正式版", - "branch": "main", - "operating_system": "Linux", - "architecture": architecture, - **metadata, - "supported_distributions": [ - "Debian", - "Ubuntu", - "CentOS", - "RHEL", - "Rocky Linux", - "AlmaLinux", - "Fedora", - "Oracle Linux", - "Amazon Linux", - "Alpine Linux", - ], - } - (package_root / "BUILD_INFO.json").write_text( - json.dumps(build_info, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) + for name in RELEASE_FILES: + shutil.copy2(root / name, package_root / name) + shutil.copytree(root / "mirror", package_root / "mirror") - archive_path = output_dir / f"{package_name}.tar.gz" - with tarfile.open(archive_path, "w:gz") as archive: - archive.add(package_root, arcname=package_name) - archives.append(archive_path) + archive_path = output_dir / f"{package_name}.tar.gz" + with tarfile.open(archive_path, "w:gz") as archive: + archive.add(package_root, arcname=package_name) - checksum_lines = [f"{sha256(path)} {path.name}" for path in archives] - checksum_path = output_dir / "sha256sums.txt" + checksum_lines = [f"{sha256(archive_path)} {archive_path.name}"] checksum_path.write_text("\n".join(checksum_lines) + "\n", encoding="ascii") - return archives + return archive_path def main() -> int: - parser = argparse.ArgumentParser(description="构建 AimiliVPN Linux 多架构发行包") + parser = argparse.ArgumentParser(description="构建 AimiliVPN Linux 通用 Python 源码发行包") parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) parser.add_argument("--output-dir", type=Path, default=Path("dist")) args = parser.parse_args() - archives = build_archives(args.root.resolve(), args.output_dir.resolve()) - for archive in archives: - print(archive) + archive = build_archive(args.root.resolve(), args.output_dir.resolve()) + print(archive) return 0 diff --git a/tests/test_manager_logic.py b/tests/test_manager_logic.py index ab361f2..976de4e 100644 --- a/tests/test_manager_logic.py +++ b/tests/test_manager_logic.py @@ -403,6 +403,7 @@ class ManagerLogicTests(unittest.TestCase): self.assertIn("/releases/latest", manager.INDEX_HTML) self.assertNotIn("/tree/bate", manager.INDEX_HTML) self.assertNotIn(">测试版<", manager.INDEX_HTML) + self.assertIn('id="deployment_mode_label"', manager.INDEX_HTML) def test_installer_updates_only_from_main(self) -> None: install_text = (manager.ROOT_DIR / "install.sh").read_text(encoding="utf-8") @@ -447,6 +448,39 @@ class ManagerLogicTests(unittest.TestCase): self.assertFalse(result["update_available"]) self.assertEqual("V2.1 正式版", result["current_version_label"]) + def test_latest_release_check_reports_source_update_command(self) -> None: + release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False} + with ( + mock.patch.object(manager, "fetch_api_text", return_value=json.dumps(release)), + mock.patch.object(manager, "DEPLOYMENT_MODE", "source"), + mock.patch.object(manager, "DEPLOYMENT_MODE_LABEL", "Python 源码"), + mock.patch.object(manager, "UPDATE_COMMAND", "ml update"), + ): + result = manager.check_latest_release() + + self.assertEqual("source", result["deployment_mode"]) + self.assertEqual("ml update", result["update_command"]) + + def test_latest_release_check_reports_docker_update_command(self) -> None: + release = {"tag_name": "v2.2.0", "draft": False, "prerelease": False} + with ( + mock.patch.object(manager, "fetch_api_text", return_value=json.dumps(release)), + mock.patch.object(manager, "DEPLOYMENT_MODE", "docker"), + mock.patch.object(manager, "DEPLOYMENT_MODE_LABEL", "Docker 容器"), + mock.patch.object( + manager, + "UPDATE_COMMAND", + "docker compose pull && docker compose up -d", + ), + ): + result = manager.check_latest_release() + + self.assertEqual("docker", result["deployment_mode"]) + self.assertEqual( + "docker compose pull && docker compose up -d", + result["update_command"], + ) + def test_fetch_uses_github_mirror_after_official_sources(self) -> None: csv_text = valid_snapshot() diff --git a/vpngate_manager.py b/vpngate_manager.py index fbf6925..125459c 100644 --- a/vpngate_manager.py +++ b/vpngate_manager.py @@ -117,6 +117,15 @@ LOCAL_PROXY_PORT = env_int("LOCAL_PROXY_PORT", 7928, 1, 65535) UI_HOST = os.environ.get("UI_HOST", "::") UI_PORT = env_int("UI_PORT", 8787, 1, 65535) INVALID_BACKOFF_SECONDS = env_int("INVALID_BACKOFF_SECONDS", 30 * 60, 1) +DEPLOYMENT_MODE = os.environ.get("DEPLOYMENT_MODE", "source").strip().lower() +if DEPLOYMENT_MODE not in {"source", "docker"}: + DEPLOYMENT_MODE = "source" +DEPLOYMENT_MODE_LABEL = "Docker 容器" if DEPLOYMENT_MODE == "docker" else "Python 源码" +UPDATE_COMMAND = ( + "docker compose pull && docker compose up -d" + if DEPLOYMENT_MODE == "docker" + else "ml update" +) ROOT_DIR = Path(sys.executable).resolve().parent if globals().get("__compiled__") else Path(__file__).resolve().parent DEFAULT_APP_VERSION = "2.1.0" @@ -429,6 +438,8 @@ def get_state() -> dict[str, Any]: state.setdefault("blacklisted_nodes", 0) state["app_version"] = APP_VERSION state["app_version_label"] = APP_VERSION_LABEL + state["deployment_mode"] = DEPLOYMENT_MODE + state["deployment_mode_label"] = DEPLOYMENT_MODE_LABEL # Pre-populate settings inputs in UI ui_cfg = load_ui_config() @@ -765,6 +776,9 @@ def check_latest_release() -> dict[str, Any]: "update_available": latest_version > current_version, "release_url": release_url, "main_branch_url": GITHUB_MAIN_BRANCH_URL, + "deployment_mode": DEPLOYMENT_MODE, + "deployment_mode_label": DEPLOYMENT_MODE_LABEL, + "update_command": UPDATE_COMMAND, } def is_certificate_verification_error(exc: BaseException) -> bool: @@ -3915,7 +3929,7 @@ INDEX_HTML = r"""