主要变更: 1. 动态更新功能 - 新增 Python 更新服务 (docker/update-service.py) - 添加系统更新前端页面 (frontend/src/pages/SystemUpdate.tsx) - 配置 Nginx 代理更新服务 API - 更新 Docker 启动脚本支持多进程管理 - 修复权限验证接口 (AuthController.verify) 2. Release 创建脚本 - 新增 create-release.sh 脚本支持快速创建 GitHub Release - 支持自动拼接 -beta 后缀(pre-release) - 支持无交互模式(--yes 参数) - 添加详细的使用文档 3. GitHub Actions 增强 - 添加更新包构建和上传流程 - 支持 Pre-release 检测和过滤 4. 文档完善 - 添加动态更新技术方案文档 - 添加 Docker 版本号确定流程文档 - 添加 Release 脚本使用说明
37 KiB
PolyHermes 动态更新技术方案
1. 方案概述
1.1 核心目标
在不重启 Docker 容器的情况下,实现后端 JAR 和前端产物的动态更新。
1.2 关键设计
- 单一更新包:前后端打包在一个 tar.gz 文件中
- 独立更新服务:Python Flask 服务(端口 9090)专门负责更新
- 进程隔离:更新服务与主应用分离,互不影响
- 自动回滚:更新失败自动恢复到旧版本
2. 架构设计
2.1 整体架构
┌────────────────────────────────────────────────────────┐
│ GitHub Releases │
│ Release v1.3.0 │
│ └── polyhermes-v1.3.0-update.tar.gz │
└──────────────────────┬─────────────────────────────────┘
│ HTTPS Download
▼
┌─────────────────────────────────────────────────────────┐
│ Docker 容器 │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Nginx (Port 80) │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ / → 前端静态文件 │ │ │
│ │ │ /api/ → http://localhost:8000 │ │ │
│ │ │ /api/update/ → http://localhost:9090 ←【新】│ │
│ │ └──────────────────────────────────────────┘ │ │
│ └──────┬───────────────────────────┬─────────────┘ │
│ │ │ │
│ │ │ │
│ ┌──────▼────────────┐ ┌────────▼──────────────┐ │
│ │ 后端应用 (8000) │ │ 更新服务 (9090) │ │
│ │ - 业务 API │ │ - GET /check │ │
│ │ - 无更新功能 │←✅ │ - POST /update │ │
│ └───────────────────┘ │ - GET /status │ │
│ │ - GET /logs │ │
│ │ - GET /version │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────┘
▲
│ HTTP/HTTPS
┌─────┴─────┐
│ 用户 │
└───────────┘
关键设计:
- ✅ Nginx 直接代理 -
/api/update/*直接转发到 Python (9090) - ✅ 后端无感知 - 不需要 SystemUpdateController
- ✅ 独立性强 - 后端崩溃不影响更新功能
2.2 进程架构
PID 1: start.sh
├── PID 10: python3 update-service.py (9090) ← 更新服务
├── PID 20: java -jar app.jar (8000) ← 主应用
└── PID 30: nginx -g "daemon off;" (80) ← 代理 + 静态文件
调用链路:
用户请求 /api/update/check
↓
Nginx 接收 (80)
↓
匹配规则 location /api/update/
↓
代理转发 proxy_pass http://localhost:9090/
↓
Python 处理 GET /check
↓
返回 JSON { code: 0, data: {...} }
关键:Nginx 作为前台进程保持容器存活,Java 和 Python 可被重启。
3. 更新包结构
polyhermes-v1.3.0-update.tar.gz
├── backend/
│ └── polyhermes.jar
├── frontend/
│ ├── index.html
│ ├── assets/
│ └── ...
└── version.json
version.json 格式:
{
"version": "1.3.0",
"tag": "v1.3.0",
"buildTime": "2026-01-20T15:00:00Z",
"releaseNotes": "## 新功能\n..."
}
4. GitHub Actions 配置
4.1 ⚠️ 重要:不创建新文件
不要创建 release-build.yml,而是直接修改现有的 .github/workflows/docker-build.yml。
原因:
- 现有的
docker-build.yml已经监听release.published事件 - 创建新文件会导致两个 workflow 同时触发(冲突)
- 在一个 workflow 中统一管理更高效
4.2 编译优化策略
关键优化:前后端只编译一次,产物复用三次
编译流程:
Steps 3-6: 编译产物
├── gradle bootJar → backend/build/libs/*.jar
└── npm run build → frontend/dist/*
复用1: Step 7
└── Create Update Package ← 复用编译产物
复用2: Step 10
└── Build Docker Image ← 复用编译产物(不再编译)
复用3: (可选)
└── 缓存供后续构建使用
时间节省:
- 传统方式:编译2次 ~ 15分钟
- 优化后:编译1次 ~ 8分钟
- 节省约 7 分钟
4.3 修改方案
在现有的 docker-build.yml 中增加以下步骤(在构建 Docker 镜像之前):
步骤1:增加权限声明
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: write # 【新增】需要写权限以上传 Assets
步骤2:构建后端 JAR(在 Docker 构建之前)
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Backend JAR
run: |
cd backend
gradle bootJar --no-daemon
echo "✅ 后端构建完成"
ls -lh build/libs/*.jar
步骤3:构建前端
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Build Frontend
run: |
cd frontend
npm ci
npm run build
echo "✅ 前端构建完成"
步骤4:打包更新包
- name: Create Update Package
run: |
echo "📦 打包更新包..."
mkdir -p update-package/backend update-package/frontend
# 复制后端 JAR
cp backend/build/libs/*.jar update-package/backend/polyhermes.jar
# 复制前端产物
cp -r frontend/dist/* update-package/frontend/
# 创建版本信息
cat > update-package/version.json <<EOF
{
"version": "${{ steps.extract_version.outputs.VERSION }}",
"tag": "${{ steps.extract_version.outputs.TAG }}",
"buildTime": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"releaseNotes": $(echo '${{ github.event.release.body }}' | jq -Rs .)
}
EOF
# 打包
cd update-package
tar -czf ../polyhermes-${{ steps.extract_version.outputs.TAG }}-update.tar.gz .
cd ..
echo "✅ 打包完成"
ls -lh polyhermes-*.tar.gz
步骤5:计算校验和
- name: Calculate Checksum
id: checksum
run: |
FILE="polyhermes-${{ steps.extract_version.outputs.TAG }}-update.tar.gz"
CHECKSUM=$(sha256sum "$FILE" | awk '{print $1}')
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_OUTPUT
echo "$CHECKSUM $FILE" > checksums.txt
步骤6:上传到 Release Assets
- name: Upload Update Package
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./polyhermes-${{ steps.extract_version.outputs.TAG }}-update.tar.gz
asset_name: polyhermes-${{ steps.extract_version.outputs.TAG }}-update.tar.gz
asset_content_type: application/gzip
- name: Upload Checksums
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: ./checksums.txt
asset_name: checksums.txt
asset_content_type: text/plain
4.3 完整的步骤顺序
现有步骤:
1. Checkout code
2. Extract version
3. Send Telegram (build started)
【新增步骤】:
4. Setup JDK 17
5. Build Backend JAR
6. Setup Node.js
7. Build Frontend
8. Create Update Package
9. Calculate Checksum
10. Upload Update Package
11. Upload Checksums
现有步骤(保持不变):
12. Set up Docker Buildx
13. Log in to Docker Hub
14. Build and push Docker image
15. Send Telegram notification
优势:
- ✅ 后端和前端只编译一次(Docker 构建可以复用)
- ✅ 所有发布产物在一个 workflow 中完成
- ✅ 避免 workflow 冲突
4.4 可选优化:修改 Telegram 通知
在最后的通知步骤中,可以增加更新包信息:
- name: Send Telegram notification
# ...
run: |
MESSAGE="✅ <b>PolyHermes ${TAG} 发布成功</b>
📦 版本: ${VERSION}
🔗 <a href=\"${RELEASE_URL}\">查看 Release</a>
<b>已上传:</b>
- Docker 镜像: wrbug/polyhermes:${TAG}
- 更新包: polyhermes-${TAG}-update.tar.gz
<b>使用:</b>
- Docker 部署: docker pull wrbug/polyhermes:${TAG}
- 在线更新: 系统设置 → 系统更新"
# ...发送消息
5. Docker 容器配置
5.1 编译策略(最佳实践)
采用混合方案:条件编译
编译策略:
GitHub Actions (BUILD_IN_DOCKER=false):
├── Actions 编译产物 ← 编译1次
└── Docker 跳过编译,复用产物 ← 不编译
本地 deploy.sh (BUILD_IN_DOCKER=true):
└── Docker 内部编译 ← 编译1次
核心思路:
- 通过
BUILD_IN_DOCKER参数控制编译位置 - GitHub Actions:先编译,Docker 复用(快速)
- 本地开发:Docker 自动编译(方便)
优势:
- ✅ Actions 编译1次,节省约 5-7 分钟
- ✅ 本地 deploy.sh 完全兼容,零改动
- ✅ Docker 镜像可独立构建
- ✅ 灵活性最高
5.2 Dockerfile(混合方案)
# 构建参数:控制是否在 Docker 内编译
# true = Docker 内部编译(本地开发)
# false = 使用外部产物(GitHub Actions)
ARG BUILD_IN_DOCKER=true
# ==================== 阶段1:构建后端 ====================
FROM gradle:8.5-jdk17 AS backend-build
ARG BUILD_IN_DOCKER
WORKDIR /app/backend
# 复制构建配置
COPY backend/build.gradle.kts backend/settings.gradle.kts ./
COPY backend/gradle ./gradle
# 条件:仅在 Docker 内部编译时下载依赖
RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \
gradle dependencies --no-daemon || true; \
fi
# 复制源码
COPY backend/src ./src
# 条件:仅在 Docker 内部编译时执行构建
RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \
echo "🔨 Docker 内部编译后端..."; \
gradle bootJar --no-daemon; \
else \
echo "⏭️ 跳过编译,使用外部产物"; \
fi
# ==================== 阶段2:构建前端 ====================
FROM node:18-alpine AS frontend-build
ARG BUILD_IN_DOCKER
WORKDIR /app/frontend
# 复制 package.json
COPY frontend/package*.json ./
# 条件:仅在 Docker 内部编译时安装依赖
RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \
npm ci; \
fi
# 复制源码
COPY frontend/ ./
# 条件:仅在 Docker 内部编译时执行构建
RUN if [ "$BUILD_IN_DOCKER" = "true" ]; then \
echo "🔨 Docker 内部编译前端..."; \
npm run build; \
else \
echo "⏭️ 跳过编译,使用外部产物"; \
fi
# ==================== 阶段3:运行环境 ====================
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
# 安装 Python 和依赖
RUN apt-get update && \
apt-get install -y nginx curl tzdata jq python3 python3-pip && \
pip3 install flask requests && \
rm -rf /var/lib/apt/lists/* && \
rm -rf /etc/nginx/sites-enabled/default
# 复制构建产物
# - 如果 BUILD_IN_DOCKER=true: 从构建阶段复制
# - 如果 BUILD_IN_DOCKER=false: 从 context 复制(外部产物)
COPY --from=backend-build /app/backend/build/libs/*.jar app.jar
COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/nginx.conf
# 创建更新服务相关目录和脚本
RUN mkdir -p /app/updates /app/backups /var/log/polyhermes
COPY docker/update-service.py /app/update-service.py
COPY docker/start.sh /app/start.sh
RUN chmod +x /app/start.sh
# 记录初始版本(从构建参数)
ARG VERSION=dev
ARG GIT_TAG=dev
RUN echo "{\"version\":\"${VERSION}\",\"tag\":\"${GIT_TAG}\",\"buildTime\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > /app/version.json
# 设置目录权限
RUN useradd -m -u 1000 appuser && \
mkdir -p /var/log/nginx /var/lib/nginx /var/cache/nginx /var/run && \
chown -R appuser:appuser /app && \
chown -R root:root /usr/share/nginx/html /var/log/nginx /var/lib/nginx /var/cache/nginx /etc/nginx /var/run
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost/api/system/health || exit 1
ENTRYPOINT ["/app/start.sh"]
关键设计:
ARG BUILD_IN_DOCKER=true- 默认在 Docker 内编译(本地开发友好)- 条件判断
if [ "$BUILD_IN_DOCKER" = "true" ]- 根据参数决定是否编译 COPY --from=backend-build- 无论如何都从构建阶段复制(统一路径)
5.3 GitHub Actions 使用方式
在 .github/workflows/docker-build.yml 中:
steps:
# 【先编译产物】
- name: Setup JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build Backend JAR
run: |
cd backend
gradle bootJar --no-daemon
echo "✅ 后端编译完成"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Build Frontend
run: |
cd frontend
npm ci
npm run build
echo "✅ 前端编译完成"
# 【打包更新包 - 复用产物】
- name: Create Update Package
run: |
mkdir -p update-package/backend update-package/frontend
cp backend/build/libs/*.jar update-package/backend/polyhermes.jar
cp -r frontend/dist/* update-package/frontend/
# ... 打包
# 【构建 Docker - 跳过编译】
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: wrbug/polyhermes:${{ steps.version.outputs.TAG }}
build-args: |
BUILD_IN_DOCKER=false ← 关键:不在 Docker 内编译
VERSION=${{ steps.version.outputs.VERSION }}
GIT_TAG=${{ steps.version.outputs.TAG }}
流程:
1. Actions 编译产物 → backend/build/libs/*.jar, frontend/dist/
2. 打包更新包(复用) → polyhermes-v1.3.0-update.tar.gz
3. Docker 构建(跳过编译) → 直接 COPY 已编译的产物
时间:约 8 分钟(编译1次)
5.4 本地 deploy.sh 使用方式
保持完全不变!deploy.sh 无需任何修改:
# deploy.sh(无需修改)
docker-compose build # ← 默认 BUILD_IN_DOCKER=true
# 或直接
docker build -t polyhermes:local . # ← 也会在 Docker 内编译
流程:
1. docker build 开始
2. BUILD_IN_DOCKER=true(默认值)
3. Docker 内部执行 gradle bootJar
4. Docker 内部执行 npm run build
5. 构建完成
时间:约 12 分钟(首次),约 5 分钟(有缓存)
5.5 本地开发其他方式
方式1:直接运行(推荐)
# 后端
cd backend
gradle bootRun
# 前端(新终端)
cd frontend
npm run dev
方式2:先编译再构建(快速)
# 1. 编译
cd backend && gradle bootJar && cd ..
cd frontend && npm ci && npm run build && cd ..
# 2. Docker 构建(跳过编译)
docker build -t polyhermes:local \
--build-arg BUILD_IN_DOCKER=false \
--build-arg VERSION=local .
5.7 为什么选择混合方案?
核心问题:如何平衡 GitHub Actions 的性能和本地开发的便利性?
| 方案 | Actions 时间 | deploy.sh | 维护成本 | 推荐度 |
|---|---|---|---|---|
| 简化版 | 8分钟 | ❌ 需要改造 | 低 | ⭐⭐⭐ |
| 多阶段(2次编译) | 13分钟 | ✅ 兼容 | 低 | ⭐⭐⭐ |
| 混合方案 | 8分钟 | ✅ 兼容 | 中 | ⭐⭐⭐⭐⭐ |
混合方案的价值:
-
✅ GitHub Actions 性能最优
- 只编译1次(8分钟)
- 与简化版相同
-
✅ 本地开发零影响
./deploy.sh保持不变- 不需要指导用户改变习惯
-
✅ Docker 镜像自包含
- 可以独立构建
- 不依赖外部产物
-
⚠️ 唯一代价
- Dockerfile 增加条件判断
- 但这是一次性成本
对比示例:
用户A(GitHub Actions 发布):
→ BUILD_IN_DOCKER=false
→ 8 分钟完成
用户B(本地部署测试):
→ ./deploy.sh
→ BUILD_IN_DOCKER=true(自动)
→ 12 分钟完成(首次),5分钟(有缓存)
→ 无需任何额外操作
5.8 启动脚本
docker/start.sh:
#!/bin/bash
set -e
# 1. 启动更新服务(后台,端口 9090)
python3 /app/update-service.py &
# 2. 启动后端服务(后台,端口 8000)
java -jar /app/app.jar --spring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} &
# 3. 等待后端就绪
for i in {1..60}; do
curl -f http://localhost:8000/api/system/health && break
sleep 1
done
# 4. 启动 Nginx(前台运行,保持容器存活)
exec nginx -g "daemon off;"
5.3 更新服务 (update-service.py)
核心功能:
@app.route('/check') # 检查更新
@app.route('/update') # 执行更新
@app.route('/status') # 更新状态
@app.route('/logs') # 更新日志
详细代码见实施方案文档。
6. Nginx 反向代理配置
6.1 方案说明
采用 Nginx 直接代理方案,不需要后端 Controller
优势:
- ✅ 减少调用链路(前端 → Nginx → Python,而不是 前端 → 后端 → Python)
- ✅ 减少代码量(不需要写 Controller 和 DTO)
- ✅ 更新服务真正独立(后端崩溃不影响更新功能)
- ✅ 降低维护成本
6.2 Nginx 配置
修改 docker/nginx.conf:
http {
# ... 现有配置保持不变
server {
listen 80;
server_name _;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 后端 API(保持不变)
location /api/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 【新增】更新服务 API(直接代理到 Python)
location /api/update/ {
# 代理到更新服务
proxy_pass http://localhost:9090/;
# 传递请求头
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 传递认证头(用于权限验证)
proxy_set_header Authorization $http_authorization;
# 超时设置(更新操作可能需要较长时间)
proxy_read_timeout 300s;
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
}
}
}
URL 映射:
前端请求 → Nginx 代理到 → Python 处理
/api/update/check → http://localhost:9090/check → GET /check
/api/update/execute → http://localhost:9090/update → POST /update
/api/update/status → http://localhost:9090/status → GET /status
/api/update/logs → http://localhost:9090/logs → GET /logs
/api/update/version → http://localhost:9090/version → GET /version
6.3 Python 更新服务权限验证
在 update-service.py 中增加权限验证:
# update-service.py
import requests
BACKEND_URL = 'http://localhost:8000'
def check_admin_permission(request):
"""
检查管理员权限
从请求头获取 Authorization token,调用后端验证
"""
auth_header = request.headers.get('Authorization')
if not auth_header:
return False
try:
# 调用后端的权限验证接口
response = requests.get(
f'{BACKEND_URL}/api/auth/verify',
headers={'Authorization': auth_header},
timeout=3
)
return response.status_code == 200
except Exception as e:
logger.error(f"权限验证失败: {e}")
return False
@app.route('/update', methods=['POST'])
def trigger_update():
"""执行更新(需要管理员权限)"""
# 【新增】权限检查
if not check_admin_permission(request):
return jsonify({
'code': 403,
'data': None,
'message': '需要管理员权限'
}), 403
if update_status['updating']:
return jsonify({
'code': 409,
'data': None,
'message': '正在更新中,请稍后'
}), 409
# 异步执行更新
import threading
thread = threading.Thread(target=perform_update, args=('latest',))
thread.start()
return jsonify({
'code': 0,
'data': '更新已启动',
'message': 'success'
})
@app.route('/logs', methods=['GET'])
def get_logs():
"""获取更新日志(需要管理员权限)"""
# 【新增】权限检查
if not check_admin_permission(request):
return jsonify({
'code': 403,
'data': None,
'message': '需要管理员权限'
}), 403
try:
if LOG_FILE.exists():
with open(LOG_FILE) as f:
lines = f.readlines()
return jsonify({
'code': 0,
'data': ''.join(lines[-1000:]),
'message': 'success'
})
return jsonify({
'code': 0,
'data': '',
'message': 'success'
})
except Exception as e:
return jsonify({
'code': 500,
'data': None,
'message': str(e)
}), 500
统一的 API 响应格式:
# 成功响应
{
"code": 0,
"data": {...},
"message": "success"
}
# 错误响应
{
"code": 403, # 或 500, 409 等
"data": null,
"message": "错误信息"
}
6.4 前端调用方式
前端直接调用 /api/update/*,Nginx 会自动代理到 Python 服务:
// 前端 API 调用
import axios from 'axios';
// 检查更新(无需权限)
const checkUpdate = async () => {
const response = await axios.get('/api/update/check');
return response.data; // { code: 0, data: {...}, message: 'success' }
};
// 执行更新(需要管理员权限,会自动从 localStorage 获取 token)
const executeUpdate = async () => {
const response = await axios.post('/api/update/execute');
return response.data;
};
// 获取更新状态(无需权限)
const getUpdateStatus = async () => {
const response = await axios.get('/api/update/status');
return response.data;
};
// 获取更新日志(需要管理员权限)
const getUpdateLogs = async () => {
const response = await axios.get('/api/update/logs');
return response.data;
};
Axios 自动携带 Authorization:
// axios 拦截器(已有配置)
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
6.5 不需要创建的文件
删除以下内容(不需要实现):
- ❌
SystemUpdateController.kt- 不需要后端 Controller - ❌
UpdateDto.kt- 不需要 DTO - ❌
SystemUpdateService.kt- 不需要 Service 层
节省代码量:约 200 行
7. 版本号识别机制
7.1 版本号来源
Docker 构建时:
Release Tag (v1.3.0)
→ Dockerfile ARG VERSION
→ /app/version.json {"version": "1.3.0"}
运行时检查更新:
1. 读取: /app/version.json → "1.3.0"
2. 请求: GitHub API /releases/latest → "v1.4.0"
3. 比较: "1.4.0" > "1.3.0" → True
7.2 前端获取当前版本
完整链路:
前端调用 GET /api/update/version
↓
Nginx 代理到 http://localhost:9090/version
↓
Python 读取 /app/version.json
↓
返回 { code: 0, data: { version: "1.3.0", ... } }
前端代码示例:
// 获取当前版本
const getCurrentVersion = async () => {
try {
const response = await axios.get('/api/update/version');
if (response.data.code === 0) {
const { version, tag, buildTime } = response.data.data;
return {
version, // "1.3.0"
tag, // "v1.3.0"
buildTime // "2026-01-20T15:30:00Z"
};
}
} catch (error) {
console.error('获取版本失败:', error);
return { version: 'unknown', tag: 'unknown', buildTime: '' };
}
};
API 响应格式:
{
"code": 0,
"data": {
"version": "1.3.0",
"tag": "v1.3.0",
"buildTime": "2026-01-20T15:30:00Z"
},
"message": "success"
}
在组件中使用:
// SystemUpdate.tsx
const SystemUpdate: React.FC = () => {
const [currentVersion, setCurrentVersion] = useState('加载中...');
useEffect(() => {
// 页面加载时获取当前版本
const fetchVersion = async () => {
const response = await axios.get('/api/update/version');
if (response.data.code === 0) {
setCurrentVersion(response.data.data.version);
}
};
fetchVersion();
}, []);
return (
<div>
<p>当前版本: {currentVersion}</p>
</div>
);
};
7.3 获取远程版本
# 更新服务请求 GitHub API
url = 'https://api.github.com/repos/WrBug/PolyHermes/releases/latest'
response = requests.get(url)
data = response.json()
tag = data['tag_name'] # "v1.3.0"
version = tag.lstrip('v') # "1.3.0"
assets = data['assets'] # 包含更新包
7.4 获取编译产物
# 从 Release Assets 中查找更新包
for asset in data['assets']:
if asset['name'].endswith('-update.tar.gz'):
download_url = asset['browser_download_url']
# 下载: https://github.com/.../releases/download/v1.3.0/polyhermes-v1.3.0-update.tar.gz
break
7.5 版本号更新流程
更新前:
/app/version.json: {"version": "1.2.0"}
前端获取: "1.2.0"
执行更新:
1. 下载 polyhermes-v1.3.0-update.tar.gz
2. 解压得到新的 version.json: {"version": "1.3.0"}
3. 替换 /app/version.json
更新后:
/app/version.json: {"version": "1.3.0"}
前端获取: "1.3.0"
8. 使用流程
8.1 发布新版本
# 1. 创建并推送 tag
git tag v1.3.0
git push origin v1.3.0
# 2. 在 GitHub 创建 Release
# - Tag: v1.3.0
# - Title: Release v1.3.0
# - Description: ## 新功能 ...
# 3. 自动触发 GitHub Actions
# - 构建后端 + 前端
# - 打包更新包
# - 上传到 Release Assets
# - 构建 Docker 镜像
8.2 用户更新
1. 登录系统
2. 系统设置 → 系统更新
3. 点击"检查更新"
4. 点击"立即升级"
5. 等待 30-60 秒
6. 页面自动刷新
9. 关键要点
9.1 文件命名规范
更新包文件名必须遵循:
polyhermes-{tag}-update.tar.gz
正确: polyhermes-v1.3.0-update.tar.gz
错误: update-v1.3.0.tar.gz
9.2 GitHub Actions 冲突避免
❌ 错误做法:创建新的 release-build.yml 文件
✅ 正确做法:修改现有的 docker-build.yml,在构建 Docker 镜像之前增加步骤
9.3 版本号格式
统一使用:vX.Y.Z 格式
- Tag:
v1.3.0 - version.json:
"version": "1.3.0"(去掉 v) - 文件名:
polyhermes-v1.3.0-update.tar.gz(保留 v)
10. 常见问题
Q1: 为什么不创建新的 workflow 文件?
A: 现有的 docker-build.yml 已经监听 release.published 事件。如果创建新文件也监听同一事件,会导致两个 workflow 同时运行,造成资源浪费和潜在冲突。
Q2: Docker 镜像和更新包的关系?
A:
- Docker 镜像:包含完整应用,用于全新部署
- 更新包:仅包含 JAR + 前端文件,用于在线更新
两者独立但同时生成,给用户不同的部署选择。
Q3: 更新失败会怎样?
A: 更新服务会:
- 自动备份当前版本
- 执行更新
- 健康检查(30秒)
- 失败则自动回滚到备份版本
Q4: 如何测试更新功能而不影响生产环境?
A: 使用 Pre-release 机制:
创建测试版本:
git tag v1.3.0-beta
git push origin v1.3.0-beta
# GitHub 创建 Release,勾选 "This is a pre-release"
测试环境配置:
docker run -e ALLOW_PRERELEASE=true wrbug/polyhermes:v1.3.0-beta
生产环境配置(默认):
docker run wrbug/polyhermes:latest # 不启用 pre-release
特性:
- ✅ Pre-release 不会触发 Telegram 通知
- ✅ Pre-release 不会推送到
latest标签 - ✅ 测试环境启用
ALLOW_PRERELEASE=true可检测 pre-release 版本 - ✅ 生产环境默认只检测正式版本
11. Pre-release 测试策略
11.1 工作流程
开发完成
↓
创建 Pre-release (v1.3.0-beta)
↓
GitHub Actions 构建(不发 TG)
↓
上传更新包到 Release Assets
↓
测试环境拉取并测试
↓
测试通过
↓
创建正式 Release (v1.3.0)
↓
GitHub Actions 构建(发 TG)
↓
生产环境更新
11.2 GitHub Actions 调整
检测 Pre-release:
在 .github/workflows/docker-build.yml 中增加检测:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Extract version and check if pre-release
id: version
run: |
TAG="${{ github.event.release.tag_name }}"
VERSION=${TAG#v}
IS_PRERELEASE="${{ github.event.release.prerelease }}"
echo "TAG=$TAG" >> $GITHUB_OUTPUT
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "IS_PRERELEASE=$IS_PRERELEASE" >> $GITHUB_OUTPUT
if [ "$IS_PRERELEASE" = "true" ]; then
echo "📋 这是 Pre-release: $TAG"
else
echo "📦 这是正式版本: $TAG"
fi
# ... 其他构建步骤
# Docker 推送(Pre-release 不推送到 latest)
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
wrbug/polyhermes:${{ steps.version.outputs.TAG }}
${{ steps.version.outputs.IS_PRERELEASE == 'false' && 'wrbug/polyhermes:latest' || '' }}
build-args: |
BUILD_IN_DOCKER=false
VERSION=${{ steps.version.outputs.VERSION }}
GIT_TAG=${{ steps.version.outputs.TAG }}
# Telegram 通知(仅正式版本)
- name: Send Telegram notification
if: steps.version.outputs.IS_PRERELEASE == 'false'
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
echo "⚠️ Telegram 未配置,跳过通知"
exit 0
fi
MESSAGE="✅ <b>PolyHermes ${{ steps.version.outputs.TAG }} 发布成功</b>
📦 版本: ${{ steps.version.outputs.VERSION }}
🔗 <a href=\"${{ github.event.release.html_url }}\">查看 Release</a>
<b>已上传:</b>
- Docker 镜像: wrbug/polyhermes:${{ steps.version.outputs.TAG }}
- 更新包: polyhermes-${{ steps.version.outputs.TAG }}-update.tar.gz"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id \"$TELEGRAM_CHAT_ID\" \
--arg text \"$MESSAGE\" \
'{chat_id: $chat_id, text: $text, parse_mode: \"HTML\"}')"
关键点:
${{ github.event.release.prerelease }}- GitHub 自动提供的判断if: steps.version.outputs.IS_PRERELEASE == 'false'- 仅正式版本发 TG- Tags 推送逻辑 - Pre-release 不推送
latest
11.3 更新服务调整
在 docker/update-service.py 中增加环境变量支持:
# 是否允许检测 pre-release 版本
ALLOW_PRERELEASE = os.getenv('ALLOW_PRERELEASE', 'false').lower() == 'true'
def fetch_latest_release():
"""获取最新 Release"""
try:
if ALLOW_PRERELEASE:
# 测试模式:获取所有 Release(包括 pre-release)
url = f'https://api.github.com/repos/{GITHUB_REPO}/releases'
response = requests.get(url, headers={'Accept': 'application/vnd.github.v3+json'})
releases = response.json()
if releases and len(releases) > 0:
latest = releases[0] # 最新的(可能是 pre-release)
logger.info(f"检测到版本: {latest['tag_name']} (pre-release: {latest.get('prerelease', False)})")
return {
'tag': latest['tag_name'],
'name': latest['name'],
'body': latest['body'],
'published_at': latest['published_at'],
'assets': latest['assets'],
'prerelease': latest.get('prerelease', False)
}
else:
# 生产模式:只获取正式版本(GitHub API 的 /latest 自动排除 pre-release)
url = f'https://api.github.com/repos/{GITHUB_REPO}/releases/latest'
response = requests.get(url, headers={'Accept': 'application/vnd.github.v3+json'})
if response.status_code == 200:
data = response.json()
return {
'tag': data['tag_name'],
'name': data['name'],
'body': data['body'],
'published_at': data['published_at'],
'assets': data['assets'],
'prerelease': False
}
return None
except Exception as e:
logger.error(f"获取 Release 失败: {e}")
return None
11.4 Docker 启动配置
测试环境(docker-compose.test.yml):
version: '3.8'
services:
app:
image: wrbug/polyhermes:v1.3.0-beta
ports:
- "8080:80"
environment:
ALLOW_PRERELEASE: "true" # ← 启用 pre-release 检测
GITHUB_REPO: "WrBug/PolyHermes"
SPRING_PROFILES_ACTIVE: "test"
生产环境(保持不变):
version: '3.8'
services:
app:
image: wrbug/polyhermes:latest
# ALLOW_PRERELEASE 默认为 false,只检测正式版本
11.5 测试流程
-
创建 Pre-release
git tag v1.3.0-beta git push origin v1.3.0-beta # GitHub: Create Release → 勾选 "This is a pre-release" -
自动构建
- ✅ GitHub Actions 构建镜像
- ✅ 上传更新包
- ❌ 不发送 Telegram 通知(因为 IS_PRERELEASE=true)
- ❌ 不推送到
latest标签
-
测试环境验证
# 拉取测试镜像 docker pull wrbug/polyhermes:v1.3.0-beta # 启动测试容器 docker-compose -f docker-compose.test.yml up -d # 系统内检查更新(会检测到 v1.3.0-beta) # 点击"立即升级"测试更新流程 -
测试通过后发布正式版
git tag v1.3.0 git push origin v1.3.0 # GitHub: Create Release(不勾选 pre-release) -
正式版本发布
- ✅ GitHub Actions 构建镜像
- ✅ 上传更新包
- ✅ 发送 Telegram 通知
- ✅ 推送到
latest标签
方案版本: v1.0
最后更新: 2026-01-20