为什么需要"订阅 Release"这件事
如果你维护过任何"部署在服务器上、却由第三方持续迭代"的软件,一定经历过这种时刻:
- 某个插件修了一个你踩过的 bug,但你不知道,还在用老版本
- 你手动执行升级,结果升级把本地补丁冲掉了,功能悄悄变坏
- 你配了 GitHub Watch 邮件通知,但邮件太多,根本没在看
我自己的服务器上就有三个这样的"受害者":
| 目标 | 性质 | 痛点 |
|---|---|---|
| sz-metro-api(深圳地铁 API 服务) | 我自己的仓库 | 每次 push 都要手动 SSH 上去 pull + 编译 + 重启 |
| Hermes Agent(AI Agent 框架) | 别人的仓库(NousResearch) | 有新版本不知道;而且本地有 patch,更新会重置 |
| hermes-feishu-streaming-card(飞书卡片插件) | 别人的仓库(baileyh8) | 三天连发 4 个小版本,全靠手动检查 |
三者的共同问题:版本更新如何被自动感知、并自动执行后续动作(部署/升级/通知)?
本文不打算只讲某一个具体方案,而是给出一个可复用的方法论:GitHub Release 自动化更新有三条路径,各自适用什么场景、有什么坑,以及如何用一张决策树快速选型。三个真实案例(上面的三个目标)就是这套方法论的实践样本。
核心决策框架:三个维度
任何"订阅 Release"的需求,都可以用三个维度来定位:
维度 1:仓库是不是自己的?——决定技术上能不能用 Webhook
GitHub 的 Webhook 只有仓库管理员(owner 或 admin 权限的协作者)能配置。这是最硬性的约束:
自己的仓库 → 原生 Webhook 可用(实时、官方、零轮询成本)
别人的仓库 → GitHub 不会给你推事件,只能轮询(Argus / Cron)
维度 2:更新敏感度?——决定要不要全自动执行
- 敏感(改代码、重启核心服务、有本地 patch 要维护)→ 需要可控的自动执行 + 失败可见
- 不敏感(换个包、重启个 sidecar)→ 简单自动即可,甚至只需要通知
维度 3:实时性要求?——决定轮询周期和架构复杂度
- 秒级响应(CI/CD 链路)→ Webhook
- 分钟级(第三方工具的版本跟进)→ Argus(30 分钟轮询)
- 小时/天级(低优先级的检查)→ Cron
决策树
flowchart TD
Q1{"仓库是自己的?"}
Q1 -- "是" --> A["方案一:原生 Webhook
实时推送 release/push 事件
配 HMAC 签名验证"]
Q1 -- "否" --> Q2{"更新敏感(需补丁恢复、
失败可见)或需要统一看板?"}
Q2 -- "是" --> B["方案二:Argus 轮询 + Webhook 转发
监控器查 GitHub API
发现新版主动发 Webhook 给你"]
Q2 -- "否" --> C["方案三:Cron 定期检查
脚本 curl 对比版本
有变化才动作,无变化静默"]
一句话版本:自己的仓库用 Webhook,别人的仓库里"重要"的用 Argus、“无所谓"的用 Cron。
方案一:GitHub 原生 Webhook(自己的仓库)
原理
仓库 Settings → Webhooks 里配一个 URL,GitHub 在 push / release 等事件发生时实时向该 URL 发 POST 请求。这是唯一"零轮询"的方案——事件发生即到达。
适用场景
- 目标仓库属于你(个人项目、团队仓库)
- 需要实时触发部署/CI/通知
- 已经有一个公网可达的接收端点(或内网穿透)
优点与缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 实时(秒级) | 只适用于自己的仓库 |
| 官方机制,零轮询成本 | 需要公网接收端点 |
| 事件类型丰富(push/release/PR…) | HMAC 签名验证要自己处理 |
实战 Case:sz-metro-api 的 push 自动部署
我的深圳地铁 API 服务(Go 项目)就是标准 Webhook 用法:GitHub push → 本机部署。
接收端用的是开源的 adnanh/webhook(一个极轻量的 webhook 监听器,配置 JSON 就能用),监听 9002 端口:
{
"id": "deploy-sz-metro",
"execute-command": "/root/cassdev/webhook/execute-command/deploy-sz-metro.sh",
"command-working-directory": "/root/cassdev/sz-metro-api",
"response-message": "Deploy triggered!",
"trigger-rule": {
"match": {
"type": "payload-hmac-sha256",
"secret": "<随机 hex>",
"parameter": { "source": "header", "name": "X-Hub-Signature-256" }
}
}
}
trigger-rule 是安全关键:GitHub 的 Webhook 请求带 X-Hub-Signature-256(用 secret 对 body 算 HMAC),adnanh 校验不通过就不执行。没有这层校验,任何知道你 URL 的人都能触发你的部署脚本。
触发后执行的部署脚本,模式非常简单(拉代码 → 构建 → 重启服务):
#!/bin/bash
set -e
PROJECT_DIR="/root/cassdev/sz-metro-api"
SERVICE="sz-metro-api.service"
LOG_FILE="/var/log/webhook-deploy.log"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploying sz-metro-api..." >> "$LOG_FILE"
cd "$PROJECT_DIR"
git pull origin main 2>&1 | tee -a "$LOG_FILE"
make build 2>&1 | tee -a "$LOG_FILE"
systemctl restart "$SERVICE" 2>&1 | tee -a "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy complete!" >> "$LOG_FILE"
这套链路跑了一周多,基本零干预——每次 push 代码,服务器自动完成"拉取 → 编译 → 重启 → 上线”。这也是后续所有方案的"接收端基建":adnanh/webhook 挂载多个 hook,各自对应不同的触发源。
踩坑记录
- HMAC 签名必须配:漏掉
trigger-rule等于把部署开关裸奔在公网 - 接收端点要公网可达:我直接用了云服务器公网 IP + 9002 端口;如果在内网,需要 frp/Cloudflare Tunnel 之类的穿透
git pull 2>&1 | tee会吞掉失败退出码:脚本set -e但没开pipefail,管道退出码取的是tee的——本地与远端分叉时git pull报fatal: Need to specify how to reconcile divergent branches.但脚本继续在旧代码上构建并重启,你完全无感知。建议set -o pipefail+git pull --ff-only,失败就停
方案二:Argus 轮询 + Webhook 转发(别人的仓库)
原理
别人的仓库我们配不了 Webhook,但可以"雇佣一个看门狗":Argus(github.com/release-argus/Argus)定期查询目标仓库的 GitHub API,当最新版本号发生变化时,它主动向你的端点发送GitHub 风格的 Webhook(同样带 HMAC 签名)。
也就是说:Argus 把"轮询"包装成了"Webhook 推送",你的接收端完全不需要区分触发源是 GitHub 官方还是 Argus——接收端基建与方案一完全复用。
适用场景
- 目标仓库是别人的,但你需要自动触发更新/部署流程
- 需要监控多个第三方仓库,统一管理
- 有 Web UI 审批需求(Argus 支持人工审批 Webhook 或
auto_approve全自动)
优点与缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 别人仓库也能"Webhook"式触发 | 分钟级延迟(不是实时) |
| 接收端与方案一统一 | 多一个容器要维护 |
| 支持私有仓库(Access Token) | 轮询消耗 GitHub API 配额 |
| 有 Web UI 看板 + 审批 | 配置细节多,有坑 |
实战 Case:Hermes Agent 本体自动更新
Hermes Agent(AI Agent 框架,跑在我的主服务器上)是 NousResearch 的仓库——我没有 Webhook 权限,而且它的更新很敏感:更新会重置本地补丁、需要重启 gateway(核心服务)。这正是 Argus 的用武之地。
完整链路:
flowchart LR
subgraph GitHub["GitHub 远程"]
R1["NousResearch/hermes-agent
(别人的仓库)"]
end
subgraph TC["tc 主服务器"]
A["Argus 容器
每 30 分钟轮询"]
W["adnanh/webhook :9002
update-hermes hook"]
S["update-hermes.sh
备份→更新→恢复→重启"]
G["Hermes Gateway
systemd 自动拉起"]
end
U["飞书通知"]
R1 -- "发现新 release" --> A
A -- "GitHub 风格 Webhook
HMAC 签名" --> W
W -- "触发" --> S
S -- "重启" --> G
S -- "更新结果" --> U
Argus 配置(Docker 部署,监控 + Webhook 转发):
service:
hermes-agent:
options:
active: true
interval: 30m # 每 30 分钟查一次
semantic_versioning: true
latest_version:
type: github
url: NousResearch/hermes-agent
access_token: ${GITHUB_ACCESS_TOKEN} # 提高 API 配额;私有仓库必须
use_prerelease: false
webhook:
hermes-update:
type: github # GitHub 风格 Webhook(带 X-Hub-Signature-256)
url: http://127.0.0.1:9002/hooks/update-hermes
secret: <和接收端相同的 hex>
desired_status_code: 0 # 0 = 接受任意 2XX
max_tries: 3
dashboard:
auto_approve: true # 发现新版本自动发 Webhook,不需要人工审批
web_url: https://github.com/NousResearch/hermes-agent/releases/tag/{{ version }}
接收端 hook 与方案一完全同一个文件(/etc/webhook/sz-metro-hooks.json 数组里加一项),只是 execute-command 指向了 Hermes 的更新脚本。
更新脚本是这个方案真正的难点——因为 hermes update 会重置本地补丁(见下文"通用方法论"),脚本必须实现"备份 → 更新 → 恢复 → 重启 → 通知"的完整闭环(以下为要点摘录,完整版见 /root/cassdev/webhook/execute-command/update-hermes.sh):
#!/bin/bash
set -uo pipefail
HERMES_DIR=/usr/local/lib/hermes-agent
VENV_BIN=$HERMES_DIR/venv/bin
SITE_PKG=$HERMES_DIR/venv/lib/python3.11/site-packages
PATCH_DIR=/root/cassdev/patches
cd "$HERMES_DIR" || exit 1 # 所有 git 操作都依赖这个工作目录
# 1. 备份本地补丁
git diff plugins/platforms/feishu/adapter.py > "$PATCH_DIR/hermes-adapter-fix.patch"
cp -f .hermes_feishu_card_manifest "$PATCH_DIR/"
cp -f .hermes_feishu_card_recovery.lock "$PATCH_DIR/"
# 2. 执行更新(--yes 跳过交互;必须用 venv 里的 hermes)
$VENV_BIN/hermes update --yes
# 3. 重打 HFC 插件的 monkey-patch(3 个文件)
python3 - <<'EOF'
import sys
sys.path.insert(0, "/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages")
from hermes_feishu_card.install.patcher import apply_patch, apply_cron_patch, apply_base_patch
for path, fn in [
("gateway/run.py", apply_patch),
("cron/scheduler.py", apply_cron_patch),
("gateway/platforms/base.py", apply_base_patch),
]:
content = open(path).read()
patched = fn(content)
if patched != content:
open(path, "w").write(patched)
EOF
# 4. 恢复 adapter 修复(补丁文件存在且上游未修复时才 apply)
if grep -q "receive_id=thread_id" plugins/platforms/feishu/adapter.py \
&& [ -s "$PATCH_DIR/hermes-adapter-fix.patch" ]; then
git apply --check "$PATCH_DIR/hermes-adapter-fix.patch" && \
git apply "$PATCH_DIR/hermes-adapter-fix.patch"
fi
# 5. 重启 gateway(SIGTERM,systemd Restart=always 自动拉起;PID 判空防误杀)
GPID=$(systemctl --user show hermes-gateway.service -p MainPID --value)
if [ -n "$GPID" ] && [ "$GPID" != "0" ] && [ "$GPID" != "1" ]; then
/bin/kill -s TERM "$GPID"
fi
# 6. 飞书通知结果(带明确 chat 目标)
echo "Hermes 更新完成" | $VENV_BIN/hermes send -t "feishu:oc_dd0079426fa24cec9168e28e8daa41f0"
注意三个前置条件:所有 git 操作必须在 $HERMES_DIR 里执行;patcher 在 Hermes 的 venv site-packages 里,要用 venv 的 python 或显式 sys.path.insert;hermes/pip 命令在 webhook/cron 环境里 PATH 没有,必须用绝对路径。
踩坑记录(Argus 部署实测)
- 镜像名不是
release-argus/argus:Docker Hub 上是releaseargus/argus(少一个连字符),直接 pull 报pull access denied - config.yml 必须挂到
/app/config.yml,不是数据目录/data/。挂错位置的话,Argus 会静默地监控它自己(日志显示Found 1 services to monitor: release-argus/Argus),你完全看不出配置没生效 - 本机老版 gh(2.4.0)没有
gh auth token命令(新版 gh ≥ 2.23 才支持):想取 token 会得到unknown command "token" for "gh auth",把这段错误文本当 token 塞给 Argus 会报invalid header field value for "Authorization"。正确姿势:grep -oP 'oauth_token: \K\S+' ~/.config/gh/hosts.yml - 首次启动不触发 Webhook:Argus 只在"版本发生变化"时触发,首次查询只是记录基线——这点很好,否则部署即误触发
- 容器内以 911 用户运行:配置文件要保证 911 可读——
chown 911:911文件(或 644 且非 600),所在目录要可遍历(755 或同样 chown 给 911)。只把文件chmod 600属主却是 root,容器会读不到配置,然后静默监控它自己
方案三:Cron 定期检查(低敏感、批量)
原理
最朴素的方式:cron 定时跑一个脚本,curl GitHub API/raw 文件拿最新版本号,和本地版本对比,有变化才动作,无变化静默退出。
适用场景
- 更新不敏感(换包、重启附属进程),或者只需要通知不需要自动执行
- 要批量监控很多项目(一个 cron 脚本可以循环检查 N 个仓库)
- 不想为"看看有没有新版"这件事引入任何常驻服务
优点与缺点
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 零常驻服务,就是一个脚本 | 延迟取决于周期(小时/天级) |
| 逻辑完全透明,易调试 | 轮询消耗 GitHub API 配额(60 次/小时匿名) |
| 静默/通知模式灵活(空输出=安静) | 每个目标要写版本对比逻辑 |
实战 Case:飞书卡片插件(HFC)每天 0 点检查
HFC 是给 Hermes 加飞书卡片渲染的第三方插件,更新敏感度低(pip 换包 + 重启 sidecar,sidecar 是独立进程不影响会话),所以我选了 Cron:每天 0 点检查一次,无更新就安静,有更新自动升级并推送飞书报告。
#!/bin/bash
# HFC release 检查 + 自动升级。无更新 → 空输出(cron 静默);有更新 → 升级并输出报告。
set -uo pipefail
export XDG_RUNTIME_DIR=/run/user/0 # systemctl --user 在 cron 环境必需
PIP=/usr/local/lib/hermes-agent/venv/bin/pip
SITE_PKG=/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages
REPO=baileyh8/hermes-feishu-streaming-card
LOG=/var/log/hfc-update.log
LOCAL=$($PIP show hermes-feishu-streaming-card 2>/dev/null | awk '/^Version/{print $2}')
LATEST=$(curl -sL --max-time 20 "https://raw.githubusercontent.com/$REPO/main/hermes_feishu_card/__init__.py" \
| grep -oP '__version__ = "\K[^"]+')
[ -z "$LATEST" ] && exit 0 # 网络失败静默
[ "$LOCAL" = "$LATEST" ] && exit 0 # 无更新静默
export PIP_ROOT_USER_ACTION=ignore
$PIP install --upgrade "git+https://github.com/$REPO.git" --index-url https://pypi.org/simple >> "$LOG" 2>&1
systemctl --user restart hermes-feishu-card-sidecar.service >> "$LOG" 2>&1 \
|| systemctl restart hermes-feishu-card-sidecar.service >> "$LOG" 2>&1 # --user 失败时回退系统级
# 判断 hook_runtime.py 是否变化,决定要不要重启 gateway(有变化才重启)
curl -sL --max-time 20 -o /tmp/hfc_hook_old.py "https://raw.githubusercontent.com/$REPO/v$LOCAL/hermes_feishu_card/hook_runtime.py"
if [ -s /tmp/hfc_hook_old.py ] && ! diff -q /tmp/hfc_hook_old.py "$SITE_PKG/hermes_feishu_card/hook_runtime.py" >/dev/null; then
( sleep 20; kill -TERM "$(systemctl --user show hermes-gateway.service -p MainPID --value)" ) &
fi
# 回读实际版本:pip 失败时给出告警而不是假成功
NEW=$($PIP show hermes-feishu-streaming-card 2>/dev/null | awk '/^Version/{print $2}')
if [ "$NEW" = "$LATEST" ]; then
echo "✅ HFC 自动升级完成:$LOCAL → $NEW"
else
echo "⚠️ HFC 升级异常:目标 $LATEST,实际 $NEW(详见 $LOG)"
fi
注册到 Hermes 的 cron(no_agent 纯脚本模式:stdout 非空才推送,空输出 = 完全静默):
hermes cron create "0 0 * * *" --name hfc-release-update-check \
--script hfc-update-checker.sh --no-agent
踩坑记录
- cron 环境的 PATH 不含 venv:裸
pip会command not found,必须用/usr/local/lib/hermes-agent/venv/bin/pip绝对路径 systemctl --user需要XDG_RUNTIME_DIR:非交互/cron 环境里不设export XDG_RUNTIME_DIR=/run/user/0,systemctl --user会报 “Failed to connect to bus” 静默失败——这个比 PATH 问题更隐蔽[ -s ]守卫:curl 下载旧版本文件失败时不能拿空文件去 diff,否则误判"hook_runtime 变了"导致 gateway 被无谓重启- 网络失败要静默:
curl超时应该exit 0而不是报错刷屏——反正下次 cron 还会跑 - 升级后回读版本:
pip install成功不代表版本对,回读pip show实际版本,不一致要告警而不是报"完成"
通用方法论:任何"自动更新"都必须处理的 4 件事
无论选哪种方案,更新脚本本身有几个跨方案的共同难点,是方法论的核心:
1. 签名验证(Webhook 链路)
凡是公网可达的接收端点,必须验证请求签名。GitHub 和 Argus 都支持 X-Hub-Signature-256(HMAC-SHA256),接收端(adnanh/webhook)校验不通过就不执行。上线前务必实测三种情况:
| 请求 | 期望行为 |
|---|---|
| 正确签名 | 执行 ✅ |
| 无签名 | 不执行(即使返回 200) |
| 错误签名 | 不执行(返回 500) |
2. 本地补丁的备份与恢复(最容易被忽略)
如果目标软件在你的环境里被改过(monkey-patch、git apply、手改配置),更新 = 重置这些修改。更新脚本必须"备份 → 更新 → 恢复":
- 更新前:
git diff导出补丁、备份 manifest 文件 - 更新后:重打补丁(用
git apply --check先探测——上游已经修了的话补丁不再适用,自动跳过) - 永远假设"更新会冲掉一切",而不是假设"这次应该没事"
3. 服务重启技巧
- 常驻服务配
Restart=always后,SIGTERM 主进程即可优雅重启,不要用systemctl restart(在服务自己进程内执行时可能把自己一起带走) - cron 场景要延迟重启:cron 调度器跑在目标服务(比如 gateway)里,立即重启会让升级报告来不及投递。用子 shell
( sleep 20; kill -TERM $PID ) &延迟执行 - 只在必要的组件变化时才重启:比如只判断
hook_runtime.py变了才重启 gateway,否则重启 sidecar 就够——把"重启"的爆炸半径控制到最小
4. 失败可见性
- 所有输出落日志文件(
/var/log/*.log) - 成功/失败都通知到人(飞书/邮件/Slack)
- 无更新时保持静默——通知渠道只有在"有情况"时才响
选型总结
| 维度 | 方案一:Webhook | 方案二:Argus | 方案三:Cron |
|---|---|---|---|
| 仓库所有权 | 必须自己的 | 任意(含私有) | 任意 |
| 实时性 | 秒级 | 分钟级(30m) | 周期决定(小时/天) |
| 部署成本 | 接收端点 + 公网 | Docker 容器 + 配置 | 一个脚本 |
| 额外能力 | 事件类型丰富 | Web UI 看板/审批、多仓库 | 批量循环检查 |
| 典型场景 | 自己项目的 CI/CD | 第三方工具自动更新 | 低敏感、批量巡检 |
| 我的实践 | sz-metro-api push 自动部署 | Hermes 本体自动更新 | HFC 插件每日检查 |
三个案例对应三个真实约束:
- sz-metro-api:我的仓库 → 原生 Webhook,实时部署 ✅
- Hermes:别人的仓库 + 更新敏感 + 本地有 patch → Argus 轮询转发,全自动 + 补丁恢复 ✅
- HFC:别人的仓库 + 更新不敏感 → Cron 每日检查,静默升级 ✅
最后想强调的是:这不是"哪个方案更好"的问题,而是"当前约束下哪个方案合适"的问题。仓库所有权决定了技术上限,敏感度决定了自动化程度,实时性决定了架构复杂度——把这三个维度想清楚,任何"订阅 Release"的需求都能快速落地,且不会踩"更新冲掉本地补丁"这种暗坑。