mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-06 16:16:53 +08:00
Refine V2.1 source and Docker release
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-4
@@ -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` 能力。
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
+24
-2
@@ -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"""<!doctype html>
|
||||
<div id="github_dropdown" class="dropdown-content github-dropdown">
|
||||
<div class="version-current">
|
||||
<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>
|
||||
<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>
|
||||
@@ -4634,6 +4648,10 @@ function render(){
|
||||
const versionLabel = state.app_version_label || "V2.1 正式版";
|
||||
if ($("github_version_label")) $("github_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 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 (result.update_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 {
|
||||
statusBox.className = "update-check-status current";
|
||||
statusBox.textContent = `当前 ${result.current_version_label} 已是最新正式版。`;
|
||||
|
||||
Reference in New Issue
Block a user