Monorepo for Aesthetic.Computer
aesthetic.computer
1#!/usr/bin/env bash
2# webhook.sh — Smart auto-deploy for lith
3# Called by POST /lith/deploy (from GitHub push webhook)
4#
5# Strategy:
6# 1. git pull
7# 2. Check which files changed
8# 3. If only static frontend files changed → done (Caddy serves from disk)
9# 4. If backend functions or lith/ changed → npm install if needed, restart lith
10# 5. If Caddyfile changed → reload caddy
11#
12# This means most frontend pushes deploy instantly with zero downtime.
13
14set -euo pipefail
15
16REMOTE_DIR="/opt/ac"
17LOG_TAG="[lith-deploy]"
18DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
19
20log() { echo "$LOG_TAG $*"; }
21
22if ! [[ "$DEPLOY_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]]; then
23 log "invalid DEPLOY_BRANCH: $DEPLOY_BRANCH"
24 exit 2
25fi
26
27cd "$REMOTE_DIR"
28
29# Record HEAD before pull
30OLD_HEAD=$(git rev-parse HEAD)
31OLD_BRANCH=$(git branch --show-current)
32
33# Pull latest
34log "pulling branch $DEPLOY_BRANCH..."
35git fetch origin "$DEPLOY_BRANCH" --quiet
36
37if git show-ref --verify --quiet "refs/heads/$DEPLOY_BRANCH"; then
38 git checkout "$DEPLOY_BRANCH" --quiet
39else
40 git checkout -B "$DEPLOY_BRANCH" "origin/$DEPLOY_BRANCH" --quiet
41fi
42
43git reset --hard "origin/$DEPLOY_BRANCH" --quiet
44
45NEW_HEAD=$(git rev-parse HEAD)
46NEW_BRANCH=$(git branch --show-current)
47
48if [ "$OLD_HEAD" = "$NEW_HEAD" ]; then
49 log "already up to date on $NEW_BRANCH ($NEW_HEAD)"
50 exit 0
51fi
52
53# Write commit ref for version endpoint
54echo "$NEW_HEAD" > system/public/.commit-ref
55
56# Get list of changed files
57CHANGED=$(git diff --name-only "$OLD_HEAD" "$NEW_HEAD")
58log "updated $OLD_BRANCH/$OLD_HEAD -> $NEW_BRANCH/$NEW_HEAD"
59log "changed files:"
60echo "$CHANGED" | sed 's/^/ /'
61
62NEED_RESTART=false
63NEED_CADDY_RELOAD=false
64NEED_NPM_INSTALL=false
65NEED_DP1_FEED_RESTART=false
66PURGE_URLS=()
67
68# Map a system/public/<host>/<rel> path to the public URL(s) it serves.
69# papers.aesthetic.computer is also reachable as papers.prompt.ac, so emit both.
70emit_urls_for() {
71 local file="$1"
72 case "$file" in
73 system/public/papers.aesthetic.computer/*)
74 local rel="${file#system/public/papers.aesthetic.computer/}"
75 PURGE_URLS+=("https://papers.aesthetic.computer/${rel}")
76 PURGE_URLS+=("https://papers.prompt.ac/${rel}")
77 ;;
78 system/public/aesthetic.computer/*)
79 local rel="${file#system/public/aesthetic.computer/}"
80 PURGE_URLS+=("https://aesthetic.computer/${rel}")
81 ;;
82 system/public/*)
83 # Other subdomains — strip the host segment and emit one URL.
84 local stripped="${file#system/public/}"
85 local host="${stripped%%/*}"
86 local rel="${stripped#*/}"
87 PURGE_URLS+=("https://${host}/${rel}")
88 ;;
89 esac
90}
91
92while IFS= read -r file; do
93 case "$file" in
94 lith/server.mjs|lith/package.json)
95 NEED_RESTART=true
96 ;;
97 lith/Caddyfile)
98 NEED_CADDY_RELOAD=true
99 ;;
100 lith/package-lock.json)
101 NEED_NPM_INSTALL=true
102 NEED_RESTART=true
103 ;;
104 system/netlify/functions/*)
105 NEED_RESTART=true
106 ;;
107 system/package.json|system/package-lock.json)
108 NEED_NPM_INSTALL=true
109 NEED_RESTART=true
110 ;;
111 shared/*)
112 NEED_RESTART=true
113 ;;
114 lith/dp1-feed-config.yaml|lith/dp1-feed.service)
115 NEED_DP1_FEED_RESTART=true
116 ;;
117 system/public/*)
118 emit_urls_for "$file"
119 ;;
120 *)
121 # Other files (docs, tests, etc.) — no action needed
122 ;;
123 esac
124done <<< "$CHANGED"
125
126if $NEED_NPM_INSTALL; then
127 log "installing dependencies..."
128 cd "$REMOTE_DIR/system" && npm install --omit=dev --quiet 2>&1 | tail -1
129 cd "$REMOTE_DIR/lith" && npm install --omit=dev --quiet 2>&1 | tail -1
130fi
131
132if $NEED_CADDY_RELOAD; then
133 # Caddy reads /etc/caddy/Caddyfile, not /opt/ac/lith/Caddyfile, so the
134 # checked-out copy has to be installed before the reload — otherwise
135 # `systemctl reload caddy` re-reads the same stale config and the change
136 # silently no-ops. (deploy.fish does the same cp; webhook missed it.)
137 # See: 2026-04-29 jeffrey-platter Caddy try_files fix that didn't apply.
138 log "installing Caddyfile + reloading caddy..."
139 cp "$REMOTE_DIR/lith/Caddyfile" /etc/caddy/Caddyfile
140 systemctl reload caddy
141fi
142
143if $NEED_DP1_FEED_RESTART; then
144 if systemctl is-active dp1-feed &>/dev/null; then
145 log "updating dp1-feed config..."
146 cp "$REMOTE_DIR/lith/dp1-feed-config.yaml" /opt/dp1-feed/config.yaml
147 cp "$REMOTE_DIR/lith/dp1-feed.service" /etc/systemd/system/dp1-feed.service
148 systemctl daemon-reload
149 systemctl restart dp1-feed
150 fi
151fi
152
153if $NEED_RESTART; then
154 log "restarting lith (backend changes detected)..."
155 systemctl restart lith
156else
157 log "static-only deploy — no restart needed"
158fi
159
160if [ ${#PURGE_URLS[@]} -gt 0 ]; then
161 if [ -n "${CLOUDFLARE_PURGE_TOKEN:-}" ] && [ -n "${CLOUDFLARE_ZONE_ID:-}" ]; then
162 log "purging ${#PURGE_URLS[@]} Cloudflare URL(s) on zone $CLOUDFLARE_ZONE_ID..."
163 # Cloudflare's purge_cache takes up to 30 URLs per request. Chunk the list.
164 chunk=()
165 purge_chunk() {
166 local files_json
167 files_json=$(printf '%s\n' "${chunk[@]}" | python3 -c 'import sys, json; print(json.dumps({"files": [l.strip() for l in sys.stdin if l.strip()]}))')
168 CF_RESPONSE=$(curl -sS -X POST \
169 "https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache" \
170 -H "Authorization: Bearer ${CLOUDFLARE_PURGE_TOKEN}" \
171 -H "Content-Type: application/json" \
172 --data "$files_json" \
173 --max-time 20 || echo '{"success":false,"errors":[{"message":"curl failed"}]}')
174 if echo "$CF_RESPONSE" | grep -q '"success":true'; then
175 log " purged ${#chunk[@]} URL(s)"
176 else
177 log " WARN: purge failed: $CF_RESPONSE"
178 fi
179 }
180 for url in "${PURGE_URLS[@]}"; do
181 chunk+=("$url")
182 if [ ${#chunk[@]} -ge 30 ]; then
183 purge_chunk
184 chunk=()
185 fi
186 done
187 if [ ${#chunk[@]} -gt 0 ]; then
188 purge_chunk
189 fi
190 else
191 log "skipping CF purge of ${#PURGE_URLS[@]} URL(s) — CLOUDFLARE_PURGE_TOKEN / CLOUDFLARE_ZONE_ID not set"
192 log " set them in aesthetic-computer-vault/lith/.env (uploaded to /opt/ac/system/.env on deploy)"
193 fi
194fi
195
196log "done"