Skip to content

Commit 373d591

Browse files
committed
Add deploy documentation and deploy script
1 parent 4dbf15f commit 373d591

3 files changed

Lines changed: 181 additions & 3 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,29 @@ jobs:
1616
env:
1717
WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
1818
run: |
19-
REF="refs/tags/${{ github.event.workflow_run.head_branch }}"
19+
TAG="${{ github.event.workflow_run.head_branch }}"
20+
REF="refs/tags/$TAG"
2021
PAYLOAD="{\"ref\":\"$REF\"}"
2122
SIGNATURE="sha256=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | cut -d' ' -f2)"
22-
RESPONSE=$(curl -sf --max-time 120 \
23+
24+
HTTP_CODE=$(curl --max-time 120 \
25+
-o /tmp/deploy-output.txt \
26+
-w "%{http_code}" \
2327
-X POST \
2428
-H "Content-Type: application/json" \
2529
-H "X-Hub-Signature-256: $SIGNATURE" \
2630
-d "$PAYLOAD" \
2731
"https://dochub.be/webhook/dochub-deploy")
28-
echo "$RESPONSE"
32+
33+
echo "=== Deploy output ==="
34+
cat /tmp/deploy-output.txt
35+
36+
if [ "$HTTP_CODE" != "200" ]; then
37+
echo "Webhook returned HTTP $HTTP_CODE"
38+
exit 1
39+
fi
40+
41+
if ! grep -q "Deployed $TAG" /tmp/deploy-output.txt; then
42+
echo "Deploy did not complete successfully"
43+
exit 1
44+
fi

DEPLOY.md

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Auto-deploy
2+
3+
Pushing a tag triggers an automatic deploy. Tests must pass first.
4+
5+
## Flow
6+
7+
```
8+
Tag push
9+
→ python-tests.yml runs
10+
→ deploy.yml triggers (on workflow_run success)
11+
→ POSTs HMAC-signed payload to https://dochub.be/webhook/dochub-deploy
12+
→ server validates signature, runs deploy.sh
13+
→ output returned to GitHub Action logs
14+
```
15+
16+
## How to deploy
17+
18+
Push a tag matching the `YYYY.M.N` format:
19+
20+
```bash
21+
git tag 2026.4.0 && git push origin 2026.4.0
22+
```
23+
24+
Check the **Actions** tab for deploy output, or on the server:
25+
26+
```bash
27+
journalctl -fu dochub-webhook
28+
```
29+
30+
## What `deploy.sh` does
31+
32+
1. Validates the tag format
33+
2. Acquires a file lock (prevents concurrent deploys)
34+
3. `git fetch --tags --prune origin && git checkout $TAG`
35+
4. `uv run manage.py migrate --noinput`
36+
5. `uv run manage.py collectstatic --noinput`
37+
6. `sudo systemctl restart dochub-gunicorn dochub-celery`
38+
39+
If any step fails, `set -e` stops the script before restarting services, so the old code keeps running.
40+
41+
## GitHub secret
42+
43+
`DEPLOY_WEBHOOK_SECRET` (repo Settings > Secrets and variables > Actions) must match the secret in `/etc/webhook/hooks.json` on the server.
44+
45+
## Server-side components
46+
47+
These files live on the server only (not in git):
48+
49+
| File | Purpose |
50+
|------|---------|
51+
| `/srv/dochub/deploy.sh` | Thin wrapper that `exec`s the repo's `deploy.sh` |
52+
| `/etc/webhook/hooks.json` | Webhook config: HMAC validation, ref check, runs as `dochub` |
53+
| `/etc/systemd/system/dochub-webhook.service` | Runs `webhook` on `127.0.0.1:9000` |
54+
| `/etc/caddy/Caddyfile` | Proxies `/webhook/*` to the webhook service |
55+
| `/etc/sudoers.d/dochub-deploy` | Allows `dochub` to restart services without password |
56+
57+
## Appendix: server-side file contents
58+
59+
### `/srv/dochub/deploy.sh`
60+
61+
```bash
62+
#!/bin/bash
63+
# Thin wrapper — real logic lives in /srv/dochub/source/deploy.sh (versioned in git)
64+
exec /srv/dochub/source/deploy.sh "$@"
65+
```
66+
67+
### `/etc/webhook/hooks.json`
68+
69+
```json
70+
[
71+
{
72+
"id": "dochub-deploy",
73+
"execute-command": "/srv/dochub/deploy.sh",
74+
"command-working-directory": "/srv/dochub",
75+
"include-command-output-in-response": true,
76+
"trigger-rule": {
77+
"and": [
78+
{
79+
"match": {
80+
"type": "payload-hmac-sha256",
81+
"secret": "<DEPLOY_WEBHOOK_SECRET>",
82+
"parameter": {
83+
"source": "header",
84+
"name": "X-Hub-Signature-256"
85+
}
86+
}
87+
},
88+
{
89+
"match": {
90+
"type": "regex",
91+
"regex": "^refs/tags/",
92+
"parameter": {
93+
"source": "payload",
94+
"name": "ref"
95+
}
96+
}
97+
}
98+
]
99+
},
100+
"pass-arguments-to-command": [
101+
{
102+
"source": "payload",
103+
"name": "ref"
104+
}
105+
]
106+
}
107+
]
108+
```
109+
110+
### `/etc/systemd/system/dochub-webhook.service`
111+
112+
```ini
113+
[Unit]
114+
Description=DocHub deploy webhook
115+
After=network.target
116+
117+
[Service]
118+
ExecStart=/usr/bin/webhook -hooks /etc/webhook/hooks.json -ip 127.0.0.1 -port 9000
119+
User=dochub
120+
Restart=on-failure
121+
RestartSec=5
122+
123+
[Install]
124+
WantedBy=multi-user.target
125+
```
126+
127+
### `/etc/caddy/Caddyfile` (relevant block)
128+
129+
```
130+
handle_path /webhook/* {
131+
rewrite * /hooks{uri}
132+
reverse_proxy localhost:9000
133+
}
134+
```
135+
136+
### `/etc/sudoers.d/dochub-deploy`
137+
138+
```
139+
dochub ALL=(root) NOPASSWD: /bin/systemctl restart dochub-gunicorn dochub-celery
140+
```

deploy.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
TAG="${1#refs/tags/}"
5+
6+
# Validate tag name to prevent injection
7+
if [[ ! "$TAG" =~ ^v?[0-9]+\.[0-9]+(\.[0-9]+)?(-[a-zA-Z0-9.]+)?$ ]]; then
8+
echo "Invalid tag format: $TAG" >&2
9+
exit 1
10+
fi
11+
12+
# Prevent concurrent deploys
13+
exec 200>/tmp/dochub-deploy.lock
14+
flock -n 200 || { echo "Deploy already running, skipping" >&2; exit 1; }
15+
16+
cd /srv/dochub/source
17+
git fetch --tags --prune origin
18+
git checkout "$TAG"
19+
/srv/dochub/.local/bin/uv run manage.py migrate --noinput
20+
/srv/dochub/.local/bin/uv run manage.py collectstatic --noinput
21+
sudo /bin/systemctl restart dochub-gunicorn dochub-celery
22+
echo "Deployed $TAG"

0 commit comments

Comments
 (0)