一个 webhook 触发的自动部署流水线

curl -X POST https://<WEBHOOK_HOST>/hooks/deploy -H "X-Signature: <SIGNATURE>" 可以作为自动部署的触发入口。

这篇只讲一个最小可用方案:代码仓库推送后,向自己的机器发送 webhook;机器验证签名;通过后执行部署脚本;脚本拉代码、构建、切换版本、做一次健康检查。重点是能跑、能回滚、重复执行不容易出问题。

流水线拆开看

这里的 webhook,可以简单理解成“外部系统一有动作,就向你发一个 HTTP 请求”。

最小链路只有四步:

  1. 代码仓库收到 push
  2. 仓库向 webhook 地址发请求
  3. 服务器验证请求确实来自仓库
  4. 服务器执行部署脚本

CI/CD 这个词很大,放到这里可以只理解成两段:

  • CI:有人提交代码后,自动做检查或构建
  • CD:检查通过后,自动部署到目标机器

如果只是个人项目或者低频更新,可以先把“检查”和“部署”分开,先打通部署链路。

webhook 服务别写复杂

如果已经有反向代理,挂一个很小的服务就够了。下面用 Node.js 做示例,重点只有两件事:验签、落日志。

一个最小的 webhook 接收器

js
import express from “express”;
import crypto from “crypto”;
import { execFile } from “child_process”;
import fs from “fs”;

const app = express();
const SECRET = process.env.WEBHOOK_SECRET || ““;

app.use(express.raw({ type: “/“ }));

function verifySignature(req) {
const sig = req.header(“X-Hub-Signature-256”) || “”;
const hmac =
“sha256=” +
crypto.createHmac(“sha256”, SECRET).update(req.body).digest(“hex”);

const sigBuf = Buffer.from(sig);
const hmacBuf = Buffer.from(hmac);

if (sigBuf.length !== hmacBuf.length) {
return false;
}

return crypto.timingSafeEqual(sigBuf, hmacBuf);
}

app.post(“/hooks/deploy”, (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send(“bad signature”);
}

fs.appendFileSync(
“/var/log/deploy-hook.log”,
${new Date().toISOString()} deploy\n
);

execFile(“/bin/bash”, [“/srv/app/deploy.sh”], (err, stdout, stderr) => {
fs.appendFileSync(
“/var/log/deploy-hook.log”,
(stdout || “”) + (stderr || “”) + (err ? \n${err.message}\n : “”)
);
});

res.send(“accepted”);
});

app.listen(9000);

这段代码里有几个点不能省:

  • 密钥只放环境变量,不写死在仓库
  • 验签必须做,否则任何人都能请求部署接口
  • 收到请求先快速返回,不要让 webhook 长时间等待
  • 日志单独落盘,出错时才知道卡在哪

部署脚本要可重复执行

部署脚本最怕第一次成功,第二次出问题。幂等可以简单理解为:同一个脚本跑两次,结果尽量一致。

一个偏保守的 deploy.sh

1
2
#!/usr/bin/env bash
set -euo pipefail

APP_DIR=”/srv/app”
REPO_DIR=”$APP_DIR/repo”
CURRENT_LINK=”$APP_DIR/current”
RELEASES_DIR=”$APP_DIR/releases”
STAMP=”$(date +%Y%m%d%H%M%S)”
NEW_RELEASE=”$RELEASES_DIR/$STAMP”

mkdir -p “$REPO_DIR” “$RELEASES_DIR”

if [ ! -d “$REPO_DIR/.git” ]; then
git clone “$REPO_DIR”
fi

cd “$REPO_DIR”
git fetch –all
git reset –hard origin/main

mkdir -p “$NEW_RELEASE”
rsync -a –delete –exclude “.git” “$REPO_DIR/“ “$NEW_RELEASE/“

cd “$NEW_RELEASE”
npm ci
npm run build

ln -sfn “$NEW_RELEASE” “$CURRENT_LINK”

systemctl restart app.service
sleep 2
curl -fsS >/dev/null

这份脚本刻意用了几个保守做法:

  • git reset --hard origin/main:每次都回到远端最新状态,避免本地脏文件影响部署
  • rsync 到新目录:不要在运行中的目录直接覆盖
  • ln -sfn 切软链接:切换版本接近瞬时,回滚也简单
  • 健康检查失败就退出:部署不是“服务重启了”就算完成

如果要回滚,也很直接:把 current 链接指回上一个 release,再重启服务。这个方案不复杂,出问题时也容易手动处理。

这几处最容易踩坑

1. webhook 收到太多次

一个 push 可能触发多类事件,或者短时间连续推送。最省事的办法是加锁,防止并发部署。

1
flock -n /tmp/deploy.lock /srv/app/deploy.sh

拿不到锁就直接退出,比两次部署互相覆盖安全。

2. “成功”只是脚本执行完

真正要看的不是命令返回 0,而是服务能不能对外响应。健康检查地址最好单独做;如果没有,也至少检查进程和端口。关于检查哪些依赖、返回哪些状态,本文未验证统一做法。

3. 日志太少

部署至少要记三类日志:

  • webhook 是否收到
  • 脚本执行到哪一步
  • 健康检查是否通过

不要只留一行 deploy success

用现成 CI,还是自己接 webhook

如果已经在用 GitHub Actions、GitLab CI 这类平台,也可以让它们通过 SSH 直接部署。自己接 webhook 的价值通常在这几种场景:

  • 想少一层平台配置
  • 机器在内网,通过反向代理暴露一个入口
  • 部署逻辑很本地化,脚本比平台配置更直观
  • 想完全掌握日志和回滚方式

代价也明确:安全、重试、并发控制、审计,很多事都要自己补。

一个够用的边界

如果这是公开服务,建议至少再加三道限制:

  • 反向代理只放行 POST /hooks/deploy
  • 限制来源地址;如果来源列表会变,先标注为“待确认”,不要写死未经验证的范围
  • webhook 进程和应用进程分离,别共用过高权限

另外,不要让 webhook 直接执行任意命令。固定调用一个脚本,比把参数从请求体透传进去安全得多。

下一步可以补什么

上面这套只解决了“触发后自动部署”。如果要继续完善,通常会补上测试、制品打包、灰度发布和自动回滚。先补哪一步,取决于当前最常见的失败点。


一个 webhook 触发的自动部署流水线
https://ghost.kasumi.live/2026/04/27/一个 webhook 触发的自动部署流水线/
作者
Amadeus
发布于
2026年4月27日
许可协议