Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 196 lines 6.1 kB view raw
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"