为什么需要"订阅 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,各自对应不同的触发源。

踩坑记录

  1. HMAC 签名必须配:漏掉 trigger-rule 等于把部署开关裸奔在公网
  2. 接收端点要公网可达:我直接用了云服务器公网 IP + 9002 端口;如果在内网,需要 frp/Cloudflare Tunnel 之类的穿透
  3. git pull 2>&1 | tee 会吞掉失败退出码:脚本 set -e 但没开 pipefail,管道退出码取的是 tee 的——本地与远端分叉时 git pullfatal: Need to specify how to reconcile divergent branches. 但脚本继续在旧代码上构建并重启,你完全无感知。建议 set -o pipefail + git pull --ff-only,失败就停

方案二:Argus 轮询 + Webhook 转发(别人的仓库)

原理

别人的仓库我们配不了 Webhook,但可以"雇佣一个看门狗":Argusgithub.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.inserthermes/pip 命令在 webhook/cron 环境里 PATH 没有,必须用绝对路径。

踩坑记录(Argus 部署实测)

  1. 镜像名不是 release-argus/argus:Docker Hub 上是 releaseargus/argus(少一个连字符),直接 pull 报 pull access denied
  2. config.yml 必须挂到 /app/config.yml,不是数据目录 /data/。挂错位置的话,Argus 会静默地监控它自己(日志显示 Found 1 services to monitor: release-argus/Argus),你完全看不出配置没生效
  3. 本机老版 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
  4. 首次启动不触发 Webhook:Argus 只在"版本发生变化"时触发,首次查询只是记录基线——这点很好,否则部署即误触发
  5. 容器内以 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

踩坑记录

  1. cron 环境的 PATH 不含 venv:裸 pipcommand not found,必须用 /usr/local/lib/hermes-agent/venv/bin/pip 绝对路径
  2. systemctl --user 需要 XDG_RUNTIME_DIR:非交互/cron 环境里不设 export XDG_RUNTIME_DIR=/run/user/0systemctl --user 会报 “Failed to connect to bus” 静默失败——这个比 PATH 问题更隐蔽
  3. [ -s ] 守卫:curl 下载旧版本文件失败时不能拿空文件去 diff,否则误判"hook_runtime 变了"导致 gateway 被无谓重启
  4. 网络失败要静默curl 超时应该 exit 0 而不是报错刷屏——反正下次 cron 还会跑
  5. 升级后回读版本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"的需求都能快速落地,且不会踩"更新冲掉本地补丁"这种暗坑。