添加webhook
This commit is contained in:
parent
e037e0e11c
commit
6af0532185
|
|
@ -22,6 +22,12 @@ logging:
|
||||||
file: .devops/logs/devops.log
|
file: .devops/logs/devops.log
|
||||||
max_size: 10485760 # 10MB
|
max_size: 10485760 # 10MB
|
||||||
|
|
||||||
|
# 钉钉通知配置
|
||||||
|
dingtalk:
|
||||||
|
enabled: true # 是否启用钉钉通知
|
||||||
|
access_token: ed3533e05cf13e090c098436ee6cd52b2adfa2d85b5b2b9da1ae2bccdaecb8f3
|
||||||
|
secret: SEC66372694e16e7e931f53aefb4b847b7fb6c42350a10f0f27fbf4151785353261
|
||||||
|
|
||||||
# 基础设施服务配置(只部署一次)
|
# 基础设施服务配置(只部署一次)
|
||||||
infrastructure:
|
infrastructure:
|
||||||
- name: ruoyi-mysql
|
- name: ruoyi-mysql
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
from scripts.log import Logger
|
from scripts.log import Logger
|
||||||
from scripts import docker, maven, npm
|
from scripts import docker, maven, npm
|
||||||
from scripts.init import mysql, redis, nacos
|
from scripts.init import mysql, redis, nacos
|
||||||
|
from scripts.dingtalk import DingTalkNotifier
|
||||||
|
|
||||||
|
|
||||||
class GitMonitor:
|
class GitMonitor:
|
||||||
|
|
@ -33,11 +34,13 @@ class GitMonitor:
|
||||||
self.global_branch = 'main'
|
self.global_branch = 'main'
|
||||||
self.project_root = None
|
self.project_root = None
|
||||||
self.runtime_path = None
|
self.runtime_path = None
|
||||||
|
self.dingtalk_notifier = None
|
||||||
|
|
||||||
# 初始化
|
# 初始化
|
||||||
self._print_startup_banner()
|
self._print_startup_banner()
|
||||||
self._load_config()
|
self._load_config()
|
||||||
self._init_paths()
|
self._init_paths()
|
||||||
|
self._init_dingtalk()
|
||||||
|
|
||||||
def _print_startup_banner(self):
|
def _print_startup_banner(self):
|
||||||
"""打印启动横幅"""
|
"""打印启动横幅"""
|
||||||
|
|
@ -89,6 +92,24 @@ class GitMonitor:
|
||||||
Logger.error(f"路径初始化失败: {e}")
|
Logger.error(f"路径初始化失败: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
def _init_dingtalk(self):
|
||||||
|
"""初始化钉钉通知器"""
|
||||||
|
try:
|
||||||
|
dingtalk_config = self.config.get('dingtalk', {})
|
||||||
|
if dingtalk_config.get('enabled', False):
|
||||||
|
access_token = dingtalk_config.get('access_token')
|
||||||
|
secret = dingtalk_config.get('secret')
|
||||||
|
|
||||||
|
if access_token:
|
||||||
|
self.dingtalk_notifier = DingTalkNotifier(access_token, secret)
|
||||||
|
Logger.info("✓ 钉钉通知已启用")
|
||||||
|
else:
|
||||||
|
Logger.warning("钉钉通知已启用但未配置 access_token")
|
||||||
|
else:
|
||||||
|
Logger.info("钉钉通知未启用")
|
||||||
|
except Exception as e:
|
||||||
|
Logger.warning(f"钉钉通知初始化失败: {e}")
|
||||||
|
|
||||||
def get_remote_commit(self, repo_url, branch):
|
def get_remote_commit(self, repo_url, branch):
|
||||||
"""获取远程仓库的最新提交 hash"""
|
"""获取远程仓库的最新提交 hash"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -263,11 +284,22 @@ class GitMonitor:
|
||||||
def deploy(self, repo_config):
|
def deploy(self, repo_config):
|
||||||
"""执行部署流程"""
|
"""执行部署流程"""
|
||||||
repo_path = self.runtime_path / 'a-cloud-all'
|
repo_path = self.runtime_path / 'a-cloud-all'
|
||||||
|
repo_name = repo_config['name']
|
||||||
|
commit_hash = self.last_commits.get(repo_name, 'unknown')
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
Logger.separator()
|
Logger.separator()
|
||||||
Logger.info(f"开始部署: {repo_config['name']}")
|
Logger.info(f"开始部署: {repo_name}")
|
||||||
Logger.separator()
|
Logger.separator()
|
||||||
|
|
||||||
|
# 发送构建开始通知
|
||||||
|
if self.dingtalk_notifier:
|
||||||
|
self.dingtalk_notifier.send_build_start(
|
||||||
|
repo_name=repo_name,
|
||||||
|
branch=self.global_branch,
|
||||||
|
commit_hash=commit_hash
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 更新主仓库和子模块
|
# 1. 更新主仓库和子模块
|
||||||
if not self.update_main_repo():
|
if not self.update_main_repo():
|
||||||
|
|
@ -303,12 +335,46 @@ class GitMonitor:
|
||||||
service_name = repo_config['docker_service']
|
service_name = repo_config['docker_service']
|
||||||
|
|
||||||
if not docker.run_docker_compose(compose_dir, service_name):
|
if not docker.run_docker_compose(compose_dir, service_name):
|
||||||
|
# 发送构建失败通知
|
||||||
|
if self.dingtalk_notifier:
|
||||||
|
duration = time.time() - start_time
|
||||||
|
self.dingtalk_notifier.send_build_failure(
|
||||||
|
repo_name=repo_name,
|
||||||
|
branch=self.global_branch,
|
||||||
|
commit_hash=commit_hash,
|
||||||
|
error_msg="Docker 部署失败"
|
||||||
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 计算构建耗时
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# 发送构建成功通知
|
||||||
|
if self.dingtalk_notifier:
|
||||||
|
self.dingtalk_notifier.send_build_success(
|
||||||
|
repo_name=repo_name,
|
||||||
|
branch=self.global_branch,
|
||||||
|
commit_hash=commit_hash,
|
||||||
|
duration=duration
|
||||||
|
)
|
||||||
|
|
||||||
Logger.info(f"部署完成: {repo_config['name']}")
|
Logger.info(f"部署完成: {repo_config['name']}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
# 计算构建耗时
|
||||||
|
duration = time.time() - start_time
|
||||||
|
|
||||||
|
# 发送构建失败通知
|
||||||
|
if self.dingtalk_notifier:
|
||||||
|
self.dingtalk_notifier.send_build_failure(
|
||||||
|
repo_name=repo_name,
|
||||||
|
branch=self.global_branch,
|
||||||
|
commit_hash=commit_hash,
|
||||||
|
error_msg=str(e),
|
||||||
|
at_all=True
|
||||||
|
)
|
||||||
|
|
||||||
Logger.error(f"部署异常: {e}")
|
Logger.error(f"部署异常: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
钉钉 Webhook 通知模块
|
||||||
|
用于发送构建和部署通知到钉钉群
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class DingTalkNotifier:
|
||||||
|
"""钉钉通知器"""
|
||||||
|
|
||||||
|
def __init__(self, access_token, secret=None):
|
||||||
|
"""
|
||||||
|
初始化钉钉通知器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_token: 钉钉机器人的 access_token
|
||||||
|
secret: 钉钉机器人的加签密钥(可选)
|
||||||
|
"""
|
||||||
|
self.access_token = access_token
|
||||||
|
self.secret = secret
|
||||||
|
self.base_url = "https://oapi.dingtalk.com/robot/send"
|
||||||
|
|
||||||
|
def _generate_sign(self, timestamp):
|
||||||
|
"""
|
||||||
|
生成钉钉加签
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timestamp: 当前时间戳(毫秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
签名字符串
|
||||||
|
"""
|
||||||
|
if not self.secret:
|
||||||
|
return None
|
||||||
|
|
||||||
|
string_to_sign = f'{timestamp}\n{self.secret}'
|
||||||
|
string_to_sign_enc = string_to_sign.encode('utf-8')
|
||||||
|
secret_enc = self.secret.encode('utf-8')
|
||||||
|
|
||||||
|
hmac_code = hmac.new(
|
||||||
|
secret_enc,
|
||||||
|
string_to_sign_enc,
|
||||||
|
digestmod=hashlib.sha256
|
||||||
|
).digest()
|
||||||
|
|
||||||
|
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
|
||||||
|
return sign
|
||||||
|
|
||||||
|
def _build_url(self):
|
||||||
|
"""
|
||||||
|
构建完整的 webhook URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
完整的 URL 字符串
|
||||||
|
"""
|
||||||
|
url = f"{self.base_url}?access_token={self.access_token}"
|
||||||
|
|
||||||
|
if self.secret:
|
||||||
|
timestamp = str(round(time.time() * 1000))
|
||||||
|
sign = self._generate_sign(timestamp)
|
||||||
|
url += f"×tamp={timestamp}&sign={sign}"
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
def send_text(self, content, at_mobiles=None, at_all=False):
|
||||||
|
"""
|
||||||
|
发送文本消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: 消息内容
|
||||||
|
at_mobiles: @的手机号列表
|
||||||
|
at_all: 是否@所有人
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"msgtype": "text",
|
||||||
|
"text": {
|
||||||
|
"content": content
|
||||||
|
},
|
||||||
|
"at": {
|
||||||
|
"atMobiles": at_mobiles or [],
|
||||||
|
"isAtAll": at_all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._send_request(data)
|
||||||
|
|
||||||
|
def send_build_success(self, repo_name, branch, commit_hash, duration):
|
||||||
|
"""
|
||||||
|
发送构建成功通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_name: 仓库名称
|
||||||
|
branch: 分支名称
|
||||||
|
commit_hash: 提交哈希
|
||||||
|
duration: 构建耗时(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
title = f"✅ 构建成功 - {repo_name}"
|
||||||
|
text = f"""### ✅ 构建成功
|
||||||
|
|
||||||
|
**仓库**: {repo_name}
|
||||||
|
**分支**: {branch}
|
||||||
|
**提交**: {commit_hash[:8]}
|
||||||
|
**耗时**: {duration:.1f} 秒
|
||||||
|
**时间**: {now}
|
||||||
|
|
||||||
|
---
|
||||||
|
构建和部署已完成!
|
||||||
|
"""
|
||||||
|
return self.send_markdown(title, text)
|
||||||
|
|
||||||
|
def send_build_failure(self, repo_name, branch, commit_hash, error_msg, at_all=False):
|
||||||
|
"""
|
||||||
|
发送构建失败通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_name: 仓库名称
|
||||||
|
branch: 分支名称
|
||||||
|
commit_hash: 提交哈希
|
||||||
|
error_msg: 错误信息
|
||||||
|
at_all: 是否@所有人
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
title = f"❌ 构建失败 - {repo_name}"
|
||||||
|
# 限制错误信息长度
|
||||||
|
error_display = error_msg[:500] if len(error_msg) > 500 else error_msg
|
||||||
|
text = f"""### ❌ 构建失败
|
||||||
|
|
||||||
|
**仓库**: {repo_name}
|
||||||
|
**分支**: {branch}
|
||||||
|
**提交**: {commit_hash[:8]}
|
||||||
|
**时间**: {now}
|
||||||
|
|
||||||
|
**错误信息**:
|
||||||
|
```
|
||||||
|
{error_display}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
请检查日志并修复问题!
|
||||||
|
"""
|
||||||
|
return self.send_markdown(title, text, at_all=at_all)
|
||||||
|
|
||||||
|
def send_build_start(self, repo_name, branch, commit_hash):
|
||||||
|
"""
|
||||||
|
发送构建开始通知
|
||||||
|
|
||||||
|
Args:
|
||||||
|
repo_name: 仓库名称
|
||||||
|
branch: 分支名称
|
||||||
|
commit_hash: 提交哈希
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
title = f"🚀 构建开始 - {repo_name}"
|
||||||
|
text = f"""### 🚀 构建开始
|
||||||
|
|
||||||
|
**仓库**: {repo_name}
|
||||||
|
**分支**: {branch}
|
||||||
|
**提交**: {commit_hash[:8]}
|
||||||
|
**时间**: {now}
|
||||||
|
|
||||||
|
---
|
||||||
|
构建任务已启动,请稍候...
|
||||||
|
"""
|
||||||
|
return self.send_markdown(title, text)
|
||||||
|
|
||||||
|
def _send_request(self, data):
|
||||||
|
"""
|
||||||
|
发送 HTTP 请求到钉钉 webhook
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: 请求数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
url = self._build_url()
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
json_data = json.dumps(data).encode('utf-8')
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, data=json_data, headers=headers)
|
||||||
|
response = urllib.request.urlopen(req, timeout=10)
|
||||||
|
result = json.loads(response.read().decode('utf-8'))
|
||||||
|
|
||||||
|
if result.get('errcode') == 0:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"钉钉通知发送失败: {result.get('errmsg')}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"钉钉通知发送异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_markdown(self, title, text, at_mobiles=None, at_all=False):
|
||||||
|
"""
|
||||||
|
发送 Markdown 消息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: 消息标题
|
||||||
|
text: Markdown 格式的消息内容
|
||||||
|
at_mobiles: @的手机号列表
|
||||||
|
at_all: 是否@所有人
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果 (True/False)
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
"msgtype": "markdown",
|
||||||
|
"markdown": {
|
||||||
|
"title": title,
|
||||||
|
"text": text
|
||||||
|
},
|
||||||
|
"at": {
|
||||||
|
"atMobiles": at_mobiles or [],
|
||||||
|
"isAtAll": at_all
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self._send_request(data)
|
||||||
Loading…
Reference in New Issue