chore(release): v1.0.9 移除 Webhook 自动部署功能
- 删除 webhook/ 目录及相关文件 - 删除 deploy.sh 部署脚本 - 删除 WEBHOOK_SETUP.md 配置文档 - 更新 docker-compose.yaml 移除 webhook 服务 - 更新 README.md 和 CHANGELOG.md
This commit is contained in:
@@ -5,6 +5,15 @@
|
|||||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||||
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
|
## [1.0.9] - 2026-01-19
|
||||||
|
|
||||||
|
### 移除
|
||||||
|
- **移除 Webhook 自动部署功能** - 删除 webhook 服务及相关文件
|
||||||
|
- 删除 `webhook/` 目录(Dockerfile、main.go、go.mod、README.md)
|
||||||
|
- 删除 `deploy.sh` 部署脚本
|
||||||
|
- 删除 `WEBHOOK_SETUP.md` 配置文档
|
||||||
|
- 移除 docker-compose.yaml 中的 webhook 服务配置
|
||||||
|
|
||||||
## [1.0.8] - 2026-01-18
|
## [1.0.8] - 2026-01-18
|
||||||
|
|
||||||
### 重构
|
### 重构
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
一个基于微服务架构的个人账单分析工具,支持微信和支付宝账单的自动解析、智能分类和可视化分析。
|
一个基于微服务架构的个人账单分析工具,支持微信和支付宝账单的自动解析、智能分类和可视化分析。
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -20,7 +20,7 @@
|
|||||||
- 🔍 **复核修正** - 对不确定的分类进行人工复核
|
- 🔍 **复核修正** - 对不确定的分类进行人工复核
|
||||||
- 💾 **数据持久化** - MongoDB 存储原始数据和清洗后数据,支持去重检查
|
- 💾 **数据持久化** - MongoDB 存储原始数据和清洗后数据,支持去重检查
|
||||||
- 🐳 **一键部署** - Docker Compose 快速启动全部服务
|
- 🐳 **一键部署** - Docker Compose 快速启动全部服务
|
||||||
- 🚀 **自动部署** - Gitea Webhook 触发零停机热更新
|
- <EFBFBD> **用户认证** - JWT Token 登录认证
|
||||||
|
|
||||||
## 🏗️ 系统架构
|
## 🏗️ 系统架构
|
||||||
|
|
||||||
@@ -129,13 +129,8 @@ BillAI/
|
|||||||
│ │ └── wechat.py # 微信
|
│ │ └── wechat.py # 微信
|
||||||
│ └── Dockerfile
|
│ └── Dockerfile
|
||||||
│
|
│
|
||||||
├── webhook/ # Webhook 服务 (Go)
|
├── mock_data/ # 测试数据目录
|
||||||
│ ├── main.go # Webhook 入口
|
├── mongodata/ # MongoDB 数据持久化
|
||||||
│ └── Dockerfile
|
|
||||||
│
|
|
||||||
├── deploy.sh # 自动部署脚本
|
|
||||||
├── data/ # 测试数据目录
|
|
||||||
├── mongo/ # MongoDB 数据
|
|
||||||
└── docker-compose.yaml # 容器编排
|
└── docker-compose.yaml # 容器编排
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -275,10 +270,12 @@ python server.py
|
|||||||
|
|
||||||
| 版本 | 日期 | 主要更新 |
|
| 版本 | 日期 | 主要更新 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| **v1.0.7** | 2026-01-16 | 🔐 Token 过期由后端统一判断、401 自动退登;后端数据访问层收敛 |
|
| **v1.0.9** | 2026-01-19 | 🗑️ 移除 Webhook 自动部署功能 |
|
||||||
|
| **v1.0.8** | 2026-01-18 | ♻️ 前端账单模型统一为 UIBill、新增账单详情弹窗组件 |
|
||||||
|
| **v1.0.7** | 2026-01-16 | 🔐 Token 过期由后端统一判断、401 自动退登 |
|
||||||
| **v1.0.6** | 2026-01-08 | 🐛 修复数据分析页面总支出和大盘数据错误 |
|
| **v1.0.6** | 2026-01-08 | 🐛 修复数据分析页面总支出和大盘数据错误 |
|
||||||
| **v1.0.5** | 2026-01-08 | 🐛 修复支付宝时间格式解析错误,修复WebHook编译错误 |
|
| **v1.0.5** | 2026-01-08 | 🐛 修复支付宝时间格式解析错误 |
|
||||||
| **v1.0.4** | 2026-01-13 | 🚀 Gitea Webhook 自动部署、零停机热更新 |
|
| **v1.0.4** | 2026-01-13 | ✨ MongoDB 数据持久化、上传去重 |
|
||||||
| **v1.0.3** | 2026-01-13 | ✨ DateTimePicker 组件、收支分类动态切换 |
|
| **v1.0.3** | 2026-01-13 | ✨ DateTimePicker 组件、收支分类动态切换 |
|
||||||
| **v1.0.2** | 2026-01-11 | 🐛 修复时区和金额解析问题 |
|
| **v1.0.2** | 2026-01-11 | 🐛 修复时区和金额解析问题 |
|
||||||
| **v1.0.1** | 2026-01-11 | 🐛 修复复核页面显示错误 |
|
| **v1.0.1** | 2026-01-11 | 🐛 修复复核页面显示错误 |
|
||||||
|
|||||||
490
WEBHOOK_SETUP.md
490
WEBHOOK_SETUP.md
@@ -1,490 +0,0 @@
|
|||||||
# 自动部署配置指南
|
|
||||||
|
|
||||||
这个指南将帮助你配置 Gitea webhook 以实现代码推送时的自动部署。
|
|
||||||
|
|
||||||
## 整体架构
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[🏠 Gitea<br/>代码仓库] -->|Push Event| B[🔗 Webhook Service<br/>Port 9000]
|
|
||||||
B -->|验证签名| C{签名有效?}
|
|
||||||
C -->|❌ 无效| D[拒绝请求]
|
|
||||||
C -->|✅ 有效| E{主分支?}
|
|
||||||
E -->|❌ 否| F[跳过部署]
|
|
||||||
E -->|✅ 是| G[📜 Deploy Script<br/>deploy.sh]
|
|
||||||
G --> H[📥 git pull<br/>拉取最新代码]
|
|
||||||
H --> I[🔨 热更新部署<br/>零停机]
|
|
||||||
I --> J[🧹 清理旧镜像]
|
|
||||||
J --> K[🏥 健康检查]
|
|
||||||
K --> L[✅ 部署完成]
|
|
||||||
|
|
||||||
style A fill:#f9f,stroke:#333
|
|
||||||
style B fill:#bbf,stroke:#333
|
|
||||||
style G fill:#bfb,stroke:#333
|
|
||||||
style L fill:#9f9,stroke:#333
|
|
||||||
```
|
|
||||||
|
|
||||||
## 第一步:生成 Webhook Secret
|
|
||||||
|
|
||||||
打开终端,运行以下命令生成安全的随机密钥:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux / macOS
|
|
||||||
openssl rand -hex 32
|
|
||||||
|
|
||||||
# Windows PowerShell
|
|
||||||
[System.BitConverter]::ToString([System.Security.Cryptography.RandomNumberGenerator]::GetBytes(32)).Replace('-','').ToLower()
|
|
||||||
```
|
|
||||||
|
|
||||||
示例输出:
|
|
||||||
```
|
|
||||||
a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
|
||||||
```
|
|
||||||
|
|
||||||
## 第二步:配置 docker-compose.yaml
|
|
||||||
|
|
||||||
编辑 `docker-compose.yaml` 文件中的 webhook 服务配置:
|
|
||||||
|
|
||||||
### 设置 Secret
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
webhook:
|
|
||||||
environment:
|
|
||||||
WEBHOOK_SECRET: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" # 替换为你生成的 Secret
|
|
||||||
```
|
|
||||||
|
|
||||||
### 配置仓库路径(关键)
|
|
||||||
|
|
||||||
**重要!** 根据你的部署方式修改挂载路径:
|
|
||||||
|
|
||||||
#### 如果使用本地路径部署:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
webhook:
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- /home/user/billai:/app # 替换为你的实际路径
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 如果在云服务器上部署:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
webhook:
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
- /opt/billai:/app # 替换为你的实际路径
|
|
||||||
```
|
|
||||||
|
|
||||||
### 网络配置说明
|
|
||||||
|
|
||||||
**webhook 服务对外暴露 9000 端口**,用于接收 Gitea webhook 事件。
|
|
||||||
|
|
||||||
这样的好处是:
|
|
||||||
- ✅ 支持外部 Gitea 服务器的 webhook 回调
|
|
||||||
- ✅ 灵活的部署架构
|
|
||||||
- ✅ 支持多种网络拓扑
|
|
||||||
|
|
||||||
**Webhook 访问地址:**
|
|
||||||
|
|
||||||
- 本地访问:`http://localhost:9000/webhook`
|
|
||||||
- 内部网络:`http://<internal-ip>:9000/webhook`
|
|
||||||
- 公网访问:`http://<public-ip>:9000/webhook`
|
|
||||||
- 通过域名:`http://webhook.yourdomain.com:9000/webhook`
|
|
||||||
|
|
||||||
## 第三步:启动 Webhook 服务
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 进入项目根目录
|
|
||||||
cd /path/to/billai
|
|
||||||
|
|
||||||
# 构建并启动 webhook 服务
|
|
||||||
docker-compose up -d webhook
|
|
||||||
|
|
||||||
# 验证服务是否启动
|
|
||||||
docker-compose logs webhook
|
|
||||||
|
|
||||||
# 测试健康检查
|
|
||||||
curl http://localhost:9000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
输出应该显示:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"time": "2026-01-13T10:30:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 第四步:配置 Gitea Webhook
|
|
||||||
|
|
||||||
### 4.1 进入 Gitea 仓库设置
|
|
||||||
|
|
||||||
1. 打开 Gitea 管理界面
|
|
||||||
2. 找到你的 BillAI 仓库
|
|
||||||
3. 点击 **设置 (Settings)** → **Webhooks**
|
|
||||||
|
|
||||||
### 4.2 添加新 Webhook
|
|
||||||
|
|
||||||
点击 "添加 Webhook (Add Webhook)" 按钮,选择 "Gitea"
|
|
||||||
|
|
||||||
### 4.3 填写 Webhook 配置
|
|
||||||
|
|
||||||
| 字段 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| **目标 URL** | `http://<your-server-ip>:9000/webhook` |
|
|
||||||
| **内容类型** | `application/json` |
|
|
||||||
| **密钥** | `a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6` (你生成的 Secret) |
|
|
||||||
| **事件** | ✓ 推送事件 (Push Events) |
|
|
||||||
| **活跃** | ✓ 是 |
|
|
||||||
|
|
||||||
### 4.4 配置说明
|
|
||||||
|
|
||||||
#### 目标 URL
|
|
||||||
|
|
||||||
根据 Gitea 部署方式选择合适的 URL:
|
|
||||||
|
|
||||||
**情况 1:Gitea 和 webhook 都在同一台机器**
|
|
||||||
|
|
||||||
```
|
|
||||||
http://localhost:9000/webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
**情况 2:Gitea 在不同机器,webhook 在 Docker**
|
|
||||||
|
|
||||||
使用 webhook 所在服务器的 IP 地址:
|
|
||||||
```
|
|
||||||
http://<webhook-server-ip>:9000/webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
**情况 3:使用域名访问**
|
|
||||||
|
|
||||||
```
|
|
||||||
http://webhook.yourdomain.com:9000/webhook
|
|
||||||
或
|
|
||||||
https://webhook.yourdomain.com/webhook (需要配置反向代理处理 HTTPS)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 防火墙配置
|
|
||||||
|
|
||||||
确保防火墙允许 9000 端口的入站连接:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Linux (ufw)
|
|
||||||
sudo ufw allow 9000/tcp
|
|
||||||
|
|
||||||
# Linux (iptables)
|
|
||||||
sudo iptables -A INPUT -p tcp --dport 9000 -j ACCEPT
|
|
||||||
|
|
||||||
# Windows Firewall (PowerShell)
|
|
||||||
New-NetFirewallRule -DisplayName "Allow Webhook" -Direction Inbound -LocalPort 9000 -Protocol TCP -Action Allow
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.5 测试 Webhook
|
|
||||||
|
|
||||||
在 Gitea webhook 设置页面,找到刚添加的 webhook,点击 "测试 (Test)" 按钮。
|
|
||||||
|
|
||||||
预期响应:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "非主分支,跳过部署"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
(这是因为测试会使用一个默认的测试分支)
|
|
||||||
|
|
||||||
## 第五步:网络配置
|
|
||||||
|
|
||||||
### 防火墙配置
|
|
||||||
|
|
||||||
在部署前,确保服务器防火墙允许以下端口:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 允许 9000 端口(webhook)
|
|
||||||
sudo ufw allow 9000/tcp
|
|
||||||
|
|
||||||
# 允许 3000 端口(前端web)- 已配置
|
|
||||||
sudo ufw allow 3000/tcp
|
|
||||||
|
|
||||||
# 允许 80 端口(HTTP,如果使用)
|
|
||||||
sudo ufw allow 80/tcp
|
|
||||||
|
|
||||||
# 允许 443 端口(HTTPS,如果使用)
|
|
||||||
sudo ufw allow 443/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
### 可选:Nginx 反向代理配置
|
|
||||||
|
|
||||||
如果需要通过 HTTPS 访问 webhook,可以配置 Nginx 反向代理:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name webhook.yourdomain.com;
|
|
||||||
|
|
||||||
ssl_certificate /path/to/cert.pem;
|
|
||||||
ssl_certificate_key /path/to/key.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:9000;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP 重定向到 HTTPS
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name webhook.yourdomain.com;
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 第六步:测试自动部署
|
|
||||||
|
|
||||||
现在你可以测试自动部署功能:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 进入仓库目录
|
|
||||||
cd /path/to/billai
|
|
||||||
|
|
||||||
# 做一些改动
|
|
||||||
echo "# 测试自动部署" >> README.md
|
|
||||||
|
|
||||||
# 提交并推送到主分支
|
|
||||||
git add .
|
|
||||||
git commit -m "test: 测试自动部署功能"
|
|
||||||
git push origin master
|
|
||||||
```
|
|
||||||
|
|
||||||
### 监控部署过程
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看 webhook 服务日志
|
|
||||||
docker-compose logs -f webhook
|
|
||||||
|
|
||||||
# 查看部署脚本日志
|
|
||||||
docker exec billai-webhook tail -f /tmp/billai_deploy.log
|
|
||||||
|
|
||||||
# 查看其他服务日志
|
|
||||||
docker-compose logs -f server
|
|
||||||
docker-compose logs -f web
|
|
||||||
docker-compose logs -f analyzer
|
|
||||||
```
|
|
||||||
|
|
||||||
## 常见问题排查
|
|
||||||
|
|
||||||
### 问题 1:Webhook 签名验证失败
|
|
||||||
|
|
||||||
**症状**:日志中出现 "无效的签名"
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 确认 Gitea 中设置的 Secret 与 docker-compose.yaml 中的 WEBHOOK_SECRET 完全相同
|
|
||||||
- 检查是否有多余空格或特殊字符
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在 docker-compose.yaml 中查看
|
|
||||||
grep WEBHOOK_SECRET docker-compose.yaml
|
|
||||||
|
|
||||||
# 在 Gitea 中重新复制 Secret,确保没有多余空格
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题 2:无法连接到 Webhook 服务
|
|
||||||
|
|
||||||
**症状**:Gitea webhook 测试失败
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
1. 检查 webhook 容器是否运行:
|
|
||||||
```bash
|
|
||||||
docker-compose ps webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 检查端口是否正确暴露:
|
|
||||||
```bash
|
|
||||||
docker-compose logs webhook | grep "listen"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 检查防火墙规则:
|
|
||||||
```bash
|
|
||||||
# Linux
|
|
||||||
netstat -tulpn | grep 9000
|
|
||||||
sudo ufw status
|
|
||||||
|
|
||||||
# Windows
|
|
||||||
netstat -ano | findstr 9000
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 测试本地连接:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:9000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
5. 如果是远程连接,测试网络连通性:
|
|
||||||
```bash
|
|
||||||
# 从 Gitea 服务器测试
|
|
||||||
curl http://<webhook-server-ip>:9000/health
|
|
||||||
|
|
||||||
netstat -ano | findstr 9000 # Windows
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 测试本地连接:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://localhost:9000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题 3:部署脚本无法执行
|
|
||||||
|
|
||||||
**症状**:日志中显示 "部署脚本执行失败"
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
1. 检查 deploy.sh 是否存在:
|
|
||||||
```bash
|
|
||||||
ls -la deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 检查文件权限:
|
|
||||||
```bash
|
|
||||||
chmod +x deploy.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 检查容器内的环境:
|
|
||||||
```bash
|
|
||||||
docker exec billai-webhook bash -c "ls -la /app/deploy.sh"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 查看详细错误信息:
|
|
||||||
```bash
|
|
||||||
docker exec billai-webhook tail -f /tmp/billai_deploy.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题 4:Docker 操作权限不足
|
|
||||||
|
|
||||||
**症状**:日志中显示 "permission denied" 或 "Cannot connect to Docker daemon"
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
1. 检查 Docker Socket 是否正确挂载:
|
|
||||||
```yaml
|
|
||||||
webhook:
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 检查 Docker Socket 权限:
|
|
||||||
```bash
|
|
||||||
ls -l /var/run/docker.sock
|
|
||||||
chmod 666 /var/run/docker.sock # 可能需要 sudo
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 确保 webhook 容器以正确的用户运行(通常是 root)
|
|
||||||
|
|
||||||
### 问题 5:部署后服务无法启动
|
|
||||||
|
|
||||||
**症状**:健康检查超时,部署失败
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
1. 检查 docker-compose build 是否成功:
|
|
||||||
```bash
|
|
||||||
docker-compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 检查依赖服务是否启动:
|
|
||||||
```bash
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 查看各服务的详细日志:
|
|
||||||
```bash
|
|
||||||
docker-compose logs server
|
|
||||||
docker-compose logs web
|
|
||||||
docker-compose logs analyzer
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 增加健康检查的超时时间(修改 deploy.sh 中的 sleep 时间)
|
|
||||||
|
|
||||||
## 监控和维护
|
|
||||||
|
|
||||||
### 定期检查 Webhook 状态
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看最近 50 条日志
|
|
||||||
docker-compose logs --tail=50 webhook
|
|
||||||
|
|
||||||
# 实时监控日志
|
|
||||||
docker-compose logs -f webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看部署历史
|
|
||||||
|
|
||||||
部署脚本的日志保存在:`/tmp/billai_deploy.log`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看部署历史
|
|
||||||
docker exec billai-webhook cat /tmp/billai_deploy.log
|
|
||||||
|
|
||||||
# 持续监控
|
|
||||||
docker exec billai-webhook tail -f /tmp/billai_deploy.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### 禁用/启用自动部署
|
|
||||||
|
|
||||||
如果需要临时禁用自动部署,修改 deploy.sh:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 临时禁用部署,取消下面的注释
|
|
||||||
# exit 0
|
|
||||||
|
|
||||||
# ... 后续脚本内容
|
|
||||||
```
|
|
||||||
|
|
||||||
然后重新启动 webhook 服务:
|
|
||||||
```bash
|
|
||||||
docker-compose restart webhook
|
|
||||||
```
|
|
||||||
|
|
||||||
## 高级配置
|
|
||||||
|
|
||||||
### 只部署特定分支
|
|
||||||
|
|
||||||
编辑 webhook/main.go,修改分支检查逻辑:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// 检查分支,修改条件为只部署 master 和 develop 分支
|
|
||||||
if !strings.Contains(payload.Ref, "master") &&
|
|
||||||
!strings.Contains(payload.Ref, "develop") {
|
|
||||||
// 跳过部署
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 添加 Slack 通知
|
|
||||||
|
|
||||||
修改 deploy.sh,在部署完成后发送 Slack 消息:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 在部署完成后添加
|
|
||||||
curl -X POST -H 'Content-type: application/json' \
|
|
||||||
--data "{\"text\":\"BillAI 部署完成!\"}" \
|
|
||||||
YOUR_SLACK_WEBHOOK_URL
|
|
||||||
```
|
|
||||||
|
|
||||||
### 限制部署频率
|
|
||||||
|
|
||||||
在 webhook/main.go 中添加速率限制,防止频繁部署。
|
|
||||||
|
|
||||||
## 安全建议
|
|
||||||
|
|
||||||
1. **使用强密钥**:确保 WEBHOOK_SECRET 是随机生成的强密钥
|
|
||||||
2. **限制网络访问**:只允许 Gitea 服务器的 IP 访问 webhook
|
|
||||||
3. **HTTPS**:在生产环境中使用 HTTPS 而不是 HTTP
|
|
||||||
4. **日志监控**:定期检查部署日志,及时发现问题
|
|
||||||
5. **备份**:在部署前备份重要数据
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
|
|
||||||
- [webhook/README.md](../webhook/README.md) - Webhook 服务文档
|
|
||||||
- [deploy.sh](../deploy.sh) - 部署脚本
|
|
||||||
- [docker-compose.yaml](../docker-compose.yaml) - Docker Compose 配置
|
|
||||||
- [webhook/main.go](../webhook/main.go) - Webhook 服务源代码
|
|
||||||
146
deploy.sh
146
deploy.sh
@@ -1,146 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# BillAI 自动部署脚本
|
|
||||||
# 此脚本由 Gitea webhook 触发,自动执行 git pull 并重新构建部署
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
REPO_ROOT="/app"
|
|
||||||
LOG_FILE="/tmp/billai_deploy.log"
|
|
||||||
|
|
||||||
# 可由 webhook 传入:GIT_REF=refs/heads/main 或 refs/heads/master
|
|
||||||
GIT_REF="${GIT_REF:-}"
|
|
||||||
GIT_COMMIT="${GIT_COMMIT:-}"
|
|
||||||
|
|
||||||
# 颜色输出
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
BLUE='\033[0;34m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
log() {
|
|
||||||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
error() {
|
|
||||||
echo -e "${RED}[错误]${NC} $1" | tee -a "$LOG_FILE"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
success() {
|
|
||||||
echo -e "${GREEN}[成功]${NC} $1" | tee -a "$LOG_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
log "=========================================="
|
|
||||||
log "🚀 BillAI 自动部署开始"
|
|
||||||
log "=========================================="
|
|
||||||
|
|
||||||
# 进入仓库目录
|
|
||||||
cd "$REPO_ROOT" || error "无法进入仓库目录: $REPO_ROOT"
|
|
||||||
log "📁 工作目录: $(pwd)"
|
|
||||||
|
|
||||||
if [ -n "$GIT_REF" ]; then
|
|
||||||
log "🌿 触发分支: $GIT_REF"
|
|
||||||
fi
|
|
||||||
if [ -n "$GIT_COMMIT" ]; then
|
|
||||||
log "🧾 触发提交: ${GIT_COMMIT:0:7}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 拉取最新代码
|
|
||||||
log "📥 正在拉取最新代码..."
|
|
||||||
if ! git fetch origin; then
|
|
||||||
error "git fetch 失败"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 选择部署分支(优先使用 webhook 传入的 ref)
|
|
||||||
DEPLOY_BRANCH=""
|
|
||||||
if [ "$GIT_REF" = "refs/heads/main" ]; then
|
|
||||||
DEPLOY_BRANCH="main"
|
|
||||||
elif [ "$GIT_REF" = "refs/heads/master" ]; then
|
|
||||||
DEPLOY_BRANCH="master"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 兜底:按远端分支存在性选择(兼容仓库从 master 切到 main)
|
|
||||||
if [ -z "$DEPLOY_BRANCH" ]; then
|
|
||||||
if git show-ref --verify --quiet refs/remotes/origin/main; then
|
|
||||||
DEPLOY_BRANCH="main"
|
|
||||||
else
|
|
||||||
DEPLOY_BRANCH="master"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "🌿 部署分支: $DEPLOY_BRANCH"
|
|
||||||
|
|
||||||
if ! git reset --hard "origin/$DEPLOY_BRANCH"; then
|
|
||||||
error "git reset 失败"
|
|
||||||
fi
|
|
||||||
|
|
||||||
success "代码已更新"
|
|
||||||
|
|
||||||
# 显示当前提交信息
|
|
||||||
log "📝 当前提交:"
|
|
||||||
git log --oneline -1
|
|
||||||
|
|
||||||
# 检查 docker-compose 是否存在
|
|
||||||
if [ ! -f "docker-compose.yaml" ]; then
|
|
||||||
error "docker-compose.yaml 不存在"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "🐳 开始热更新部署(不停机)..."
|
|
||||||
|
|
||||||
# 定义需要重新部署的服务(排除 webhook 自身,否则会自杀)
|
|
||||||
SERVICES="web server analyzer"
|
|
||||||
|
|
||||||
# 拉取基础镜像更新
|
|
||||||
log "📦 检查基础镜像更新..."
|
|
||||||
docker-compose pull mongodb mongo-express || true
|
|
||||||
|
|
||||||
# 热更新:重新构建并替换容器(不停止旧服务,直接替换)
|
|
||||||
log "🔨 热更新服务(构建 + 替换)..."
|
|
||||||
if ! docker-compose up -d --build --force-recreate --no-deps $SERVICES; then
|
|
||||||
error "Docker 热更新失败"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 清理旧的未使用镜像
|
|
||||||
log "🧹 清理旧镜像..."
|
|
||||||
docker image prune -f || true
|
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
log "⏳ 等待服务启动..."
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
# 检查服务健康状态
|
|
||||||
log "🏥 检查服务健康状态..."
|
|
||||||
for i in {1..30}; do
|
|
||||||
if curl -f http://localhost:8080/health > /dev/null 2>&1; then
|
|
||||||
success "后端服务已启动"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $i -eq 30 ]; then
|
|
||||||
error "后端服务启动超时"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
for i in {1..30}; do
|
|
||||||
if curl -f http://localhost:3000 > /dev/null 2>&1; then
|
|
||||||
success "前端服务已启动"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $i -eq 30 ]; then
|
|
||||||
error "前端服务启动超时"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
log "=========================================="
|
|
||||||
success "🎉 部署完成"
|
|
||||||
log "=========================================="
|
|
||||||
log "⏰ 完成时间: $(date +'%Y-%m-%d %H:%M:%S')"
|
|
||||||
log "✅ 所有服务已启动并正常运行"
|
|
||||||
log ""
|
|
||||||
log "📍 访问地址:"
|
|
||||||
log " 前端: http://localhost:3000"
|
|
||||||
log " 后端: http://localhost:8080"
|
|
||||||
log ""
|
|
||||||
@@ -125,31 +125,3 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
mongodb:
|
mongodb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
# Gitea Webhook 服务 - 自动部署
|
|
||||||
webhook:
|
|
||||||
build:
|
|
||||||
context: ./webhook
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: billai-webhook
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
environment:
|
|
||||||
WEBHOOK_PORT: "9000"
|
|
||||||
# 重要:更改此 Secret 为强随机值
|
|
||||||
# 生成方法: openssl rand -hex 32
|
|
||||||
WEBHOOK_SECRET: "change-this-to-your-secret-key"
|
|
||||||
REPO_PATH: "/app"
|
|
||||||
volumes:
|
|
||||||
# 挂载 Docker Socket 以支持容器操作
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
# 挂载仓库目录
|
|
||||||
- ./:/app
|
|
||||||
working_dir: /app
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:9000/health"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
start_period: 5s
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.8",
|
"version": "1.0.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# Webhook 服务 Dockerfile
|
|
||||||
# 多阶段构建:编译阶段 + 运行阶段
|
|
||||||
|
|
||||||
# ===== 编译阶段 =====
|
|
||||||
FROM golang:1.21-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# 配置 Alpine 镜像源(国内)
|
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|
||||||
|
|
||||||
# 配置 Go 代理(国内镜像)
|
|
||||||
ENV GOPROXY=https://goproxy.cn,direct
|
|
||||||
|
|
||||||
# 安装编译依赖
|
|
||||||
RUN apk add --no-cache git
|
|
||||||
|
|
||||||
# 复制代码
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# 构建二进制
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o webhook .
|
|
||||||
|
|
||||||
# ===== 运行阶段 =====
|
|
||||||
FROM alpine:3.19
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 配置 Alpine 镜像源(国内)
|
|
||||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
|
||||||
|
|
||||||
# 安装必要工具
|
|
||||||
RUN apk add --no-cache bash curl git docker-cli docker-compose ca-certificates tzdata
|
|
||||||
|
|
||||||
# 设置时区
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
|
|
||||||
# 从编译阶段复制二进制
|
|
||||||
COPY --from=builder /build/webhook /usr/local/bin/webhook
|
|
||||||
|
|
||||||
# 注意:deploy.sh 通过 volume 挂载,不在构建时复制
|
|
||||||
# 因为 deploy.sh 在仓库根目录,而 Dockerfile 在 webhook 目录
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 9000
|
|
||||||
|
|
||||||
# 健康检查
|
|
||||||
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
|
|
||||||
CMD curl -f http://localhost:9000/health || exit 1
|
|
||||||
|
|
||||||
# 启动服务
|
|
||||||
CMD ["webhook"]
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Webhook 服务
|
|
||||||
|
|
||||||
用于监听 Gitea webhook 事件,自动触发服务器拉取最新代码并重新构建部署。
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
|
|
||||||
- ✅ 接收 Gitea webhook 推送事件
|
|
||||||
- ✅ HMAC-SHA256 签名验证
|
|
||||||
- ✅ 仅处理主分支 (master/main) 的推送
|
|
||||||
- ✅ 自动执行部署脚本
|
|
||||||
- ✅ 异步部署,快速响应
|
|
||||||
- ✅ 零停机热更新
|
|
||||||
|
|
||||||
## 配置
|
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
| 变量 | 说明 | 默认值 |
|
|
||||||
|-----|------|-------|
|
|
||||||
| `WEBHOOK_PORT` | webhook 服务监听端口 | 9000 |
|
|
||||||
| `WEBHOOK_SECRET` | Gitea webhook 签名密钥 | your-webhook-secret |
|
|
||||||
| `REPO_PATH` | 仓库根目录路径 | /app |
|
|
||||||
|
|
||||||
### 部署脚本
|
|
||||||
|
|
||||||
脚本位置: `/app/deploy.sh`
|
|
||||||
|
|
||||||
执行以下操作:
|
|
||||||
1. 拉取最新代码 (`git pull`)
|
|
||||||
2. 热更新部署(零停机)
|
|
||||||
3. 自动清理旧镜像
|
|
||||||
4. 健康检查
|
|
||||||
|
|
||||||
## 安全性
|
|
||||||
|
|
||||||
### 设置 Webhook Secret
|
|
||||||
|
|
||||||
在 Gitea 中配置 webhook 时,需要设置 Secret:
|
|
||||||
|
|
||||||
1. 生成一个随机密钥:
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32
|
|
||||||
# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 在 Docker 容器中设置环境变量:
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
WEBHOOK_SECRET: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gitea Webhook 配置
|
|
||||||
|
|
||||||
### 1. 进入仓库设置
|
|
||||||
|
|
||||||
在 Gitea 中打开仓库,进入 **设置 → Webhook**
|
|
||||||
|
|
||||||
### 2. 添加新 Webhook
|
|
||||||
|
|
||||||
- **URL**: `http://<your-server>:9000/webhook`
|
|
||||||
- **内容类型**: `application/json`
|
|
||||||
- **密钥**: 使用上面生成的 Secret
|
|
||||||
- **触发器**: 勾选 `推送事件`
|
|
||||||
- **活跃**: 是
|
|
||||||
|
|
||||||
### 3. 测试连接
|
|
||||||
|
|
||||||
点击 "测试" 按钮验证连接
|
|
||||||
|
|
||||||
## 日志
|
|
||||||
|
|
||||||
查看部署日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 查看容器日志
|
|
||||||
docker logs billai-webhook
|
|
||||||
|
|
||||||
# 查看部署脚本日志
|
|
||||||
docker exec billai-webhook tail -f /tmp/billai_deploy.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## 访问端点
|
|
||||||
|
|
||||||
- **Webhook**: `POST /webhook`
|
|
||||||
- **健康检查**: `GET /health`
|
|
||||||
|
|
||||||
## 工作流
|
|
||||||
|
|
||||||
```
|
|
||||||
Gitea Push Event
|
|
||||||
↓
|
|
||||||
Webhook Service (port 9000)
|
|
||||||
↓
|
|
||||||
验证 HMAC-SHA256 签名
|
|
||||||
↓
|
|
||||||
检查分支 (仅主分支)
|
|
||||||
↓
|
|
||||||
异步触发部署脚本
|
|
||||||
↓
|
|
||||||
执行 git pull
|
|
||||||
↓
|
|
||||||
执行 docker-compose build/up
|
|
||||||
↓
|
|
||||||
健康检查
|
|
||||||
↓
|
|
||||||
部署完成
|
|
||||||
```
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 签名验证失败
|
|
||||||
|
|
||||||
- 检查 Gitea 中配置的 Secret 是否与环境变量 `WEBHOOK_SECRET` 一致
|
|
||||||
|
|
||||||
### 部署脚本无法执行
|
|
||||||
|
|
||||||
- 确保 `deploy.sh` 存在于仓库根目录
|
|
||||||
- 检查文件权限:`chmod +x deploy.sh`
|
|
||||||
- 检查容器内是否有 git、docker 等工具
|
|
||||||
|
|
||||||
### Docker 操作失败
|
|
||||||
|
|
||||||
- 确保容器有权限访问 Docker Socket
|
|
||||||
- 在 docker-compose 中正确挂载 Docker Socket:
|
|
||||||
```yaml
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
```
|
|
||||||
|
|
||||||
## 示例 docker-compose 配置
|
|
||||||
|
|
||||||
参考项目根目录的 `docker-compose.yaml`
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module webhook
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
282
webhook/main.go
282
webhook/main.go
@@ -1,282 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GiteaPayload Gitea webhook 推送事件
|
|
||||||
type GiteaPayload struct {
|
|
||||||
Repository struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
CloneURL string `json:"clone_url"`
|
|
||||||
} `json:"repository"`
|
|
||||||
Pusher struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
} `json:"pusher"`
|
|
||||||
Ref string `json:"ref"`
|
|
||||||
Commit string `json:"after"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config webhook 服务配置
|
|
||||||
type Config struct {
|
|
||||||
Port string
|
|
||||||
Secret string
|
|
||||||
RepoPath string
|
|
||||||
ScriptPath string
|
|
||||||
MaxBody int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg Config
|
|
||||||
|
|
||||||
var deployMu sync.Mutex
|
|
||||||
var deployRunning bool
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultSecret = "your-webhook-secret"
|
|
||||||
maxBodyBytes = int64(1 << 20) // 1MB
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// 从环境变量读取配置
|
|
||||||
cfg.Port = os.Getenv("WEBHOOK_PORT")
|
|
||||||
if cfg.Port == "" {
|
|
||||||
cfg.Port = "9000"
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Secret = os.Getenv("WEBHOOK_SECRET")
|
|
||||||
if cfg.Secret == "" {
|
|
||||||
cfg.Secret = defaultSecret
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.RepoPath = os.Getenv("REPO_PATH")
|
|
||||||
if cfg.RepoPath == "" {
|
|
||||||
cfg.RepoPath = "/app"
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.ScriptPath = filepath.Join(cfg.RepoPath, "deploy.sh")
|
|
||||||
cfg.MaxBody = maxBodyBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
if cfg.Secret == "" || cfg.Secret == defaultSecret {
|
|
||||||
log.Fatalf("❌ WEBHOOK_SECRET 未配置或仍为默认值;请设置强随机 secret(例如 openssl rand -hex 32)")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("🚀 Webhook 服务启动 | 监听端口: %s\n", cfg.Port)
|
|
||||||
log.Printf("📁 仓库路径: %s\n", cfg.RepoPath)
|
|
||||||
log.Printf("📜 部署脚本: %s\n", cfg.ScriptPath)
|
|
||||||
log.Printf("🔒 请求体大小限制: %d bytes\n", cfg.MaxBody)
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/webhook", handleWebhook)
|
|
||||||
mux.HandleFunc("/health", handleHealth)
|
|
||||||
|
|
||||||
srv := &http.Server{
|
|
||||||
Addr: ":" + cfg.Port,
|
|
||||||
Handler: mux,
|
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
|
||||||
ReadTimeout: 10 * time.Second,
|
|
||||||
WriteTimeout: 30 * time.Second,
|
|
||||||
IdleTimeout: 60 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Fatalf("❌ 服务器启动失败: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleHealth 健康检查端点
|
|
||||||
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"status": "ok",
|
|
||||||
"time": time.Now().Format(time.RFC3339),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleWebhook 处理 Gitea webhook
|
|
||||||
func handleWebhook(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != http.MethodPost {
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 单实例部署:避免并发执行 deploy.sh 引发互相踩踏
|
|
||||||
if !tryStartDeploy() {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Header().Set("Retry-After", "30")
|
|
||||||
w.WriteHeader(http.StatusConflict)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"message": "部署进行中,请稍后再试",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer finishDeploy()
|
|
||||||
|
|
||||||
// 限制请求体大小,防止大包占用内存
|
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, cfg.MaxBody)
|
|
||||||
|
|
||||||
// 读取请求体
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("❌ 读取请求体失败: %v", err)
|
|
||||||
http.Error(w, "Failed to read body", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// 验证签名
|
|
||||||
if !verifySignature(r.Header.Get("X-Gitea-Signature"), body) {
|
|
||||||
log.Println("⚠️ 无效的签名")
|
|
||||||
http.Error(w, "Invalid signature", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 payload
|
|
||||||
var payload GiteaPayload
|
|
||||||
if err := json.Unmarshal(body, &payload); err != nil {
|
|
||||||
log.Printf("❌ 解析 JSON 失败: %v", err)
|
|
||||||
http.Error(w, "Failed to parse JSON", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只处理主分支推送(严格匹配)
|
|
||||||
if payload.Ref != "refs/heads/master" && payload.Ref != "refs/heads/main" {
|
|
||||||
log.Printf("⏭️ 忽略非主分支推送: %s", payload.Ref)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"message": "非主分支,跳过部署",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("✅ 收到有效的 Webhook 请求")
|
|
||||||
log.Printf(" 仓库: %s", payload.Repository.FullName)
|
|
||||||
log.Printf(" 推送者: %s", payload.Pusher.Username)
|
|
||||||
log.Printf(" 分支: %s", payload.Ref)
|
|
||||||
commitShort := payload.Commit
|
|
||||||
if len(commitShort) > 7 {
|
|
||||||
commitShort = commitShort[:7]
|
|
||||||
}
|
|
||||||
log.Printf(" 提交: %s", commitShort)
|
|
||||||
|
|
||||||
// 异步执行部署(释放 HTTP 线程)
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
// 避免 goroutine panic 导致“卡住 running 状态”
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
log.Printf("❌ 部署流程 panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
executeDeployment(payload)
|
|
||||||
}()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusAccepted)
|
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
|
||||||
"message": "部署已启动",
|
|
||||||
"commit": payload.Commit,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// verifySignature 验证 Gitea webhook 签名
|
|
||||||
func verifySignature(signature string, body []byte) bool {
|
|
||||||
if signature == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := hmac.New(sha256.New, []byte(cfg.Secret))
|
|
||||||
hash.Write(body)
|
|
||||||
expected := []byte("sha256=" + hex.EncodeToString(hash.Sum(nil)))
|
|
||||||
return hmac.Equal([]byte(signature), expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeDeployment 执行部署
|
|
||||||
func executeDeployment(payload GiteaPayload) {
|
|
||||||
log.Println("\n🔄 开始部署流程...")
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
|
|
||||||
// 检查脚本是否存在
|
|
||||||
if _, err := os.Stat(cfg.ScriptPath); os.IsNotExist(err) {
|
|
||||||
log.Printf("❌ 部署脚本不存在: %s", cfg.ScriptPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行部署脚本(把 ref/commit 传给脚本,便于按分支部署与日志记录)
|
|
||||||
cmd := exec.Command("bash", cfg.ScriptPath)
|
|
||||||
cmd.Dir = cfg.RepoPath
|
|
||||||
cmd.Env = append(os.Environ(),
|
|
||||||
"GIT_REF="+payload.Ref,
|
|
||||||
"GIT_COMMIT="+payload.Commit,
|
|
||||||
)
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
log.Printf("📜 执行部署脚本: %s", cfg.ScriptPath)
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Printf("❌ 部署脚本执行失败: %v", err)
|
|
||||||
if stdout.Len() > 0 {
|
|
||||||
log.Printf("输出:\n%s", stdout.String())
|
|
||||||
}
|
|
||||||
if stderr.Len() > 0 {
|
|
||||||
log.Printf("错误输出:\n%s", stderr.String())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
duration := time.Since(startTime).Seconds()
|
|
||||||
|
|
||||||
log.Printf("✅ 部署完成")
|
|
||||||
log.Printf(" 耗时: %.2f 秒", duration)
|
|
||||||
|
|
||||||
if stdout.Len() > 0 {
|
|
||||||
log.Printf("输出:\n%s", stdout.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("📝 部署日志:\n")
|
|
||||||
log.Printf(" 仓库: %s", payload.Repository.FullName)
|
|
||||||
commitShort := payload.Commit
|
|
||||||
if len(commitShort) > 7 {
|
|
||||||
commitShort = commitShort[:7]
|
|
||||||
}
|
|
||||||
log.Printf(" 提交: %s", commitShort)
|
|
||||||
log.Printf(" 分支: %s", payload.Ref)
|
|
||||||
log.Printf(" 推送者: %s", payload.Pusher.Username)
|
|
||||||
log.Printf(" 时间: %s", time.Now().Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryStartDeploy() bool {
|
|
||||||
deployMu.Lock()
|
|
||||||
defer deployMu.Unlock()
|
|
||||||
if deployRunning {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
deployRunning = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func finishDeploy() {
|
|
||||||
deployMu.Lock()
|
|
||||||
deployRunning = false
|
|
||||||
deployMu.Unlock()
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user