1386 lines
37 KiB
Markdown
1386 lines
37 KiB
Markdown
|
|
# 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 格式**:
|
|||
|
|
```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:增加权限声明**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
jobs:
|
|||
|
|
build-and-push:
|
|||
|
|
runs-on: ubuntu-latest
|
|||
|
|
|
|||
|
|
permissions:
|
|||
|
|
contents: write # 【新增】需要写权限以上传 Assets
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### **步骤2:构建后端 JAR(在 Docker 构建之前)**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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:构建前端**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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:打包更新包**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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:计算校验和**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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 通知
|
|||
|
|
|
|||
|
|
在最后的通知步骤中,可以增加更新包信息:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
- 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(混合方案)
|
|||
|
|
|
|||
|
|
```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` 中:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
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` 无需任何修改:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 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:直接运行(推荐)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 后端
|
|||
|
|
cd backend
|
|||
|
|
gradle bootRun
|
|||
|
|
|
|||
|
|
# 前端(新终端)
|
|||
|
|
cd frontend
|
|||
|
|
npm run dev
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### **方式2:先编译再构建(快速)**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 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 增加条件判断
|
|||
|
|
- 但这是一次性成本
|
|||
|
|
|
|||
|
|
**对比示例**:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户A(GitHub Actions 发布):
|
|||
|
|
→ BUILD_IN_DOCKER=false
|
|||
|
|
→ 8 分钟完成
|
|||
|
|
|
|||
|
|
用户B(本地部署测试):
|
|||
|
|
→ ./deploy.sh
|
|||
|
|
→ BUILD_IN_DOCKER=true(自动)
|
|||
|
|
→ 12 分钟完成(首次),5分钟(有缓存)
|
|||
|
|
→ 无需任何额外操作
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.8 启动脚本
|
|||
|
|
|
|||
|
|
`docker/start.sh`:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
#!/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)
|
|||
|
|
|
|||
|
|
核心功能:
|
|||
|
|
```python
|
|||
|
|
@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`:
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
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` 中增加权限验证:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 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 响应格式**:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 成功响应
|
|||
|
|
{
|
|||
|
|
"code": 0,
|
|||
|
|
"data": {...},
|
|||
|
|
"message": "success"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 错误响应
|
|||
|
|
{
|
|||
|
|
"code": 403, # 或 500, 409 等
|
|||
|
|
"data": null,
|
|||
|
|
"message": "错误信息"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 前端调用方式
|
|||
|
|
|
|||
|
|
前端直接调用 `/api/update/*`,Nginx 会自动代理到 Python 服务:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 前端 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**:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 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", ... } }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**前端代码示例**:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 获取当前版本
|
|||
|
|
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 响应格式**:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 0,
|
|||
|
|
"data": {
|
|||
|
|
"version": "1.3.0",
|
|||
|
|
"tag": "v1.3.0",
|
|||
|
|
"buildTime": "2026-01-20T15:30:00Z"
|
|||
|
|
},
|
|||
|
|
"message": "success"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**在组件中使用**:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 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 获取远程版本
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 更新服务请求 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 获取编译产物
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 从 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 发布新版本
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 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** 机制:
|
|||
|
|
|
|||
|
|
**创建测试版本**:
|
|||
|
|
```bash
|
|||
|
|
git tag v1.3.0-beta
|
|||
|
|
git push origin v1.3.0-beta
|
|||
|
|
# GitHub 创建 Release,勾选 "This is a pre-release"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**测试环境配置**:
|
|||
|
|
```bash
|
|||
|
|
docker run -e ALLOW_PRERELEASE=true wrbug/polyhermes:v1.3.0-beta
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**生产环境配置**(默认):
|
|||
|
|
```bash
|
|||
|
|
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` 中增加检测:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
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` 中增加环境变量支持:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 是否允许检测 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`):
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
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"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**生产环境**(保持不变):
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
version: '3.8'
|
|||
|
|
|
|||
|
|
services:
|
|||
|
|
app:
|
|||
|
|
image: wrbug/polyhermes:latest
|
|||
|
|
# ALLOW_PRERELEASE 默认为 false,只检测正式版本
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 11.5 测试流程
|
|||
|
|
|
|||
|
|
1. **创建 Pre-release**
|
|||
|
|
```bash
|
|||
|
|
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. **测试环境验证**
|
|||
|
|
```bash
|
|||
|
|
# 拉取测试镜像
|
|||
|
|
docker pull wrbug/polyhermes:v1.3.0-beta
|
|||
|
|
|
|||
|
|
# 启动测试容器
|
|||
|
|
docker-compose -f docker-compose.test.yml up -d
|
|||
|
|
|
|||
|
|
# 系统内检查更新(会检测到 v1.3.0-beta)
|
|||
|
|
# 点击"立即升级"测试更新流程
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **测试通过后发布正式版**
|
|||
|
|
```bash
|
|||
|
|
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
|