Files
PolyHermes/docs/zh/DYNAMIC_UPDATE.md
WrBug 26dd3bb387 feat: 实现系统动态更新功能和 Release 创建脚本
主要变更:

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 脚本使用说明
2026-01-21 03:34:16 +08:00

37 KiB
Raw Permalink Blame History

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"]

关键设计

  1. ARG BUILD_IN_DOCKER=true - 默认在 Docker 内编译(本地开发友好)
  2. 条件判断 if [ "$BUILD_IN_DOCKER" = "true" ] - 根据参数决定是否编译
  3. 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分钟 兼容

混合方案的价值

  1. GitHub Actions 性能最优

    • 只编译1次(8分钟)
    • 与简化版相同
  2. 本地开发零影响

    • ./deploy.sh 保持不变
    • 不需要指导用户改变习惯
  3. Docker 镜像自包含

    • 可以独立构建
    • 不依赖外部产物
  4. ⚠️ 唯一代价

    • Dockerfile 增加条件判断
    • 但这是一次性成本

对比示例

用户AGitHub 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: 更新服务会:

  1. 自动备份当前版本
  2. 执行更新
  3. 健康检查(30秒)
  4. 失败则自动回滚到备份版本

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\"}')"

关键点

  1. ${{ github.event.release.prerelease }} - GitHub 自动提供的判断
  2. if: steps.version.outputs.IS_PRERELEASE == 'false' - 仅正式版本发 TG
  3. 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 测试流程

  1. 创建 Pre-release

    git tag v1.3.0-beta
    git push origin v1.3.0-beta
    # GitHub: Create Release → 勾选 "This is a pre-release"
    
  2. 自动构建

    • GitHub Actions 构建镜像
    • 上传更新包
    • 不发送 Telegram 通知(因为 IS_PRERELEASE=true
    • 不推送到 latest 标签
  3. 测试环境验证

    # 拉取测试镜像
    docker pull wrbug/polyhermes:v1.3.0-beta
    
    # 启动测试容器
    docker-compose -f docker-compose.test.yml up -d
    
    # 系统内检查更新(会检测到 v1.3.0-beta
    # 点击"立即升级"测试更新流程
    
  4. 测试通过后发布正式版

    git tag v1.3.0
    git push origin v1.3.0
    # GitHub: Create Release(不勾选 pre-release
    
  5. 正式版本发布

    • GitHub Actions 构建镜像
    • 上传更新包
    • 发送 Telegram 通知
    • 推送到 latest 标签

方案版本: v1.0
最后更新: 2026-01-20