想象一下:你在本地写完代码,git push 敲下去,服务器上自动 git pull,项目重启,前端页面上的版本号自动更新。全程不用碰服务器,爽不爽?这篇文章手把手教你搞定!
整体流程概览
① 本地开发
写代码 → git commit → git push
② GitHub
收到 push → 触发 Webhook
③ 服务器
Webhook 接口收到请求 → git pull → 重启服务
④ 前端展示
Django 读取 Git 版本号 → 渲染到页面底部
第一步:本地开发环境准备
1.1 初始化 Git 仓库并关联 GitHub
# 进入你的 Django 项目目录 cd /path/to/your/project # 初始化 git(如果还没有) git init # 添加远程仓库 git remote add origin git@github.com:yourname/yourproject.git # 首次推送 git add . git commit -m "🎉 初始化项目" git push -u origin main
推荐使用 SSH 方式连接 GitHub,后续服务器自动 pull 更安全。如果你还没配 SSH key:
# 本地生成密钥 ssh-keygen -t ed25519 -C "your@email.com" # 复制公钥 cat ~/.ssh/id_ed25519.pub # 粘贴到 GitHub → Settings → SSH and GPG keys → New SSH key
第二步:GitHub 配置 Webhook
2.1 创建 Webhook
进入你的 GitHub 仓库 → Settings → Webhooks → Add webhook
| 配置项 | 填写内容 | 说明 |
| Payload URL | https://your-server.com/webhook/deploy/ | 服务器上接收 webhook 的接口地址 |
| Content type | application/json | 选 JSON 格式 |
| Secret | 一个随机字符串(如 openssl rand -hex 32 生成) | 用于签名验证,防止伪造请求 |
| Which events? | Just the push event | 只有 push 时才触发自动部署 |
2.2 Webhook 安全:签名验证原理
为什么需要 Secret?
没有签名验证的话,任何人都可以往你的接口发请求,相当于把服务器大门敞开。GitHub 会在请求头里带上用 Secret 计算的 HMAC-SHA256 签名,服务器收到后自己也算一遍,对上了才执行 pull。
import hmac
import hashlib
WEBHOOK_SECRET = b"your-random-secret-here"
def verify_signature(payload_body: bytes, signature_header: str) -> bool:
"""验证 GitHub Webhook 签名"""
if not signature_header:
return False
hash_hex = hmac.new(
WEBHOOK_SECRET,
msg=payload_body,
digestmod=hashlib.sha256
).hexdigest()
expected = f"sha256={hash_hex}"
return hmac.compare_digest(expected, signature_header)
第三步:服务器接收 Webhook + 自动部署
3.1 Django 视图处理 Webhook
import json
import hmac
import hashlib
import subprocess
import os
from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
PROJECT_DIR = "/www/yourproject"
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "").encode()
@csrf_exempt
@require_POST
def github_webhook(request):
"""接收 GitHub push 事件,自动 git pull"""
# 1. 验证签名
signature = request.headers.get("X-Hub-Signature-256", "")
if not verify_signature(request.body, signature):
return HttpResponseForbidden("Invalid signature")
# 2. 解析 payload 判断分支
try:
payload = json.loads(request.body)
branch = payload.get("ref", "")
except json.JSONDecodeError:
return HttpResponse("Invalid JSON", status=400)
# 3. 只处理 main 分支的 push
if branch != "refs/heads/main":
return HttpResponse(f"Ignored branch: {branch}")
# 4. 执行 git pull
try:
output = subprocess.check_output(
["git", "-C", PROJECT_DIR, "pull", "origin", "main"],
stderr=subprocess.STDOUT,
timeout=30
).decode()
except subprocess.CalledProcessError as e:
return HttpResponse(f"Git pull failed: {e.output.decode()}", status=500)
# 5. 重启 Gunicorn / 收集静态文件
subprocess.run(["systemctl", "restart", "yourproject-gunicorn"])
return HttpResponse(f"Deployed! \n{output}")
def verify_signature(body: bytes, signature: str) -> bool:
if not signature:
return False
expected = "sha256=" + hmac.new(
WEBHOOK_SECRET, body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
3.2 配置 URL 路由
from django.urls import path
from . import views
urlpatterns = [
path("webhook/deploy/", views.github_webhook, name="github-webhook"),
]
3.3 服务器环境变量设置
不要把 Secret 写死在代码里!
用环境变量或 .env 文件配合 python-decouple / django-environ 来管理。
# 在 /etc/environment 或 ~/.bashrc 中 export WEBHOOK_SECRET="your-random-secret-from-step-2"
3.4 别忘了:服务器也要配 SSH Key
服务器需要能免密 pull 你的 GitHub 仓库:
# 生成 deploy key ssh-keygen -t ed25519 -C "deploy@your-server" # 把公钥添加到 GitHub 仓库的 Deploy Keys # GitHub → Settings → Deploy keys → Add deploy key cat ~/.ssh/id_ed25519.pub
第四步:Django 读取 Git 版本号并显示在前端
4.1 生成版本号文件
创建一个工具函数,从 Git 标签和提交哈希生成语义化版本号:
import subprocess
import os
from pathlib import Path
def get_git_version() -> str:
"""从 Git 仓库获取版本号,格式: v1.2.3-4-gabc1234"""
project_dir = Path(__file__).resolve().parent.parent # Django 根目录
try:
# git describe: 生成类似 v1.0-3-gab12cd 的版本号
tag = subprocess.check_output(
["git", "-C", str(project_dir), "describe", "--tags", "--always",
"--dirty=-dirty"],
stderr=subprocess.DEVNULL,
timeout=5
).decode().strip()
return tag
except (subprocess.CalledProcessError, FileNotFoundError):
return "unknown"
def get_build_info() -> dict:
"""获取构建信息"""
project_dir = Path(__file__).resolve().parent.parent
info = {"version": "unknown", "branch": "unknown", "commit": "unknown"}
try:
info["version"] = subprocess.check_output(
["git", "-C", str(project_dir), "describe", "--tags", "--always"],
stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
pass
try:
info["branch"] = subprocess.check_output(
["git", "-C", str(project_dir), "rev-parse", "--abbrev-ref", "HEAD"],
stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
pass
try:
info["commit"] = subprocess.check_output(
["git", "-C", str(project_dir), "rev-parse", "--short", "HEAD"],
stderr=subprocess.DEVNULL
).decode().strip()
except Exception:
pass
return info
4.2 创建 Context Processor 注入版本号
from .utils.version import get_build_info
def version_context(request):
"""将版本号注入到所有模板的 context 中"""
info = get_build_info()
return {
"APP_VERSION": info["version"],
"GIT_BRANCH": info["branch"],
"GIT_COMMIT": info["commit"],
}
4.3 注册 Context Processor
TEMPLATES = [
{
# ... 其他配置 ...
"OPTIONS": {
"context_processors": [
# ... Django 默认的 processors ...
"yourproject.context_processors.version_context", # ← 新增
],
},
},
]
4.4 前端模板中显示版本号
<footer class="footer">
<div class="container">
<span title="Branch: {{ GIT_BRANCH }} | Commit: {{ GIT_COMMIT }}">
🏷️ {{ APP_VERSION }}
</span>
</div>
</footer>
4.5 进阶:写入版本文件避免运行时依赖 Git
性能优化小技巧:每次请求都调用 git describe 会有 IO 开销。推荐在部署时把版本号写入一个文件,Django 启动时读取一次就够了。
# 写入版本文件
git describe --tags --always > /www/yourproject/VERSION
from pathlib import Path
def get_version() -> str:
version_file = Path(__file__).resolve().parent.parent / "VERSION"
if version_file.exists():
return version_file.read_text().strip()
return "unknown"
第五步:完整的 Webhook 部署脚本
如果想用独立的部署脚本而不是 Django 视图,可以用 Flask / FastAPI 做个微型服务,或者写一个 shell 脚本 + nginx 代理:
import hmac
import hashlib
import json
import subprocess
import os
from http.server import HTTPServer, BaseHTTPRequestHandler
SECRET = os.environ["WEBHOOK_SECRET"].encode()
PROJECT_DIR = "/www/yourproject"
class DeployHandler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/webhook/deploy/":
self.send_error(404)
return
body = self.rfile.read(int(self.headers["Content-Length"]))
# 验证签名
sig = self.headers.get("X-Hub-Signature-256", "")
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
self.send_error(403)
return
# 解析事件类型
payload = json.loads(body)
ref = payload.get("ref", "")
if ref != "refs/heads/main":
self.send_response(200)
self.end_headers()
self.wfile.write(f"Ignored {ref}".encode())
return
# 拉代码
result = subprocess.run(
["git", "-C", PROJECT_DIR, "pull"],
capture_output=True, text=True, timeout=30
)
# 写版本号
subprocess.run(
f"git -C {PROJECT_DIR} describe --tags --always > {PROJECT_DIR}/VERSION",
shell=True
)
# 重启服务
subprocess.run(["systemctl", "restart", "yourproject-gunicorn"])
self.send_response(200 if result.returncode == 0 else 500)
self.end_headers()
self.wfile.write(result.stdout.encode())
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", 9000), DeployHandler)
print("🚀 Deploy server on :9000")
server.serve_forever()
用 systemd 管理这个服务,再用 nginx 反向代理到 :9000,就相当于一个独立的 Webhook 服务。
最终效果
到此,整个流程就打通了:
- 你在本地改完代码 →
git commit -m "修复登录bug"→git push - GitHub 收到 push → 调用 Webhook 接口
- 服务器收到请求 → 验证签名 →
git pull→ 写版本文件 → 重启 Gunicorn - 用户刷新页面 → Django 读取版本号 → 页面底部显示
v1.5.3-2-ga8f12cd
一行命令,全自动搞定。你只管写代码,剩下的交给 Webhook。
常见问题排查
| 问题 | 解决方法 |
| Webhook 不触发 | 去 GitHub → Settings → Webhooks → Recent Deliveries 查看发送历史和响应内容 |
| 签名验证失败 | 确认服务器上的 SECRET 和 GitHub 配置的完全一致(注意末尾换行符) |
| git pull 无权限 | 服务器上的 SSH key 是否已添加为 Deploy Key?用 ssh -T git@github.com 测试 |
| Gunicorn 重启失败 | 用 journalctl -u yourproject-gunicorn -n 50 查看日志 |
| 版本号显示 unknown | 服务器上有没有 .git 目录?是否用 git clone 而非直接复制文件? |
| git pull 有本地修改冲突 | 在 pull 前加 git reset --hard && git clean -fd(注意会丢掉服务器上未提交的修改!) |