本文档用于指导你实现一个「GitLab Webhook 触发自动部署」系统,并在页面中图形化展示部署全过程。
- 当 GitLab 仓库发生
push(例如main分支)时,自动触发部署。 - 后端记录并实时更新部署步骤状态。
- 前端以可视化流程图 + 日志时间线方式展示部署过程。
- GitLab:发送 Webhook 事件。
- Hook Server(推荐 FastAPI):接收事件、验签、入队。
- Deploy Worker:执行部署脚本(拉代码、构建、发布、健康检查)。
- Status Store(Redis/DB):保存任务状态。
- Dashboard(前端):通过 SSE/WebSocket 订阅状态并图形化展示。
flowchart LR
A[GitLab Push Event] --> B[Webhook Endpoint /api/hook/gitlab]
B --> C{Token 校验/分支过滤}
C -- 通过 --> D[创建 Deploy Task]
C -- 拒绝 --> X[返回 401/忽略]
D --> E[Worker 执行部署]
E --> E1[拉取代码]
E1 --> E2[安装依赖/构建]
E2 --> E3[发布到目标环境]
E3 --> E4[健康检查]
E4 --> F{是否成功}
F -- 成功 --> G[更新状态 Success]
F -- 失败 --> H[回滚]
H --> I[更新状态 Failed]
G --> J[Dashboard 实时展示]
I --> J
sequenceDiagram
participant GL as GitLab
participant API as Hook Server
participant Q as Queue
participant WK as Deploy Worker
participant UI as Dashboard
GL->>API: POST Webhook (push)
API->>API: 校验 X-Gitlab-Token
API->>Q: 入队 deploy_job
API-->>GL: 200 OK
WK->>Q: 拉取任务
WK->>WK: 执行部署步骤
WK-->>API: 上报步骤状态
API-->>UI: SSE/WebSocket 推送状态
UI->>UI: 更新流程节点颜色与日志
在 GitLab 项目中设置:
Settings -> WebhooksURL:https://your-domain.com/api/hook/gitlabSecret Token: 与服务端环境变量一致(如GITLAB_WEBHOOK_SECRET)- 勾选事件:
Push events - 可选:只处理
main/release/*分支
可以通过环境变量指定 Hook 拉取代码的落盘目录:
DEPLOY_BASE_DIR=/data/deploy/reposGITLAB_WEBHOOK_SECRET=your-secret
建议目录规则:
${DEPLOY_BASE_DIR}/{project_path}/{branch}- 示例:
/data/deploy/repos/group-app/main
注意:
project_path和branch需要做字符清洗,避免路径穿越和非法字符。
FastAPI 最小实现示例:
from pathlib import Path
import os
import re
import subprocess
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
DEPLOY_BASE_DIR = Path(os.getenv("DEPLOY_BASE_DIR", "/data/deploy/repos"))
GITLAB_WEBHOOK_SECRET = os.getenv("GITLAB_WEBHOOK_SECRET", "")
def normalize_segment(value: str) -> str:
value = value.strip().replace("/", "-")
return re.sub(r"[^a-zA-Z0-9._-]", "-", value)[:120] or "unknown"
def build_repo_dir(project_path: str, branch: str) -> Path:
safe_project = normalize_segment(project_path)
safe_branch = normalize_segment(branch)
return DEPLOY_BASE_DIR / safe_project / safe_branch
@app.post("/api/hook/gitlab")
async def gitlab_hook(
request: Request,
x_gitlab_token: str | None = Header(default=None),
):
if not GITLAB_WEBHOOK_SECRET or x_gitlab_token != GITLAB_WEBHOOK_SECRET:
raise HTTPException(status_code=401, detail="invalid token")
payload = await request.json()
ref = payload.get("ref", "")
branch = ref.replace("refs/heads/", "")
project = payload.get("project", {})
project_path = project.get("path_with_namespace", "unknown-project")
repo_url = project.get("git_http_url") or project.get("git_ssh_url")
if not repo_url:
raise HTTPException(status_code=400, detail="missing repository url")
repo_dir = build_repo_dir(project_path, branch)
repo_dir.parent.mkdir(parents=True, exist_ok=True)
if (repo_dir / ".git").exists():
subprocess.run(["git", "-C", str(repo_dir), "fetch", "--all"], check=True)
subprocess.run(["git", "-C", str(repo_dir), "checkout", branch], check=True)
subprocess.run(["git", "-C", str(repo_dir), "pull", "--ff-only"], check=True)
else:
subprocess.run(["git", "clone", "-b", branch, repo_url, str(repo_dir)], check=True)
return {
"ok": True,
"repo_dir": str(repo_dir),
"project_path": project_path,
"branch": branch,
}POST /api/hook/gitlab:接收 GitLab HookGET /api/deploy/:id:查询任务详情GET /api/deploy/:id/stream:SSE 推送任务状态
任务状态可使用:
pendingrunningsuccessfailedrolled_back
步骤状态示例:
checkoutbuildreleasehealth_checkrollback
- 必须校验
X-Gitlab-Token。 - 仅允许受控分支触发部署。
- 增加重复事件幂等处理(
X-Gitlab-Event-UUID)。 - 部署命令在最小权限账户下执行。
- 对目录名做严格字符清洗,禁止直接拼接原始分支名/项目名。
- 顶部:任务基础信息(分支、提交人、提交 SHA、触发时间)
- 中部:流程节点图(每个步骤一个节点)
- 底部:实时日志流(按时间滚动)
pending:灰色running:蓝色(可加脉冲动画)success:绿色failed:红色rolled_back:橙色
优先使用 SSE:
- 服务端持续推送步骤状态与日志。
- 前端收到事件后,更新节点状态与进度条。
将部署流程拆分为独立步骤,便于可视化和重试:
- 代码拉取/切换版本
- 安装依赖与构建
- 发布(容器重建或服务重启)
- 健康检查
- 失败时自动回滚
- 打通 GitLab Hook 到后端接收。
- 任务可入队并执行假部署流程(sleep 模拟)。
- 前端可看到步骤状态从
pending -> running -> success/failed。 - 接入真实部署命令与回滚逻辑。
cp .env.example .env
uv sync
uv run uvicorn app.main:app --reload --port 8000pnpm install
pnpm devcurl -X POST 'http://127.0.0.1:8000/api/hook/gitlab' \
-H 'Content-Type: application/json' \
-H 'X-Gitlab-Event: Push Hook' \
-H 'X-Gitlab-Token: replace-with-your-secret' \
-H 'X-Gitlab-Event-UUID: local-test-001' \
-d '{
"ref": "refs/heads/main",
"after": "abc123",
"project": {
"path_with_namespace": "demo-group/demo-app",
"git_http_url": "https://gitlab.example.com/demo-group/demo-app.git"
}
}'任务状态查询:
curl 'http://127.0.0.1:8000/api/deploy/<task_id>'任务流式事件:
curl -N 'http://127.0.0.1:8000/api/deploy/<task_id>/stream'已提供脚本:
scripts/gitlab_bulk_setup.py
脚本能力:
- 分页获取账号可访问的全部项目(
membership=true) - 按
path_with_namespace拉取到本地目录(已存在仓库自动fetch/pull) - 为每个项目创建或更新同一个 Webhook
推荐使用 GITLAB_TOKEN(Personal Access Token):
export GITLAB_BASE_URL='https://git.cloud2go.cn'
export GITLAB_TOKEN='your-token'
export GITLAB_VERIFY_SSL='true'
# 若为自签名证书,建议使用 CA 文件而不是关闭校验
# export GITLAB_CA_CERT='/path/to/ca.pem'
export CLONE_BASE_DIR='/data/gitlab/repos'
export HOOK_URL='https://your-domain.com/api/hook/gitlab'
export HOOK_TOKEN='replace-with-your-secret'
export HOOK_BRANCH_FILTER='main'如需兼容账号密码(不推荐):
export GITLAB_BASE_URL='https://git.cloud2go.cn'
export GITLAB_USERNAME='your-account'
export GITLAB_PASSWORD='your-password'
export GITLAB_VERIFY_SSL='false'若执行时提示
401 Unauthorized,说明实例禁用了账号密码 API 鉴权,请改用GITLAB_TOKEN。
cd /Users/hikaru/Desktop/projects/auto
DRY_RUN=true uv run python scripts/gitlab_bulk_setup.pycd /Users/hikaru/Desktop/projects/auto
DRY_RUN=false uv run python scripts/gitlab_bulk_setup.py# 只拉代码
SKIP_HOOK=true uv run python scripts/gitlab_bulk_setup.py
# 只配置 Hook
SKIP_CLONE=true uv run python scripts/gitlab_bulk_setup.py已内置页面:
GET /preview
页面能力:
- 调用
GET /api/gitlab/projects获取项目列表 - 下拉选择单个项目
- 输入 Hook URL / Token / 分支过滤后,调用
POST /api/gitlab/projects/{project_id}/hook完成配置
服务端需要预先配置 GitLab 凭据(推荐 Token):
export GITLAB_BASE_URL='https://git.cloud2go.cn'
export GITLAB_TOKEN='your-token'
export GITLAB_VERIFY_SSL='false' # 自签证书场景启动后访问:
http://127.0.0.1:8000/preview
如果你需要,我可以下一步按你的服务器地址直接执行一次批量配置,并回传成功/失败项目清单。