1410 字
7 分钟
使用Gitee + Webhook自动化部署网站
2026-01-11
2026-01-16
无标签

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 用户)。

Terminal window
cat /home/www/.ssh/id_rsa.pub

将公钥粘贴在 Gitee 对应项目的 部署公钥 中。

3. 在服务器上下载项目代码并编译#

将项目代码下载到服务器。

Terminal window
sudo -u www git clone <项目ssh地址>

初步编译一下项目,确保项目可以在服务器上正常编译。

Terminal window
# 使用pnpm安装依赖库
sudo -u www npx pnpm install
# 编译一下,确保编译过程正常
sudo -u www npx pnpm run build

4. 在服务器上启动部署程序#

NOTE

运行前需要安装 flask 库。

pip3 install flask

在服务器上以 www 用户的身份运行部署程序。 注意设置一个 SECRET_KEY ,Gitee 发送部署请求时需要携带该参数(可以随意设置,这里设置的是 test)。

Terminal window
sudo -u www SECRET_KEY=test python auto_deploy.py &

运行后执行下面的命令测试一下,如果部署成功,将输出 ok

Terminal window
curl 127.0.0.1:9999

5. 设置 Webhook#

进入 Gitee 的项目,点击 管理-> webhooks -> 添加Webhook

输入基础信息并保存。

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

6. 开始使用#

在使用 git push 上传代码后,稍等片刻,即可看到网站已更新。

代码#

auto_deploy.py

import os
import shutil
import subprocess
from flask import Flask, request, jsonify, after_this_request
import filecmp
import multiprocessing
import time
import hmac
import hashlib
import base64
import os
# 配置项
# 项目工作目录
WORKING_DIR = "/www/wwwroot/CLoudSirLab-Blog/blog-cloud-site"
# 网站部署目录
DEPLOY_DIR = "/www/wwwroot/cloudsir.top"
# 默认TOKEN
DEFAULT_SECRET_TOKEN = "test"
DEPLOYING = False
process_handler = None
retry = 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/
作者
CloudSir
发布于
2026-01-11
许可协议
CC BY-NC-SA 4.0