添加webhook

This commit is contained in:
孙小云 2026-01-27 14:17:44 +08:00
parent e037e0e11c
commit 6af0532185
3 changed files with 316 additions and 1 deletions

View File

@ -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

View File

@ -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

243
.devops/scripts/dingtalk.py Normal file
View File

@ -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"&timestamp={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)