Refine V2.1 source and Docker release

This commit is contained in:
Aimili
2026-08-26 23:42:00 +08:00
parent a9cf220500
commit 92910ab2d6
9 changed files with 209 additions and 97 deletions
+81 -22
View File
@@ -4,11 +4,25 @@ on:
push: push:
tags: tags:
- "v*" - "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: permissions:
contents: write contents: write
packages: write packages: write
env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
jobs: jobs:
test: test:
name: Test Python ${{ matrix.python-version }} name: Test Python ${{ matrix.python-version }}
@@ -19,12 +33,14 @@ jobs:
python-version: ["3.9", "3.11", "3.13"] python-version: ["3.9", "3.11", "3.13"]
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
- uses: actions/setup-python@v7 - uses: actions/setup-python@v7
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Verify version tag - name: Verify formal version tag
shell: bash 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 - name: Compile Python sources
run: python -m py_compile vpngate_manager.py vpn_utils.py proxy_server.py snapshot_utils.py run: python -m py_compile vpngate_manager.py vpn_utils.py proxy_server.py snapshot_utils.py
- name: Validate installation script - name: Validate installation script
@@ -34,36 +50,53 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: python -m unittest discover -s tests -v run: python -m unittest discover -s tests -v
release: docker-smoke:
name: Build Linux release archives name: Smoke test Docker ${{ matrix.slug }}
needs: test needs: test
runs-on: ubuntu-latest 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: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: with:
python-version: "3.11" ref: ${{ env.RELEASE_TAG }}
- name: Build architecture-labelled archives - uses: docker/setup-qemu-action@v4
run: python scripts/build_release_archives.py --output-dir dist - uses: docker/setup-buildx-action@v4
- name: Create GitHub formal release - name: Build architecture image for smoke test
env: uses: docker/build-push-action@v7
GH_TOKEN: ${{ github.token }} 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 shell: bash
run: | run: |
title="AimiliVPN V$(cut -d. -f1,2 VERSION) 正式版" docker run --rm --platform "${{ matrix.platform }}" \
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then "aimilivpn-smoke:${{ matrix.slug }}" \
gh release upload "${GITHUB_REF_NAME}" dist/* --clobber --repo "${GITHUB_REPOSITORY}" python3 -c 'import vpngate_manager as app; print(app.APP_VERSION)'
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: docker:
name: Build multi-architecture Docker image name: Publish multi-architecture Docker image
needs: test needs: docker-smoke
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with:
ref: ${{ env.RELEASE_TAG }}
- uses: docker/setup-qemu-action@v4 - uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4 - uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4 - uses: docker/login-action@v4
@@ -75,11 +108,12 @@ jobs:
id: version id: version
shell: bash shell: bash
run: | run: |
version="${GITHUB_REF_NAME#v}" version="${RELEASE_TAG#v}"
minor="$(printf '%s' "${version}" | cut -d. -f1,2)" minor="$(printf '%s' "${version}" | cut -d. -f1,2)"
echo "version=${version}" >> "${GITHUB_OUTPUT}" echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "minor=${minor}" >> "${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: with:
context: . context: .
push: true push: true
@@ -93,3 +127,28 @@ jobs:
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
provenance: mode=max provenance: mode=max
sbom: true 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
+1
View File
@@ -27,6 +27,7 @@ COPY mirror ./mirror
ENV PYTHONUNBUFFERED=1 \ ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
DEPLOYMENT_MODE=docker \
VPNGATE_DATA_DIR=/data \ VPNGATE_DATA_DIR=/data \
UI_HOST=0.0.0.0 \ UI_HOST=0.0.0.0 \
UI_PORT=8787 \ UI_PORT=8787 \
+32 -15
View File
@@ -27,23 +27,23 @@ V2.1 是项目启用正式版本标志后的首个稳定版本。仓库、安装
- **国家筛选**:支持带国旗和节点数量的实时多选筛选,选择范围保存到本机,并作用于手动更新和后台周期同步。 - **国家筛选**:支持带国旗和节点数量的实时多选筛选,选择范围保存到本机,并作用于手动更新和后台周期同步。
- **节点操作**:恢复单节点“检测”按钮,补齐收藏、检测、连接和断开状态逻辑。 - **节点操作**:恢复单节点“检测”按钮,补齐收藏、检测、连接和断开状态逻辑。
- **镜像同步**GitHub Pages 每 15 分钟同步并校验官方节点快照,官方 API 被屏蔽时自动回退。 - **镜像同步**GitHub Pages 每 15 分钟同步并校验官方节点快照,官方 API 被屏蔽时自动回退。
- **Web 更新检测**:页面顶部显示 `V2.1 正式版`可直接检查 GitHub 最新稳定 Release;只展示 `main` 和正式版下载入口 - **Web 更新检测**:页面顶部显示 `V2.1 正式版`检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示正确更新命令
- **正式发布链路**GitHub 标签自动运行 Python 兼容测试、构建四类 Linux 发行包、生成 SHA-256 校验文件并发布多架构 Docker 镜像 - **正式发布链路**GitHub 标签或手动重跑会依次执行 Python 兼容测试、四架构 Docker 冒烟测试、GHCR 镜像发布,全部成功后才发布通用源码包与 SHA-256 校验文件
#### 系统与架构兼容性 #### 系统与架构兼容性
| 类型 | 正式支持范围 | GitHub 发行标识 | | 类型 | 正式支持范围 | 安装或镜像标识 |
| --- | --- | --- | | --- | --- | --- |
| Linux x64 | Intel/AMD 64 位 VPS | `linux-amd64` | | Linux x64 | Intel/AMD 64 位 VPS | 通用 Python 源码 / Docker `linux/amd64` |
| Linux x86 | Intel/AMD 32 位系统 | `linux-386` | | Linux x86 | Intel/AMD 32 位系统 | 通用 Python 源码 / Docker `linux/386` |
| Linux ARM64 | AArch64、ARMv8 VPS/开发板 | `linux-arm64` | | Linux ARM64 | AArch64、ARMv8 VPS/开发板 | 通用 Python 源码 / Docker `linux/arm64` |
| Linux ARM32 | ARMv7 设备 | `linux-armv7` | | Linux ARM32 | ARMv7 设备 | 通用 Python 源码 / Docker `linux/arm/v7` |
| Linux 发行版 | Debian、Ubuntu、CentOS、RHEL、Rocky、AlmaLinux、Fedora、Oracle Linux、Amazon Linux、Alpine | 使用同一正式核心 | | Linux 发行版 | Debian、Ubuntu、CentOS、RHEL、Rocky、AlmaLinux、Fedora、Oracle Linux、Amazon Linux、Alpine | 使用同一正式核心 |
| Docker | Linux 主机上的 amd64、386、arm64、arm/v7 | GHCR 多架构镜像 | | Docker | Linux 主机上的 amd64、386、arm64、arm/v7 | GHCR 多架构镜像 |
> AimiliVPN 依赖 Linux 的 TUN、OpenVPN、iptables 和策略路由,因此不发布虚假的 Windows/macOS 原生兼容包。Windows 或 macOS 只能作为代理客户端使用,不能直接运行完整网关;Docker Desktop 同样不等同于具备宿主机 TUN 能力的 Linux 服务器。 > 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)提供以下文件: [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-source.tar.gz`适用于支持 Python 3 和项目系统依赖的 Linux x64、x86、ARM64、ARMv7 主机
- `aimilivpn-v2.1.0-linux-386.tar.gz`x86 32 位 - `sha256sums.txt`:源码包的 SHA-256 校验值
- `aimilivpn-v2.1.0-linux-arm64.tar.gz`ARM64 / AArch64。
- `aimilivpn-v2.1.0-linux-armv7.tar.gz`ARMv7 32 位。
- `sha256sums.txt`:所有发行包的 SHA-256 校验值。
#### 方法三:Docker / Docker Compose #### 方法三:Docker / Docker Compose
Docker 镜像地址:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`。仓库中的 [`compose.yaml`](./compose.yaml) 已配置主机网络、`NET_ADMIN` 和 TUN 设备: Docker 镜像地址:`ghcr.io/baoweise-bot/aimili-vpngate:2.1`。仓库中的 [`compose.yaml`](./compose.yaml) 已配置主机网络、`NET_ADMIN` 和 TUN 设备:
```bash ```bash
docker compose pull
docker compose up -d docker compose up -d
docker logs -f aimilivpn docker logs -f aimilivpn
``` ```
无法访问 GHCR 或需要自行审查构建过程时,也可以在 VPS 的仓库目录本地构建:
```bash
docker compose build
docker compose up -d
```
也可以直接运行: 也可以直接运行:
```bash ```bash
@@ -105,12 +110,13 @@ docker run -d \
--restart unless-stopped \ --restart unless-stopped \
--network host \ --network host \
--cap-add NET_ADMIN \ --cap-add NET_ADMIN \
--cap-add NET_RAW \
--device /dev/net/tun:/dev/net/tun \ --device /dev/net/tun:/dev/net/tun \
-v aimilivpn-data:/data \ -v aimilivpn-data:/data \
ghcr.io/baoweise-bot/aimili-vpngate:2.1 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 回退值使用弱化样式并标注为预估。 * **延迟来源区分**:实测延迟正常显示,官方 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. > 💡 **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 ### 💡 Quick Start Guide
+5 -4
View File
@@ -8,11 +8,12 @@ V2.1 是仅从 `main` 主分支发布的首个正式版本标志。
- 修复节点获取缓慢、连接断开和切换失败时误伤现有连接的问题。 - 修复节点获取缓慢、连接断开和切换失败时误伤现有连接的问题。
- 恢复节点延迟列,区分本机实测值与 VPNGate 官方预估值。 - 恢复节点延迟列,区分本机实测值与 VPNGate 官方预估值。
- 加入国旗、实时多选国家筛选、国家范围持久化和单节点测试。 - 加入国旗、实时多选国家筛选、国家范围持久化和单节点测试。
- Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并只保留 `main` 主分支入口 - Web 管理端加入正式版更新检测,只检查 GitHub 最新稳定 Release,并根据 Python 源码或 Docker 部署方式显示对应更新命令
- `install.sh``ml update` 统一只更新 `origin/main` - `install.sh``ml update` 统一只更新 `origin/main`
- GitHub Release 提供 Linux `amd64``386``arm64``armv7` 发行包与 SHA-256 校验文件。 - GitHub Release 提供一个适用于 Linux `amd64``386``arm64``armv7` 的通用 Python 源码包与 SHA-256 校验文件。
- GHCR 提供相同四种架构的 Docker 镜像 - 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` 能力。
+10
View File
@@ -1,13 +1,21 @@
services: services:
aimilivpn: aimilivpn:
image: ghcr.io/baoweise-bot/aimili-vpngate:2.1 image: ghcr.io/baoweise-bot/aimili-vpngate:2.1
build:
context: .
args:
BUILD_VERSION: local
container_name: aimilivpn container_name: aimilivpn
network_mode: host network_mode: host
cap_drop:
- ALL
cap_add: cap_add:
- NET_ADMIN - NET_ADMIN
- NET_RAW
devices: devices:
- /dev/net/tun:/dev/net/tun - /dev/net/tun:/dev/net/tun
environment: environment:
DEPLOYMENT_MODE: docker
VPNGATE_DATA_DIR: /data VPNGATE_DATA_DIR: /data
UI_HOST: "0.0.0.0" UI_HOST: "0.0.0.0"
UI_PORT: "8787" UI_PORT: "8787"
@@ -16,6 +24,8 @@ services:
volumes: volumes:
- aimilivpn-data:/data - aimilivpn-data:/data
init: true init: true
security_opt:
- no-new-privileges:true
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+1
View File
@@ -137,6 +137,7 @@ WorkingDirectory=${INSTALL_DIR}
ExecStart=/usr/bin/python3 vpngate_manager.py ExecStart=/usr/bin/python3 vpngate_manager.py
Restart=always Restart=always
RestartSec=5 RestartSec=5
Environment=DEPLOYMENT_MODE=source
EnvironmentFile=-/etc/default/aimilivpn EnvironmentFile=-/etc/default/aimilivpn
[Install] [Install]
+21 -54
View File
@@ -3,21 +3,13 @@ from __future__ import annotations
import argparse import argparse
import hashlib import hashlib
import json
import shutil import shutil
import tarfile import tarfile
import tempfile import tempfile
from pathlib import Path 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 = [ RELEASE_FILES = [
".dockerignore",
"VERSION", "VERSION",
"README.md", "README.md",
"RELEASE_NOTES.md", "RELEASE_NOTES.md",
@@ -40,7 +32,7 @@ def sha256(path: Path) -> str:
return digest.hexdigest() 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 = (root / "VERSION").read_text(encoding="utf-8").strip()
version_parts = version.split(".") version_parts = version.split(".")
if len(version_parts) not in (2, 3) or any(not part.isdigit() for part in version_parts): 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") raise FileNotFoundError("发行文件缺失: mirror")
output_dir.mkdir(parents=True, exist_ok=True) 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: with tempfile.TemporaryDirectory(prefix="aimilivpn-release-") as temp_name:
temp_root = Path(temp_name) temp_root = Path(temp_name)
for architecture, metadata in TARGETS.items(): package_name = f"aimilivpn-v{version}-linux-source"
package_name = f"aimilivpn-v{version}-linux-{architecture}" package_root = temp_root / package_name
package_root = temp_root / package_name package_root.mkdir()
package_root.mkdir()
for name in RELEASE_FILES: for name in RELEASE_FILES:
shutil.copy2(root / name, package_root / name) shutil.copy2(root / name, package_root / name)
shutil.copytree(root / "mirror", package_root / "mirror") 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",
)
archive_path = output_dir / f"{package_name}.tar.gz" archive_path = output_dir / f"{package_name}.tar.gz"
with tarfile.open(archive_path, "w:gz") as archive: with tarfile.open(archive_path, "w:gz") as archive:
archive.add(package_root, arcname=package_name) archive.add(package_root, arcname=package_name)
archives.append(archive_path)
checksum_lines = [f"{sha256(path)} {path.name}" for path in archives] checksum_lines = [f"{sha256(archive_path)} {archive_path.name}"]
checksum_path = output_dir / "sha256sums.txt"
checksum_path.write_text("\n".join(checksum_lines) + "\n", encoding="ascii") checksum_path.write_text("\n".join(checksum_lines) + "\n", encoding="ascii")
return archives return archive_path
def main() -> int: 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("--root", type=Path, default=Path(__file__).resolve().parents[1])
parser.add_argument("--output-dir", type=Path, default=Path("dist")) parser.add_argument("--output-dir", type=Path, default=Path("dist"))
args = parser.parse_args() args = parser.parse_args()
archives = build_archives(args.root.resolve(), args.output_dir.resolve()) archive = build_archive(args.root.resolve(), args.output_dir.resolve())
for archive in archives: print(archive)
print(archive)
return 0 return 0
+34
View File
@@ -403,6 +403,7 @@ class ManagerLogicTests(unittest.TestCase):
self.assertIn("/releases/latest", manager.INDEX_HTML) self.assertIn("/releases/latest", manager.INDEX_HTML)
self.assertNotIn("/tree/bate", manager.INDEX_HTML) self.assertNotIn("/tree/bate", manager.INDEX_HTML)
self.assertNotIn(">测试版<", 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: def test_installer_updates_only_from_main(self) -> None:
install_text = (manager.ROOT_DIR / "install.sh").read_text(encoding="utf-8") 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.assertFalse(result["update_available"])
self.assertEqual("V2.1 正式版", result["current_version_label"]) 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: def test_fetch_uses_github_mirror_after_official_sources(self) -> None:
csv_text = valid_snapshot() csv_text = valid_snapshot()
+24 -2
View File
@@ -117,6 +117,15 @@ LOCAL_PROXY_PORT = env_int("LOCAL_PROXY_PORT", 7928, 1, 65535)
UI_HOST = os.environ.get("UI_HOST", "::") UI_HOST = os.environ.get("UI_HOST", "::")
UI_PORT = env_int("UI_PORT", 8787, 1, 65535) UI_PORT = env_int("UI_PORT", 8787, 1, 65535)
INVALID_BACKOFF_SECONDS = env_int("INVALID_BACKOFF_SECONDS", 30 * 60, 1) 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 ROOT_DIR = Path(sys.executable).resolve().parent if globals().get("__compiled__") else Path(__file__).resolve().parent
DEFAULT_APP_VERSION = "2.1.0" DEFAULT_APP_VERSION = "2.1.0"
@@ -429,6 +438,8 @@ def get_state() -> dict[str, Any]:
state.setdefault("blacklisted_nodes", 0) state.setdefault("blacklisted_nodes", 0)
state["app_version"] = APP_VERSION state["app_version"] = APP_VERSION
state["app_version_label"] = APP_VERSION_LABEL 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 # Pre-populate settings inputs in UI
ui_cfg = load_ui_config() ui_cfg = load_ui_config()
@@ -765,6 +776,9 @@ def check_latest_release() -> dict[str, Any]:
"update_available": latest_version > current_version, "update_available": latest_version > current_version,
"release_url": release_url, "release_url": release_url,
"main_branch_url": GITHUB_MAIN_BRANCH_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: def is_certificate_verification_error(exc: BaseException) -> bool:
@@ -3915,7 +3929,7 @@ INDEX_HTML = r"""<!doctype html>
<div id="github_dropdown" class="dropdown-content github-dropdown"> <div id="github_dropdown" class="dropdown-content github-dropdown">
<div class="version-current"> <div class="version-current">
<div id="current_version_label" class="version-current-label">V2.1 正式版</div> <div id="current_version_label" class="version-current-label">V2.1 正式版</div>
<div class="version-current-meta">唯一更新通道main 主分支</div> <div id="deployment_mode_label" class="version-current-meta">Python 源码部署 · 更新通道main</div>
</div> </div>
<button id="check_update_btn" type="button" onclick="checkForUpdate(event)"> <button id="check_update_btn" type="button" onclick="checkForUpdate(event)">
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" style="width:14px; height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 1121.21 8H18.5" /></svg> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" style="width:14px; height:14px;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 1121.21 8H18.5" /></svg>
@@ -4634,6 +4648,10 @@ function render(){
const versionLabel = state.app_version_label || "V2.1 正式版"; const versionLabel = state.app_version_label || "V2.1 正式版";
if ($("github_version_label")) $("github_version_label").textContent = versionLabel; if ($("github_version_label")) $("github_version_label").textContent = versionLabel;
if ($("current_version_label")) $("current_version_label").textContent = versionLabel; if ($("current_version_label")) $("current_version_label").textContent = versionLabel;
if ($("deployment_mode_label")) {
const modeLabel = state.deployment_mode_label || "Python 源码";
$("deployment_mode_label").textContent = `${modeLabel}部署 · 更新通道main`;
}
const activeNodeId = state.active_openvpn_node_id; const activeNodeId = state.active_openvpn_node_id;
const activeNode = nodes.find(n => n && (n.active || n.id === activeNodeId)); const activeNode = nodes.find(n => n && (n.active || n.id === activeNodeId));
@@ -5166,7 +5184,11 @@ async function checkForUpdate(event) {
if (releaseLink && result.release_url) releaseLink.href = result.release_url; if (releaseLink && result.release_url) releaseLink.href = result.release_url;
if (result.update_available) { if (result.update_available) {
statusBox.className = "update-check-status available"; statusBox.className = "update-check-status available";
statusBox.textContent = `发现正式版 ${result.latest_tag}当前为 ${result.current_version_label}请打开正式版下载页更新`; if (result.deployment_mode === "docker") {
statusBox.textContent = `发现正式版 ${result.latest_tag}请在 VPS 执行${result.update_command}`;
} else {
statusBox.textContent = `发现正式版 ${result.latest_tag}请执行${result.update_command}`;
}
} else { } else {
statusBox.className = "update-check-status current"; statusBox.className = "update-check-status current";
statusBox.textContent = `当前 ${result.current_version_label} 已是最新正式版`; statusBox.textContent = `当前 ${result.current_version_label} 已是最新正式版`;