1410 字
7 分钟
使用Gitee + Webhook自动化部署网站
TLDR
NOTE主要利用了 Gitee 的 Webhook 功能,当推送代码到 Gitee 时,Gitee会发送一条 POST 请求到指定的地址,只需要在服务器端编写响应程序即可实现自动化部署,流程图如下:
sequenceDiagram participant 开发者 participant Gitee participant 个人服务器 开发者->>Gitee: 设置 Webhook 地址与 SecretKey 开发者->>Gitee: git push Gitee->>个人服务器: 触发 Webhook (POST请求) 个人服务器->>个人服务器: 验证 token 个人服务器->>Gitee: 验证结果 个人服务器->>个人服务器: 开始部署 个人服务器->>开发者: 部署结果通知(飞书或微信)
步骤
1. 编写在服务器自动部署项目的程序
代码放在了文章末尾,主要流程是收到 Gitee 发出的 POST 请求后,编译项目并将输出的文件复制到网站根目录。这里的代码仅使用于使用 pnpm 进行构建的前端项目,如果是其他的项目,需要对 deploy() 函数进行修改。
2. 将服务器的公钥粘贴到 Gitee 中
在服务器上执行下面的代码获取公钥,尽量不要使用 root 用户进行部署(这里选择的是 www 用户)。
cat /home/www/.ssh/id_rsa.pub将公钥粘贴在 Gitee 对应项目的 部署公钥 中。

3. 在服务器上下载项目代码并编译
将项目代码下载到服务器。
sudo -u www git clone <项目ssh地址>初步编译一下项目,确保项目可以在服务器上正常编译。
# 使用pnpm安装依赖库sudo -u www npx pnpm install
# 编译一下,确保编译过程正常sudo -u www npx pnpm run build4. 在服务器上启动部署程序
NOTE运行前需要安装 flask 库。
pip3 install flask
在服务器上以 www 用户的身份运行部署程序。
注意设置一个 SECRET_KEY ,Gitee 发送部署请求时需要携带该参数(可以随意设置,这里设置的是 test)。
sudo -u www SECRET_KEY=test python auto_deploy.py &运行后执行下面的命令测试一下,如果部署成功,将输出 ok 。
curl 127.0.0.1:99995. 设置 Webhook
进入 Gitee 的项目,点击 管理-> webhooks -> 添加Webhook

输入基础信息并保存。

如果配置无误,点击 Webhook 页面的 测试 按钮,将显示 {"message":"Token is ok"}

6. 开始使用
在使用 git push 上传代码后,稍等片刻,即可看到网站已更新。
代码
auto_deploy.py
import osimport shutilimport subprocessfrom flask import Flask, request, jsonify, after_this_requestimport filecmpimport multiprocessingimport timeimport hmacimport hashlibimport base64import os
# 配置项
# 项目工作目录WORKING_DIR = "/www/wwwroot/CLoudSirLab-Blog/blog-cloud-site"# 网站部署目录DEPLOY_DIR = "/www/wwwroot/cloudsir.top"# 默认TOKENDEFAULT_SECRET_TOKEN = "test"
DEPLOYING = Falseprocess_handler = Noneretry = 0
def verify_signature(timestamp, sign): """ 验证签名函数 :param timestamp: 时间戳(毫秒) :param sign: 需要验证的签名 :return: bool,表示签名是否有效 """ # 从环境变量中获取密钥 secret = os.getenv('SECRET_KEY', DEFAULT_SECRET_TOKEN) # 如果环境变量未设置,则使用默认值 secret_enc = secret.encode('utf-8')
# 构造签名字符串 string_to_sign = f'{timestamp}\n{secret}' string_to_sign_enc = string_to_sign.encode('utf-8')
# 生成 HMAC 签名 hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() expected_sign = base64.b64encode(hmac_code).decode('utf-8')
print("---HMAC---") print("timestamp: ", timestamp) print("secret: ", secret) print("sign: ", sign) print("expected_sign: ", expected_sign) print("---HMAC---")
# 比较传入的签名和生成的签名 return hmac.compare_digest(sign, expected_sign)
def deploy(): global DEPLOYING global retry
try: print("拉取最新代码") # 拉取最新代码 subprocess.run(["git", "pull"], check=True)
print("开始编译") # 编译项目 build_result = subprocess.run(["npx", "pnpm", "run", "build"], check=True, capture_output=True, text=True)
if build_result.returncode == 0: print("开始部署")
# 将文件复制到部署目录 dist_path = os.path.join(WORKING_DIR, "dist") deploy_path = DEPLOY_DIR sync_directories(dist_path, deploy_path)
# 发送消息通知 notify_me("Deployment completed successfully") DEPLOYING = False else: print("部署失败") # 部署失败 error_message = build_result.stderr DEPLOYING = False
print(error_message)
if retry > 0: print("重新部署") retry = retry - 1 deploy() else: notify_me(f"Deployment failed: {error_message}")
except subprocess.CalledProcessError as e: error_message = str(e) DEPLOYING = False print(error_message) if retry > 0: print("重新部署") retry = retry - 1 deploy() else: notify_me(f"Deployment failed: {error_message}")
# 通知函数(可以改造成飞书推送、微信推送)def notify_me(s): print(s)
app = Flask(__name__)
@app.route('/'):def test(): return "ok"
@app.route('/webhook', methods=['POST'])def webhook(): global DEPLOYING global process_handler global retry
# 获取请求头 event_type = request.headers.get('X-Git-Oschina-Event') token = request.headers.get('X-Gitee-Token') timestamp = request.headers.get('X-Gitee-Timestamp')
print(request.headers)
# 逐个判断是否都存在 if not event_type: return jsonify({"error": "event_type is missing"}), 400 if not token: return jsonify({"error": "token is missing"}), 400 if not timestamp: return jsonify({"error": "timestamp is missing"}), 400
# 验证时间戳1分钟有效期 timestamp_is_valid = time.time() - (int(timestamp) / 1000) < 60 if not timestamp_is_valid: return jsonify({"error": "Timestamp is expired"}), 401
# 验证token if not verify_signature(timestamp, token): print("Invalid signature") return jsonify({"error": "Invalid signature"}), 401
# 验证事件类型 if event_type == 'Push Hook': print("Push Hook") elif event_type == 'push_hooks': return jsonify({"message": "Token is ok"}), 200 else: return jsonify({"error": "Invalid event type"}), 400
# 切换工作目录 os.chdir(WORKING_DIR)
DEPLOYING = True
if (process_handler is not None) and (process_handler.is_alive()): print("检测到正在运行的部署进程,尝试终止...") process_handler.terminate() process_handler.join() # 等待进程终止 print("旧部署进程已终止")
print("开始部署") retry = 3 process_handler = multiprocessing.Process(target=deploy) process_handler.start()
return jsonify({"message": "Start Deploying"}), 200
def sync_directories(src, dst): """ 同步两个目录,确保 dst 目录与 src 目录一致,只更新变化的文件,删除多余的文件。 """ # 遍历 src 目录,更新或添加文件 for root, dirs, files in os.walk(src): relative_path = os.path.relpath(root, src) target_path = os.path.join(dst, relative_path)
# 确保目标目录存在 if not os.path.exists(target_path): os.makedirs(target_path)
for file in files: src_file = os.path.join(root, file) dst_file = os.path.join(target_path, file)
# 如果目标文件不存在或内容不同,则更新文件 if not os.path.exists(dst_file) or not filecmp.cmp(src_file, dst_file, shallow=False): shutil.copy2(src_file, dst_file)
# 遍历 dst 目录,删除多余的文件和目录 for root, dirs, files in os.walk(dst, topdown=False): relative_path = os.path.relpath(root, dst) src_path = os.path.join(src, relative_path)
for file in files: dst_file = os.path.join(root, file) src_file = os.path.join(src_path, file)
# 如果目标文件在 src 中不存在,则删除 if not os.path.exists(src_file) and not src_file.endswith(".user.ini"): os.remove(dst_file)
for dir in dirs: dst_dir = os.path.join(root, dir) src_dir = os.path.join(src_path, dir)
# 如果目标目录在 src 中不存在,则删除 if not os.path.exists(src_dir): shutil.rmtree(dst_dir)
if __name__ == '__main__': app.run(host='0.0.0.0', port=9999) 使用Gitee + Webhook自动化部署网站
https://cloudsir.top/posts/push-blog-by-gitee-webhook/