Pushing a tag triggers an automatic deploy. Tests must pass first.
Tag push
→ python-tests.yml runs
→ deploy.yml triggers (on workflow_run success)
→ POSTs HMAC-signed payload to https://dochub.be/webhook/dochub-deploy
→ server validates signature, runs deploy.sh
→ output returned to GitHub Action logs
Push a tag matching the YYYY.M.N format:
git tag 2026.4.0 && git push origin 2026.4.0Check the Actions tab for deploy output, or on the server:
journalctl -fu dochub-webhook- Validates the tag format
- Acquires a file lock (prevents concurrent deploys)
git fetch --tags --prune origin && git checkout $TAGuv run --frozen manage.py migrate --noinputuv run --frozen manage.py collectstatic --noinputsudo systemctl restart dochub-gunicorn dochub-celery
If any step fails, set -e stops the script before restarting services, so the old code keeps running.
--frozen is mandatory: without it, uv run rewrites uv.lock's dynamic exclude-newer timestamp on every invocation, leaving a dirty working tree that breaks the next
deploy's git checkout. The dochub-gunicorn and dochub-celery systemd units pass --frozen for the same reason.
DEPLOY_WEBHOOK_SECRET (repo Settings > Secrets and variables > Actions) must match the secret in /etc/webhook/hooks.json on the server.
These files live on the server only (not in git):
| File | Purpose |
|---|---|
/srv/dochub/deploy.sh |
Thin wrapper that execs the repo's deploy.sh |
/etc/webhook/hooks.json |
Webhook config: HMAC validation, ref check, runs as dochub |
/etc/systemd/system/dochub-webhook.service |
Runs webhook on 127.0.0.1:9000 |
/etc/caddy/Caddyfile |
Proxies /webhook/* to the webhook service |
/etc/sudoers.d/dochub-deploy |
Allows dochub to restart services without password |
#!/bin/bash
# Thin wrapper — real logic lives in /srv/dochub/source/deploy.sh (versioned in git)
exec /srv/dochub/source/deploy.sh "$@"[
{
"id": "dochub-deploy",
"execute-command": "/srv/dochub/deploy.sh",
"command-working-directory": "/srv/dochub",
"include-command-output-in-response": true,
"trigger-rule": {
"and": [
{
"match": {
"type": "payload-hmac-sha256",
"secret": "<DEPLOY_WEBHOOK_SECRET>",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
},
{
"match": {
"type": "regex",
"regex": "^refs/tags/",
"parameter": {
"source": "payload",
"name": "ref"
}
}
}
]
},
"pass-arguments-to-command": [
{
"source": "payload",
"name": "ref"
}
]
}
][Unit]
Description=DocHub deploy webhook
After=network.target
[Service]
ExecStart=/usr/bin/webhook -hooks /etc/webhook/hooks.json -ip 127.0.0.1 -port 9000
User=dochub
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targethandle_path /webhook/* {
rewrite * /hooks{uri}
reverse_proxy localhost:9000
}
dochub ALL=(root) NOPASSWD: /bin/systemctl restart dochub-gunicorn dochub-celery