GitHub Webhook 服务,自动化部署多个开源项目的静态文档站(VuePress / VitePress 等) 到 nginx 目录。支持原子切换、tar.gz 备份保留、HMAC 签名校验、per-project 并发互斥。
- 一个端点,多个项目 —— 通过
?project=...&branch=...路由 - 原子切换 ——
nginx_dir被 rename 成.old,新文件解压到.staging,再一次性 rename 切换。失败时.old自动还原 - 备份 —— 每次部署把当前
nginx_dir打成tar.gz归档(保留数量可配置) - 安全 —— GitHub HMAC-SHA256 签名校验,常量时间比较防时序攻击
- 可观测 —— slog 结构化日志,同时输出到 stdout(journald 收集)和按天切分的文件(
/health和/status端点) - 恢复 —— 启动时自动清理残留的
.staging/.old与过期日志文件 - 私有仓库 ——
auth段配 PAT 或 SSH 私钥即可拉私有 GitHub 仓库;URL 含凭据时日志自动脱敏
本地快速构建:
go build -o mica-docs-deploy .
# 二进制在当前目录 mica-docs-deployLinux x86_64 静态构建(产物可丢到任何 glibc/musl 发行版直接跑,免装运行时;推荐部署用):
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-trimpath \
-tags netgo,osusergo \
-ldflags '-s -w -extldflags "-static"' \
-o mica-docs-deploy .
# 验证确实是静态链接(输出含 "statically linked")
file mica-docs-deploy
# mica-docs-deploy: ELF 64-bit LSB executable, x86-64, statically linked, Go Go1.x, not stripped参数说明:
CGO_ENABLED=0+netgo,osusergo—— 避免依赖系统的 glibc / libnss / libpam,纯 Go 实现 DNS 与 user 查询-trimpath—— 去掉二进制里的本地路径,构建可重现、泄露不了开发机目录-ldflags '-s -w'—— 去掉符号表与 DWARF,体积更小(~10MB → ~7MB)-extldflags "-static"—— 链接器参数,强制静态链接,生成 fully static ELF
如果只在部署机上编译,去掉
GOOS=linux GOARCH=amd64即可;macOS 上交叉编译默认 cgo 会用本机 Clang,去掉-extldflags "-static"也能跑(macOS 不需要 fully static)。
sudo mkdir -p /etc/mica-docs-deploy
sudo cp config.example.yaml /etc/mica-docs-deploy/config.yaml
sudo vi /etc/mica-docs-deploy/config.yaml
sudo chmod 600 /etc/mica-docs-deploy/config.yaml字段说明见「配置说明」,完整示例见下方「config.yaml 配置示例」。
sudo useradd --system --shell /usr/sbin/nologin mica-docs
sudo mkdir -p /var/lib/mica-docs-deploy/{repos,status,logs}
sudo chown -R mica-docs:mica-docs /var/lib/mica-docs-deploy
sudo mkdir -p /var/www/mica-mqtt-docs
sudo chown mica-docs:mica-docs /var/www/mica-mqtt-docssudo cp mica-docs-deploy /usr/local/bin/
sudo cp systemd/mica-docs-deploy.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now mica-docs-deploy
sudo systemctl status mica-docs-deploy在你的 GitHub 仓库 → Settings → Webhooks → Add webhook:
- Payload URL:
http://your-host:8080/webhook/github?project=mica-mqtt-docs&branch=dist - Content type:
application/json - Secret: 与
config.yaml里 secret 字段相同 - Events: 只勾选 "push event"
在源仓库加 .github/workflows/deploy-docs.yml:
name: 部署文档
on:
push:
branches:
- main
permissions:
contents: write
jobs:
deploy-gh-pages:
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: 设置 pnpm
uses: pnpm/setup@v1
with:
version: 11
runtime: node@22
cache: true
- name: 安装依赖
run: pnpm install --frozen-lockfile
- name: 构建文档
env:
NODE_OPTIONS: --max_old_space_size=8192
run: |-
pnpm run build
> src/.vuepress/dist/.nojekyll
- name: 部署文档
uses: JamesIves/github-pages-deploy-action@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
# 部署文档
branch: dist
folder: src/.vuepress/dist配置文件为 YAML 格式(模板见 config.example.yaml),分为三部分:server、backup、projects。
config.yaml 配置示例:
# 服务端配置
server:
listen: "0.0.0.0:8080" # HTTP 监听地址(GitHub Webhook 推送目标)
data_dir: "/var/lib/mica-docs-deploy" # 内部数据目录(repos/status/logs 子目录)
log_level: "info" # 日志级别:debug / info / warn / error
# log_dir: "/var/log/mica-docs-deploy" # 日志文件目录;空则默认 <data_dir>/logs
# log_keep_days: 7 # 按天日志文件保留天数;<=0 表示不清理
# 备份全局默认值(可被项目级 keep 覆盖)
backup:
default_keep: 5 # 每个项目默认保留最近 5 份 tar.gz 备份
# 项目列表:每个开源文档站一个条目
projects:
- name: "mica-mqtt-docs" # 项目名(唯一,webhook 用 ?project= 匹配)
repo_url: "https://github.com/dromara/mica-mqtt-docs.git" # 公开仓库地址
branch: "dist" # 目标分支(存放构建产物)
nginx_dir: "/var/www/mica-mqtt-docs" # nginx 站点根目录
secret: "github-webhook-secret-1" # Webhook Secret(≥16 字符)
keep: 10 # 可选:覆盖默认保留份数
backup_dir: "/var/backups/mica-mqtt-docs" # 可选:自定义备份目录;默认 <data_dir>/backups/<project>,勿放站点根目录内(避免外网可访问)
# ----- 私有仓库示例 -----
# 在项目下加 auth 段即可拉私有 GitHub 仓库。两类认证:
# type: token —— GitHub PAT(推荐 fine-grained,只授 contents: read)
# type: ssh_key —— 私钥文件(绝对路径;加密时填 ssh_key_password)
# 不配 auth = 公开仓库,行为不变。
- name: "internal-secret-docs"
repo_url: "https://github.com/your-org/secret-docs.git" # 也可写 git@github.com:your-org/secret-docs.git
branch: "dist"
nginx_dir: "/var/www/internal-secret-docs"
secret: "another-webhook-secret-16+chars"
auth:
type: "token"
token: "ghp_xxxxxxxxxxxxxxxxxxxx"校验规则: 启动时校验所有配置,错误一次性全部列出(每个错误一行),校验失败直接退出。路径会做规范化处理(去尾斜杠、解析 ..)。
auth 字段说明:
| 字段 | 必填 | 说明 |
|---|---|---|
type |
是 | token(HTTPS + PAT) 或 ssh_key(SSH 私钥) |
token |
type=token 时必填 | GitHub PAT。Username 固定为 x-access-token(GitHub 约定) |
ssh_key |
type=ssh_key 时必填 | 私钥文件绝对路径,建议 chmod 600,owner 与服务运行用户一致 |
ssh_key_password |
否 | 加密私钥的 passphrase |
GitHub 端配套:
- Token:推荐用 Fine-grained PAT,只勾选目标仓库的 Contents: Read-only 权限。旧式 classic token 用
repo范围。 - Deploy Key:在私有仓库 → Settings → Deploy keys → Add key,Allow read only,把公钥贴进去;
ssh_key字段填对应的私钥路径。 - Webhook:私有仓库的 webhook 推送不需要仓库读权限——它是 GitHub → 你的服务方向,跟拉取方向无关。Secret 照常配。
一次 webhook 触发后,服务按以下顺序执行(任一步失败可回滚,nginx_dir 不会半新半旧):
- 备份 —— 把当前
nginx_dir打成tar.gz(命名YYYY-MM-DDTHHMMSSZ.tar.gz),并清理超出保留份数的旧备份 - 拉取 —— go-git 从
repo_url建立 bare 缓存仓库并fetch目标分支 - 导出 —— 把分支的 commit tree 导出到
<nginx_dir>.staging - 原子切换 ——
nginx_dirrename 成.old,stagingrename 成nginx_dir;任一步失败自动回滚 - 清理 —— 删除
.old,记录部署状态(commit / 耗时)到内存与status/目录
同一项目并发 webhook 会串行执行(信号量锁,等待上限 lock_timeout_secs);不同项目互不影响。
/status 返回每个项目最近一次部署结果,字段:
| 字段 | 说明 |
|---|---|
last_deploy_at |
最近一次部署时间(RFC3339 UTC) |
last_commit |
部署的 commit SHA(40 位) |
last_status |
Success / Failed / Pending |
last_duration_ms |
部署耗时(毫秒) |
last_error |
失败时的错误信息 |
recovered_at |
启动恢复完成时间(如有) |
日志同时输出到 stdout(systemd 收集到 journald)和按天切分的文件:
- 目录:默认
<data_dir>/logs,可通过server.log_dir自定义 - 文件名:
mica-docs-deploy.YYYY-MM-DD.log(日期用 UTC,方便多机一致) - 保留:默认 7 天,
<= 0表示永久保留;启动时清理过期文件
# 实时看文件
tail -f /var/lib/mica-docs-deploy/logs/mica-docs-deploy.$(date -u +%F).log
# 看 systemd 收集的(与上面同一份内容)
journalctl -u mica-docs-deploy -f| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/webhook/github?project=X&branch=Y |
Webhook 入口 |
GET |
/health |
存活探针,返回 ok |
GET |
/status |
所有项目最近部署状态(JSON) |
GET |
/status/{project} |
单个项目状态(未部署过 → 404) |
| 状态码 | 含义 | Body |
|---|---|---|
| 202 | 已入队,部署在后台执行 | {"status":"queued","project":"...","branch":"..."} |
| 400 | 请求头/参数不合法(非 GitHub User-Agent、非 push 事件、branch 或 ref 不匹配) | {"error":"bad_request","detail":"..."} |
| 401 | HMAC 签名校验失败 | {"error":"invalid_signature","detail":"..."} |
| 404 | 未知 project | {"error":"unknown_project","detail":"..."} |
| 503 | 锁等待超时(同项目部署进行中) | {"error":"queue_timeout","detail":"..."} |
| 500 | 内部错误 | {"error":"internal","detail":"..."} |
如果部署中途被 kill,下次启动时本服务会:
- 删除残留的
<nginx_dir>.staging(永远安全) - 把
<nginx_dir>.old自动还原成<nginx_dir>(当 nginx 不存在时) - 如果
nginx_dir和.old同时存在 → 记录 ERROR 保留现场,等人工介入
从备份手动恢复:
tar -xzf /var/lib/mica-docs-deploy/backups/mica-mqtt-docs/2026-08-17T103000Z.tar.gz -C /var/www/mica-mqtt-docs --strip-components=1