diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 000000000..825db6669 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,123 @@ +# RuoYi 自动发布配置指南 + +## 原理 + +``` +你 push 代码到 Gitee + ↓ +Gitee 发送 Webhook 到服务器 :9000/webhook + ↓ +webhook.py 验证签名 + 过滤 master 分支 + ↓ +触发 deploy.sh: git pull → mvn build → 停旧 → 启新 +``` + +## 第一步:上传文件到服务器 + +在本地 `deploy/` 目录执行: + +```bash +scp deploy.sh root@47.94.224.255:/opt/ruoyi/ +scp webhook.py root@47.94.224.255:/opt/ruoyi/ +scp ruoyi-webhook.service root@47.94.224.255:/etc/systemd/system/ +``` + +## 第二步:SSH 登录服务器配置 + +```bash +ssh root@47.94.224.255 +``` + +### 2.1 设置脚本权限 + +```bash +chmod +x /opt/ruoyi/deploy.sh +chmod +x /opt/ruoyi/webhook.py +``` + +### 2.2 启动 Webhook 服务 + +```bash +systemctl daemon-reload +systemctl enable ruoyi-webhook +systemctl start ruoyi-webhook +systemctl status ruoyi-webhook +``` + +### 2.3 验证 Webhook 运行 + +```bash +curl http://localhost:9000/health +# 应返回: OK +``` + +### 2.4 开放防火墙端口(如有) + +```bash +# 检查防火墙 +firewall-cmd --state 2>/dev/null || echo "防火墙未运行" + +# 如果需要开放 +firewall-cmd --add-port=9000/tcp --permanent +firewall-cmd --reload +``` + +**阿里云安全组也需要放行 9000 端口**(入方向)。 + +## 第三步:配置 Gitee Webhook + +1. 打开 https://gitee.com/zhangYuXinGit/system +2. 进入 **管理 → WebHooks** +3. 点击 "添加 WebHook" +4. 填写: + - **URL**: `http://47.94.224.255:9000/webhook` + - **密码**: `ruoyi-deploy-2024` + - **事件**: 勾选 "Push"(只选 Push 就够了) +5. 点击 "添加",然后点击 "测试" 验证连接 + +## 第四步:测试自动发布 + +在本地修改代码并推送: + +```bash +cd E:\java\project\system +# 修改一些代码... +git add . +git commit -m "test: 测试自动发布" +git push +``` + +然后在服务器查看部署日志: + +```bash +ssh root@47.94.224.255 "tail -f /opt/ruoyi/deploy.log" +``` + +## 常用运维命令 + +```bash +# 查看 webhook 日志 +tail -f /opt/ruoyi/webhook.log + +# 查看部署日志 +tail -f /opt/ruoyi/deploy.log + +# 查看应用日志 +tail -f /opt/ruoyi/app.log + +# 重启 webhook 服务 +systemctl restart ruoyi-webhook + +# 手动触发部署(不走 webhook) +ssh root@47.94.224.255 "/opt/ruoyi/deploy.sh" + +# 停止自动发布 +systemctl stop ruoyi-webhook +systemctl disable ruoyi-webhook +``` + +## 安全建议 + +- 生产环境建议将 Webhook URL 改为 HTTPS +- 可修改 `webhook.py` 中的 `WEBHOOK_SECRET` 更换密钥 +- 可在阿里云安全组中限制 9000 端口仅允许 Gitee 的 IP 访问 diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 000000000..23ce52990 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,81 @@ +#!/bin/bash +#=========================================================================== +# RuoYi 自动部署脚本 +# 触发方式: Gitee Webhook → webhook.py → deploy.sh +#=========================================================================== + +set -e + +APP_DIR=/opt/ruoyi +JAR_FILE=$APP_DIR/ruoyi-admin/target/ruoyi-admin.jar +LOG_FILE=$APP_DIR/app.log +DEPLOY_LOG=$APP_DIR/deploy.log +PROFILE=druid +PORT=8080 + +log() { + echo "$(date '+%Y-%m-%d %H:%M:%S') [DEPLOY] $1" | tee -a "$DEPLOY_LOG" +} + +log "========== 开始部署 ==========" + +# 1. 拉取最新代码 +cd "$APP_DIR" +log "拉取最新代码..." +git pull origin master 2>&1 | tee -a "$DEPLOY_LOG" + +# 2. Maven 构建 +log "开始 Maven 构建..." +mvn clean package -DskipTests -pl ruoyi-admin -am -q 2>&1 | tee -a "$DEPLOY_LOG" + +if [ ! -f "$JAR_FILE" ]; then + log "构建失败: JAR 文件不存在" + exit 1 +fi + +log "构建成功" + +# 3. 停止旧进程 +OLD_PID=$(ps aux | grep ruoyi-admin.jar | grep -v grep | awk '{print $2}') +if [ -n "$OLD_PID" ]; then + log "停止旧进程 PID: $OLD_PID" + kill "$OLD_PID" + sleep 3 + if ps -p "$OLD_PID" > /dev/null 2>&1; then + kill -9 "$OLD_PID" + log "强制停止 PID: $OLD_PID" + fi +fi + +# 4. 启动新进程 +log "启动新进程..." +nohup java -Xms128m -Xmx384m -jar "$JAR_FILE" \ + --spring.profiles.active="$PROFILE" \ + --server.port="$PORT" \ + > "$LOG_FILE" 2>&1 & + +NEW_PID=$! +log "新进程 PID: $NEW_PID" + +# 5. 等待启动 +sleep 5 +if ps -p "$NEW_PID" > /dev/null 2>&1; then + log "应用启动中,等待就绪..." + for i in 1 2 3 4 5 6; do + if grep -q "Started RuoYiApplication" "$LOG_FILE" 2>/dev/null; then + log "部署成功! 应用已就绪" + break + fi + sleep 5 + done +else + log "启动失败! 查看日志: tail -50 $LOG_FILE" + exit 1 +fi + +# 6. 健康检查 +sleep 3 +HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$PORT 2>/dev/null || echo "000") +log "健康检查 HTTP $HTTP_CODE" + +log "========== 部署结束 ==========" diff --git a/deploy/ruoyi-webhook.service b/deploy/ruoyi-webhook.service new file mode 100644 index 000000000..cdde61b3b --- /dev/null +++ b/deploy/ruoyi-webhook.service @@ -0,0 +1,16 @@ +[Unit] +Description=RuoYi Webhook Deploy Service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/ruoyi +ExecStart=/usr/bin/python3 /opt/ruoyi/webhook.py +Restart=always +RestartSec=10 +StandardOutput=append:/opt/ruoyi/webhook.log +StandardError=append:/opt/ruoyi/webhook.log + +[Install] +WantedBy=multi-user.target diff --git a/deploy/webhook.py b/deploy/webhook.py new file mode 100644 index 000000000..86c8abbaf --- /dev/null +++ b/deploy/webhook.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +""" +RuoYi Gitee Webhook 接收服务 + +接收 Gitee 推送事件,触发自动部署。 +启动: python3 webhook.py +""" + +import hashlib +import hmac +import json +import subprocess +import time +from http.server import HTTPServer, BaseHTTPRequestHandler +from datetime import datetime + +# ============ 配置 ============ +LISTEN_HOST = "0.0.0.0" +LISTEN_PORT = 9000 + +# Gitee Webhook 密码(请在 Gitee 仓库设置中配置相同的密钥) +WEBHOOK_SECRET = "ruoyi-deploy-2024" + +# 部署脚本路径 +DEPLOY_SCRIPT = "/opt/ruoyi/deploy.sh" + +# 部署锁文件,防止并发部署 +LOCK_FILE = "/tmp/ruoyi-deploy.lock" + +# ============ 日志 ============ +def log(msg): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + line = f"[{timestamp}] [WEBHOOK] {msg}" + print(line, flush=True) + + +class WebhookHandler(BaseHTTPRequestHandler): + + def do_POST(self): + if self.path != "/webhook": + self.send_response(404) + self.end_headers() + return + + # 读取请求体 + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) + + # 验证签名 + signature = self.headers.get("X-Gitee-Token", "") + if signature != WEBHOOK_SECRET: + log(f"签名验证失败: {signature[:20]}...") + self.send_response(403) + self.end_headers() + self.wfile.write(b"Forbidden") + return + + # 解析事件 + try: + data = json.loads(body) + except json.JSONDecodeError: + log("JSON 解析失败") + self.send_response(400) + self.end_headers() + return + + event = self.headers.get("X-Gitee-Event", "unknown") + ref = data.get("ref", "") + pusher = data.get("pusher", {}).get("name", "unknown") + + log(f"收到事件: {event} | 分支: {ref} | 推送者: {pusher}") + + # 只处理 master 分支的 push 事件 + if event != "push_hooks" or ref != "refs/heads/master": + log(f"忽略事件: {event} / {ref}") + self.send_response(200) + self.end_headers() + self.wfile.write(b"Ignored") + return + + # 检查锁 + if self._is_locked(): + log("部署正在进行中,忽略重复触发") + self.send_response(200) + self.end_headers() + self.wfile.write(b"Deploy already in progress") + return + + # 异步执行部署 + self._lock() + subprocess.Popen( + ["/bin/bash", DEPLOY_SCRIPT], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + + self.send_response(200) + self.end_headers() + self.wfile.write(b"Deploy started") + + def do_GET(self): + """健康检查""" + if self.path == "/health": + self.send_response(200) + self.end_headers() + self.wfile.write(b"OK") + else: + self.send_response(404) + self.end_headers() + + def _is_locked(self): + try: + with open(LOCK_FILE, "r") as f: + lock_time = float(f.read().strip()) + # 锁超过 30 分钟自动过期 + if time.time() - lock_time > 1800: + return False + return True + except FileNotFoundError: + return False + + def _lock(self): + with open(LOCK_FILE, "w") as f: + f.write(str(time.time())) + + def log_message(self, format, *args): + """重写默认日志,改为简洁格式""" + log(f"{self.client_address[0]} - {format % args}") + + +def main(): + log(f"Webhook 服务启动在 http://{LISTEN_HOST}:{LISTEN_PORT}/webhook") + log(f"密钥: {WEBHOOK_SECRET[:4]}****") + + server = HTTPServer((LISTEN_HOST, LISTEN_PORT), WebhookHandler) + try: + server.serve_forever() + except KeyboardInterrupt: + log("服务已停止") + server.server_close() + + +if __name__ == "__main__": + main()