Просмотр исходного кода

feat: 添加自动部署功能

- server/webhook-deploy.py: Python webhook 接收服务
- server/deploy-auto.sh: 自动部署脚本
- AUTO_DEPLOY.md: 部署配置指南

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 4 месяцев назад
Родитель
Сommit
a61608dc26
3 измененных файлов с 323 добавлено и 0 удалено
  1. 141 0
      AUTO_DEPLOY.md
  2. 44 0
      server/deploy-auto.sh
  3. 138 0
      server/webhook-deploy.py

+ 141 - 0
AUTO_DEPLOY.md

@@ -0,0 +1,141 @@
+# 自动部署配置指南
+
+## 架构说明
+
+```
+Git Push → Gogs Webhook → 服务器 Webhook 接收器 → 自动部署脚本 → PM2 重启
+```
+
+## 服务器端配置
+
+### 1. 上传 webhook 脚本到服务器
+
+```bash
+# 上传到服务器
+scp -P 22622 server/webhook-deploy.py root@8.159.134.106:/data/ai/
+
+# 或者直接编辑服务器上的文件
+ssh -p 22622 root@8.159.134.106
+```
+
+### 2. 设置 webhook 服务自启动
+
+创建 systemd 服务文件 `/etc/systemd/system/webhook.service`:
+
+```ini
+[Unit]
+Description=Webhook Deploy Service
+After=network.target
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/data/ai
+Environment=WEBHOOK_SECRET=your-secret-key
+Environment=WEBHOOK_PORT=8080
+ExecStart=/usr/bin/python3 /data/ai/webhook-deploy.py
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+启用服务:
+```bash
+systemctl enable webhook
+systemctl start webhook
+systemctl status webhook
+```
+
+### 3. 配置 Nginx 转发
+
+在 Nginx 配置中添加:
+
+```nginx
+location /webhook {
+    proxy_pass http://127.0.0.1:8080;
+    proxy_set_header Host $http_host;
+    proxy_set_header X-Real-IP $remote_addr;
+}
+```
+
+重载 Nginx:
+```bash
+nginx -t && nginx -s reload
+```
+
+## Gogs Webhook 配置
+
+1. 登录 Gogs 进入仓库
+2. 设置 → Webhook → 添加 Webhook
+3. 选择 "Gogs"
+4. 填写:
+   - **URL**: `https://book.rrbrr.com/webhook?secret=your-secret-key`
+   - **Secret**: `your-secret-key` (与脚本中设置的一致)
+   - **触发条件**: 选择 "Push" 事件
+5. 点击"测试"验证连接
+
+## 安全建议
+
+1. **使用 HTTPS**: 确保域名已配置 SSL 证书
+2. **设置强 Secret**: 不要使用默认的 `your-secret-key-change-me`
+3. **限制 IP**: 可以在 Nginx 中限制只有 Gogs 服务器 IP 可以访问 `/webhook`
+4. **日志监控**: 定期检查 `/tmp/webhook-deploy.log`
+
+## 手动触发部署
+
+```bash
+# SSH 到服务器
+ssh -p 22622 root@8.159.134.106
+
+# 手动执行部署
+cd /data/ai
+bash deploy-auto.sh
+```
+
+## 故障排查
+
+```bash
+# 查看 webhook 服务状态
+systemctl status webhook
+
+# 查看 webhook 日志
+tail -f /tmp/webhook-deploy.log
+
+# 查看 PM2 日志
+pm2 logs server
+
+# 测试 webhook 端点
+curl -X POST https://book.rrbrr.com/webhook -d "test=1"
+```
+
+## 简化版(无需 Python)
+
+如果服务器没有 Python,也可以用纯 bash + Nginx + Git Hook:
+
+### 创建 Git Post-Receive Hook
+
+在服务器的 Git 仓库中创建 `hooks/post-receive`:
+
+```bash
+#!/bin/bash
+while read oldrev newrev refname; do
+    branch=$(echo $refname | cut -d/ -f3)
+    if [ "$branch" = "master" ]; then
+        echo "Detected push to master, deploying..."
+        cd /data/ai/audio_codebuddy
+        git pull origin master
+        cd /data/ai/audio_codebuddy/server
+        npm run build
+        pm2 restart server
+        echo "Deploy completed at $(date)" >> /tmp/deploy.log
+    fi
+done
+```
+
+```bash
+chmod +x /data/ai/audio_codebuddy/.git/hooks/post-receive
+```
+
+注意: 这种方式需要 Gogs 使用服务器的本地 Git 仓库路径,而不是 HTTP/HTTPS 克隆 URL。

+ 44 - 0
server/deploy-auto.sh

@@ -0,0 +1,44 @@
+#!/bin/bash
+# 自动部署脚本 - 由 webhook 触发
+# 用于 /data/ai/deploy.sh 的软链接
+
+set -e
+
+DEPLOY_LOG="/tmp/deploy-auto.log"
+PROJECT_DIR="/data/ai/audio_codebuddy"
+BACKEND_PORT="3100"
+
+log() {
+    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$DEPLOY_LOG"
+}
+
+log "========== 开始自动部署 =========="
+
+# 切换到项目目录
+cd "$PROJECT_DIR"
+
+# Git pull
+log "执行 git pull..."
+git pull origin master 2>&1 | tee -a "$DEPLOY_LOG"
+
+# 构建后端
+log "构建后端..."
+cd "$PROJECT_DIR/server"
+npm run build 2>&1 | tee -a "$DEPLOY_LOG"
+
+# 安装依赖(如果 package.json 有变化)
+log "检查依赖..."
+npm install --production 2>&1 | tee -a "$DEPLOY_LOG" || true
+
+# 重启后端服务
+log "重启后端服务..."
+
+# 使用 PM2 重启
+if command -v pm2 &> /dev/null; then
+    pm2 restart server 2>&1 | tee -a "$DEPLOY_LOG" || pm2 start dist/app.js --name server 2>&1 | tee -a "$DEPLOY_LOG"
+    pm2 save 2>&1 | tee -a "$DEPLOY_LOG"
+else
+    log "PM2 未安装,跳过进程管理"
+fi
+
+log "========== 部署完成 =========="

+ 138 - 0
server/webhook-deploy.py

@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+"""
+Webhook 部署脚本
+接收 Gogs/GitHub webhook 请求,自动执行部署
+
+用法:
+1. 访问 https://book.rrbrr.com/webhook-deploy.py?secret=YOUR_SECRET
+2. 或使用 curl: curl -X POST https://book.rrbrr.com/webhook-deploy.py -d "secret=YOUR_SECRET"
+"""
+
+import os
+import sys
+import hashlib
+import hmac
+import subprocess
+from http.server import HTTPServer, BaseHTTPRequestHandler
+import json
+
+# 配置
+WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'your-secret-key-change-me')
+DEPLOY_SCRIPT = '/data/ai/deploy.sh'
+LOG_FILE = '/tmp/webhook-deploy.log'
+
+def log(msg):
+    """写入日志"""
+    timestamp = subprocess.check_output(['date', '+%Y-%m-%d %H:%M:%S']).decode().strip()
+    with open(LOG_FILE, 'a') as f:
+        f.write(f"[{timestamp}] {msg}\n")
+    print(f"[{timestamp}] {msg}")
+
+def verify_signature(secret, payload, signature):
+    """验证请求签名"""
+    if not signature:
+        return False
+    mac = hmac.new(secret.encode(), payload, hashlib.sha256)
+    return mac.hexdigest() == signature.replace('sha256=', '')
+
+def execute_deploy():
+    """执行部署脚本"""
+    try:
+        log("开始执行部署...")
+
+        # 切换到项目目录
+        os.chdir('/data/ai/audio_codebuddy')
+
+        # Git pull
+        log("执行 git pull...")
+        result = subprocess.run(['git', 'pull', 'origin', 'master'],
+                              capture_output=True, text=True, timeout=60)
+        log(f"Git pull 结果: {result.returncode}")
+        if result.stdout:
+            log(f"stdout: {result.stdout}")
+        if result.stderr:
+            log(f"stderr: {result.stderr}")
+
+        # 执行部署脚本
+        log("执行部署脚本...")
+        result = subprocess.run(['bash', DEPLOY_SCRIPT],
+                              capture_output=True, text=True, timeout=600)
+        log(f"部署脚本返回: {result.returncode}")
+        if result.stdout:
+            log(f"stdout: {result.stdout[-2000:]}")  # 只保留最后2000字符
+        if result.stderr:
+            log(f"stderr: {result.stderr[-2000:]}")
+
+        if result.returncode == 0:
+            log("部署成功!")
+            return True
+        else:
+            log(f"部署失败! 返回码: {result.returncode}")
+            return False
+    except subprocess.TimeoutExpired:
+        log("部署超时!")
+        return False
+    except Exception as e:
+        log(f"部署异常: {e}")
+        return False
+
+class WebhookHandler(BaseHTTPRequestHandler):
+    def do_GET(self):
+        """处理 GET 请求(健康检查)"""
+        self.send_response(200)
+        self.send_header('Content-type', 'text/plain')
+        self.end_headers()
+        self.wfile.write(b'Webhook is running!')
+
+    def do_POST(self):
+        """处理 POST 请求(webhook)"""
+        # 读取请求体
+        content_length = int(self.headers.get('Content-Length', 0))
+        body = self.rfile.read(content_length)
+
+        # 获取签名
+        signature = self.headers.get('X-Hub-Signature-256') or \
+                   self.headers.get('X-Gogs-Signature') or \
+                   self.headers.get('X-Gitea-Signature')
+
+        # 验证签名(如果有 secret)
+        if WEBHOOK_SECRET != 'your-secret-key-change-me':
+            if not verify_signature(WEBHOOK_SECRET, body, signature):
+                log("签名验证失败!")
+                self.send_response(401)
+                self.send_header('Content-type', 'application/json')
+                self.end_headers()
+                self.wfile.write(json.dumps({'error': 'Invalid signature'}).encode())
+                return
+
+        # 解析 payload
+        try:
+            payload = json.loads(body)
+            log(f"收到 webhook: {payload.get('ref', 'unknown')}")
+        except:
+            log(f"无法解析 payload: {body[:200]}")
+            payload = {}
+
+        # 执行部署
+        success = execute_deploy()
+
+        # 返回结果
+        self.send_response(200 if success else 500)
+        self.send_header('Content-type', 'application/json')
+        self.end_headers()
+        result = {'success': success, 'message': 'Deployment completed'}
+        self.wfile.write(json.dumps(result).encode())
+
+    def log_message(self, format, *args):
+        """禁用默认日志"""
+        pass
+
+def main():
+    port = int(os.environ.get('WEBHOOK_PORT', 8080))
+    server = HTTPServer(('0.0.0.0', port), WebhookHandler)
+    log(f"Webhook 服务启动在端口 {port}")
+    print(f"Webhook 服务启动在端口 {port}")
+    server.serve_forever()
+
+if __name__ == '__main__':
+    main()