Push → deploy for all five sites on gx (1.14.133.242), with zero hand-written
receiver code: a generic off-the-shelf receiver (adnanh/webhook) plus one
config whitelist. Replaces the five per-site webhook.mjs copies
(~230 lines each) and their ten systemd units.
GitHub push
→ repo webhook (URL https://<site>/hooks/<id>, secret in /home/<user>/.webhook-secret)
→ Caddy @deployhook (path matcher → reverse_proxy unix//run/webhook.sock)
→ systemd socket activation (webhook.socket, idle = 0 processes)
→ webhook 2.8.3 (user `webhook`, /etc/webhook/hooks.json)
rules per entry: HMAC-SHA256 signature AND ref == pinned branch
→ sudo (/etc/sudoers.d/webhook-deploy, args pinned per site)
→ /usr/local/lib/webhook-deploy (runner: runs deploy.sh, appends ~/deploy.log,
best-effort ntfy to ~/.ntfy-notify)
→ <clone>/scripts/server/deploy.sh (fetch → build → rsync → publish)
| site | user | clone | branch | hook id |
|---|---|---|---|---|
| zp.xeed.ink | zig-ci | ~/zig-playground | master | zp-deploy |
| xeed.ink | blog-ci | ~/blog | astro | blog-deploy |
| poetry.xeed.ink | poetry-ci | ~/poetry | main | poetry-deploy |
| sched.xeed.ink | sched-ci | ~/schedmate | main | sched-deploy |
| zlg.xeed.ink | ziglings-ci | ~/ziglings-web | main | ziglings-deploy |
zig-playground's default remote is the zigcc org (origin = zigcc/main,
push.default=upstream); the deploy hook lives on the xihale remote, so a
deploy push is git push xihale master:master — a bare git push only goes
to zigcc.
Server (canonical; webhook-deploy, webhook.socket, webhook.service,
hooks.json.example in this directory are the repo copies of the gx assets):
/usr/local/bin/webhook— upstream static binary v2.8.3 (Debian's 2.8.0 package lacks socket activation; upgrade = swap the binary)./usr/local/lib/webhook-deploy— shared runner, owned root:root 0755./etc/webhook/hooks.json— the whitelist. root:webhook 640;-hotreloadis on, edits apply live. Regenerate with thegen()block inhooks.json.example(secrets are read from the per-user.webhook-secreton the server and must never leave it)./etc/sudoers.d/webhook-deploy— one line per site; pins runner path, deploy.sh path, ref and project (trailing*is the pushed sha)./etc/systemd/system/webhook.{socket,service}— socket ownedwebhook:caddy0660; Caddy dials it.
Repo (scripts/server/ in each site's repository): deploy.sh (the build
logic — the only thing the receiver is allowed to run) + this README.
- Server:
useradd -u <uid> -m <site>-ci, clone the repo,openssl rand -hex 32→~/.webhook-secret(0600), optionally~/.ntfy-notify(topic + user:pass). - Register the GitHub hook (
config.urlmust be insideconfig):https://<domain>/hooks/<site>-deploywith that secret. - Caddy:
@deployhook { path /hooks/<site>-deploy method POST }+reverse_proxy @deployhook unix//run/webhook.sockfirst in the route. hooks.json: append an entry (gen()inhooks.json.example) — hotreload picks it up.sudoers.d/webhook-deploy: append the pinned line. No new units, no new code.
- Unsigned request (no signature header) → 200 "hook rules were not satisfied", nothing runs (GitHub always signs; scanners get nothing).
- Wrong signature → 500 (before: 403).
- ping / non-deploy branch → 200, nothing runs (before: explicit pong).
- Branch deletion now walks into deploy.sh and fails (fetch of a dead ref) with a failure ntfy — the old code 200-ignored it. Rare; acceptable noise.
- After the first delivery the service stays resident (~10 MB Go process).
systemctl stop webhook.servicereturns to 0; the socket reactivates it. Old units were per-request spawns (0 always) — accepted trade for deleting ~1200 lines. - deploy.sh concurrency/queueing (flock + rerun), ZP_PUSH_SHA and the
ntfy message format
[project@gx] deploy success/failureare unchanged.
- No
command-working-directoryin hooks.json entries. The<site>-cihomes are0700, so thewebhookuser cannot chdir into them; the spawn then fails silently while GitHub still records a 200 delivery. The runner ignores its cwd and everydeploy.shcds into its own clone. - Rebuilding hooks.json with
jq > .new && mvbreaks-hotreload(the inotify watch dies with the old inode) — edits apply but replacements needsystemctl restart webhook.service. Prefer in-place edits, or restart. - Upgrading = swapping the binary at
/usr/local/bin/webhook(currently 2.8.3; checkwebhook -version).
- Receiver down:
journalctl -u webhook -u webhook.socket; restart =systemctl start webhook.socket(service is socket-activated). - Suspect the whitelist:
jq '.[].id' /etc/webhook/hooks.json. - Full rollback: Caddyfile backups live at
/etc/caddy/Caddyfile.bak-webhook-*; the retired per-site units are in git history (scripts/server/before 2026-10) if ever needed again. - Manual deploy (bypasses the receiver entirely):
sudo -u zig-ci bash ~/zig-playground/scripts/server/deploy.sh refs/heads/master.