

前阵子,我看到有人在吐槽自己写博客很麻烦——写麻烦、托管麻烦,推完 GitHub 之后还得去服务器上手动拉代码部署。虽然我以前也在做类似的东西,但他这么一提让我想起这件事,我就想着,不如把自己一直随便用的小玩意整理一下,顺便分享给大家。
于是就有了这个小项目:HookExec,一个用 Go 写的 GitHub Webhook 脚本执行器。推完代码,服务器自己去拉取、构建、重启,你只管写。
什么是 Webhook#
Webhook 本质上就是 GitHub 在某些操作发生时,向你指定的地址发送一个网络通知。最常见的事件包括 push、pull_request、release 等。收到通知之后,你就可以根据事件类型做一些操作,比如自动部署代码、触发测试、更新文档等等。
设置 Webhook 其实也很简单:在 GitHub 仓库里找到 Settings → Webhooks → Add webhook,填入你服务器的地址和一个 Secret,选中你想监听的事件,然后 GitHub 就会在这些事件发生时发送 POST 请求到你的服务器。只要你的服务端能接收请求、校验 Secret,再执行对应操作,就完成了。
我以前一直随便把 Webhook 放在某个在跑的服务里处理,所以一直没有好好整理过。Webhook 的核心其实不复杂,它需要的只是一个接口去接收请求,这也是之前一直随手搞的原因。
为什么用 Go 写#
Go 写这种小工具特别合适:
- 编译后就是一个二进制文件,直接丢到服务器上运行就行,不依赖其他环境。
- 运行开销小,启动快,非常适合长期驻留监听请求。
- 写起来简单,核心逻辑就是解析 HTTP 请求、验证签名、匹配事件和仓库,然后执行脚本。
它长什么样#
一个二进制、一个 SQLite 文件、一个脚本目录:
/opt/hookexec/
├── hook-exec ← 单个可执行文件
├── hook-exec.db ← SQLite,存配置和执行记录
└── shell/ ← 你的脚本放这里
└── blog.shplaintext没有外部依赖,不需要 Node.js,也不会从外网拉任何东西。服务器在内网也照样跑得起来。
管理页在 /web,打开是这样:

每个项目一张卡片,关联的仓库、脚本文件、工作目录和超时时间都写在上面,右下角是最近一次执行的结果。要加一个站,点「新建项目」把仓库和脚本关联起来就行:

执行记录在另一个标签页,点开任意一条,能看到这次投递的完整信息,以及脚本跑出来的完整输出:

我手上有一堆博客、文档站和小服务,分散在不同仓库里。让每个服务自己监听自己的 webhook 并不是做不到,但每加一个站就要重复一遍「起个小 HTTP 服务 + 验签 + 解析 payload + 调部署命令」,而且这些逻辑散在各处,出没出问题得一个个去看。
HookExec 把这件事收敛成一个常驻服务:所有仓库的 webhook 都打到它这里,它按 owner/repo 找到对应的脚本执行。加一个新站就是「新建一个项目 + 传一个脚本」,不用再动服务本身。
一个刻意的设计:脚本不由它创建#
这里要说一下我这次特意坚持的一点:网页上不能编写或修改脚本。
可执行的内容全部来自 <数据目录>/shell/,只能由你自己通过 scp / ansible / git 传上去。网页端能做的只是「从服务器上已有的脚本里挑一个执行」。
这样一来,就算管理页的密码泄露了,对方也只能在你放上去的脚本里挑一个跑,而不是能在你的服务器上执行任意命令。
脚本路径的校验我收在了同一个包里:拼出来的路径必须落在脚本目录内(软链解掉之后再判一次)、必须是普通文件,而且每次执行前都重新校验,不信任数据库里存的值——即使有人直接改库、或者导入一份恶意配置,也逃不出脚本目录。
当然,管理页的凭证仍然是服务器级别的机密,记得通过 HTTPS 反代暴露,不要直接裸奔在公网上。
跑起来#
git clone https://github.com/zxc7563598/hook-exec && cd hook-exec
make build
./hook-exec --listen 127.0.0.1:9000 --data-dir /opt/hookexecbash第一次启动会把数据目录和 shell/ 建好。没给密码的话会随机生成一个打印出来,只显示这一次,记得存下来。
然后:
- 打开
http://127.0.0.1:9000/web登录管理页 - 把脚本放进
<数据目录>/shell/并chmod +x,点「重新扫描」 - 新建项目,把仓库和脚本关联起来
- 到 GitHub 仓库的 Settings → Webhooks 填上你的地址,Secret 填和项目里一致的那个
配好之后 GitHub 会立刻发一个 ping 做连通性检查,HookExec 回 200 并且什么都不做,就说明通了。
我的部署脚本#
我博客的部署脚本大概是这样,放在 /opt/hookexec/shell/blog.sh:
#!/bin/bash
set -euo pipefail
PROJECT_DIR="/opt/blog"
GIT="/usr/bin/git"
BUN="/usr/bin/bun"
PM2="/usr/bin/pm2"
export HOME="/root"
export PM2_HOME="/root/.pm2"
log() { echo "[deploy] $1"; }
log "开始部署 Astro Blog"
log "仓库 $HOOK_REPO 分支 $HOOK_BRANCH 提交 $HOOK_SHA"
cd "$PROJECT_DIR"
log "拉取最新代码..."
$GIT fetch origin
$GIT reset --hard "${HOOK_SHA:-origin/main}"
log "安装依赖(Bun)..."
$BUN install
log "构建 Astro 项目..."
$BUN run build
log "重启 PM2 进程..."
$PM2 restart "$PROJECT_DIR/ecosystem.config.cjs"
log "部署完成"bash注意脚本是直接执行的,靠 shebang 决定解释器,不需要(也不能)在配置里写 sh 前缀。所以要记得 chmod +x,没有可执行位是跑不起来的,页面上会标黄提醒你,而不是等执行的时候抛一个晦涩的 permission denied。
脚本需要的信息全部通过环境变量传进来,常用的有这些:
HOOK_REPO——owner/repoHOOK_EVENT——push/pull_request/release…HOOK_BRANCH、HOOK_REF—— 分支名、refs/heads/mainHOOK_SHA—— 提交 SHAHOOK_ACTOR—— 触发者HOOK_PAYLOAD_FILE—— 完整 payload 的临时文件路径,要取深层字段直接jq它就行
另外也提供了一套 GitHub Actions 风格的同名变量(GITHUB_REPOSITORY、GITHUB_SHA、GITHUB_REF_NAME……),现成的部署脚本改一下就能用。
有了这个流程,我写博客只管写,编辑器里 push 完就完事了。
如果你也想试试#
项目在 GitHub 上:HookExec ↗。它轻量、零依赖,上手也快。如果你也想搞个自动部署,可以直接拿去试试,不用在意具体实现,看看 README,或者直接在 releases 里下载对应平台的包,解压就能用。
有问题或者想法,欢迎直接在 GitHub 上交流。