Monorepo for Aesthetic.Computer aesthetic.computer
at main 178 lines 5.4 kB view raw
1#!/usr/bin/env bash 2set -euo pipefail 3 4# Auto-sync AT frontend pages from GitHub to the PDS Caddy container. 5# Intended for cron/systemd on the PDS server. 6# 7# Env overrides: 8# AC_REPO="whistlegraph/aesthetic-computer" 9# AC_BRANCH="main" 10# AC_CONTAINER="caddy" 11# AC_CONTAINER_WEBROOT="/data/www" 12# AC_STATE_DIR="/var/lib/at-frontend-sync" 13# AC_HEALTH_URL="https://at.aesthetic.computer/xrpc/_health" 14# AC_FILE_MAP="at/index.html:index.html;at/user-page.html:user.html;at/media-modal.js:media-modal.js;at/media-records.js:media-records.js" 15# AC_FORCE="1" # force deploy even if SHA unchanged 16# 17# AC_FILE_MAP format: 18# "repo/source/path:container/target/path;repo/source2:path2" 19 20AC_REPO="${AC_REPO:-whistlegraph/aesthetic-computer}" 21AC_BRANCH="${AC_BRANCH:-main}" 22AC_CONTAINER="${AC_CONTAINER:-caddy}" 23AC_CONTAINER_WEBROOT="${AC_CONTAINER_WEBROOT:-/data/www}" 24AC_STATE_DIR="${AC_STATE_DIR:-/var/lib/at-frontend-sync}" 25AC_HEALTH_URL="${AC_HEALTH_URL:-https://at.aesthetic.computer/xrpc/_health}" 26AC_FILE_MAP="${AC_FILE_MAP:-at/index.html:index.html;at/user-page.html:user.html;at/media-modal.js:media-modal.js;at/media-records.js:media-records.js}" 27AC_FORCE="${AC_FORCE:-0}" 28 29RAW_BASE="https://raw.githubusercontent.com/${AC_REPO}" 30REPO_URL="https://github.com/${AC_REPO}.git" 31STATE_FILE="${AC_STATE_DIR}/last_deployed_sha" 32LOCK_FILE="${AC_STATE_DIR}/sync.lock" 33TMP_DIR="$(mktemp -d /tmp/at-frontend-sync.XXXXXX)" 34COMPARE_JSON="${TMP_DIR}/compare.json" 35 36trim() { 37 local value="$1" 38 value="${value#"${value%%[![:space:]]*}"}" 39 value="${value%"${value##*[![:space:]]}"}" 40 printf "%s" "${value}" 41} 42 43declare -a SOURCE_FILES=() 44declare -a TARGET_FILES=() 45IFS=';' read -r -a FILE_MAP_ENTRIES <<< "${AC_FILE_MAP}" 46for raw_entry in "${FILE_MAP_ENTRIES[@]}"; do 47 entry="$(trim "${raw_entry}")" 48 [[ -z "${entry}" ]] && continue 49 50 if [[ "${entry}" != *:* ]]; then 51 echo "Invalid AC_FILE_MAP entry: '${entry}' (expected source:target)" >&2 52 exit 1 53 fi 54 55 source_path="$(trim "${entry%%:*}")" 56 target_path="$(trim "${entry#*:}")" 57 58 if [[ -z "${source_path}" || -z "${target_path}" ]]; then 59 echo "Invalid AC_FILE_MAP entry: '${entry}' (empty source or target)" >&2 60 exit 1 61 fi 62 63 SOURCE_FILES+=("${source_path}") 64 TARGET_FILES+=("${target_path}") 65done 66 67if [[ "${#SOURCE_FILES[@]}" -eq 0 ]]; then 68 echo "No frontend files configured. Set AC_FILE_MAP." >&2 69 exit 1 70fi 71 72cleanup() { 73 rm -rf "${TMP_DIR}" || true 74} 75trap cleanup EXIT 76 77mkdir -p "${AC_STATE_DIR}" 78 79exec 9>"${LOCK_FILE}" 80if ! flock -n 9; then 81 echo "Auto-sync already running; exiting." 82 exit 0 83fi 84 85if ! command -v docker >/dev/null 2>&1; then 86 echo "docker is required but not found." >&2 87 exit 1 88fi 89 90if ! command -v curl >/dev/null 2>&1; then 91 echo "curl is required but not found." >&2 92 exit 1 93fi 94 95if ! command -v jq >/dev/null 2>&1; then 96 echo "jq is required but not found." >&2 97 exit 1 98fi 99 100if ! docker ps --format '{{.Names}}' | grep -Fxq "${AC_CONTAINER}"; then 101 echo "Container '${AC_CONTAINER}' is not running." >&2 102 exit 1 103fi 104 105echo "Checking latest commit for ${AC_REPO}@${AC_BRANCH}..." 106LATEST_SHA="$(git ls-remote "${REPO_URL}" "refs/heads/${AC_BRANCH}" | awk '{print $1}')" 107if [[ -z "${LATEST_SHA}" ]]; then 108 echo "Could not resolve latest SHA for ${AC_REPO}@${AC_BRANCH}" >&2 109 exit 1 110fi 111 112LAST_SHA="" 113if [[ -f "${STATE_FILE}" ]]; then 114 LAST_SHA="$(cat "${STATE_FILE}")" 115fi 116 117if [[ "${AC_FORCE}" != "1" && "${LATEST_SHA}" == "${LAST_SHA}" ]]; then 118 echo "No new commit (${LATEST_SHA:0:12}); frontend is up to date." 119 exit 0 120fi 121 122if [[ "${AC_FORCE}" != "1" && -n "${LAST_SHA}" ]]; then 123 COMPARE_URL="https://api.github.com/repos/${AC_REPO}/compare/${LAST_SHA}...${LATEST_SHA}" 124 if curl -fsSL "${COMPARE_URL}" -o "${COMPARE_JSON}"; then 125 SHOULD_DEPLOY=0 126 for source_path in "${SOURCE_FILES[@]}"; do 127 if jq -e --arg source_path "${source_path}" '.files[]? | select(.filename == $source_path)' "${COMPARE_JSON}" >/dev/null; then 128 SHOULD_DEPLOY=1 129 break 130 fi 131 done 132 133 if [[ "${SHOULD_DEPLOY}" == "0" ]]; then 134 echo "No tracked frontend files changed; skipping deploy for ${LATEST_SHA:0:12}." 135 echo "${LATEST_SHA}" > "${STATE_FILE}" 136 exit 0 137 fi 138 else 139 echo "Could not compare commit diff. Continuing with deploy." 140 fi 141fi 142 143echo "Deploying frontend for commit ${LATEST_SHA:0:12}..." 144 145declare -a TMP_FILES=() 146for i in "${!SOURCE_FILES[@]}"; do 147 source_path="${SOURCE_FILES[$i]}" 148 target_path="${TARGET_FILES[$i]}" 149 source_url="${RAW_BASE}/${LATEST_SHA}/${source_path}" 150 tmp_file="${TMP_DIR}/${i}-$(basename "${target_path}")" 151 152 curl -fsSL "${source_url}" -o "${tmp_file}" 153 if [[ "${source_path}" == *.html ]] && ! grep -qi "<!doctype html>" "${tmp_file}"; then 154 echo "Downloaded ${source_path} does not look like HTML." >&2 155 exit 1 156 fi 157 158 TMP_FILES+=("${tmp_file}") 159done 160 161for i in "${!TARGET_FILES[@]}"; do 162 target_path="${TARGET_FILES[$i]}" 163 tmp_file="${TMP_FILES[$i]}" 164 target_dir="$(dirname "${target_path}")" 165 if [[ "${target_dir}" != "." ]]; then 166 docker exec "${AC_CONTAINER}" mkdir -p "${AC_CONTAINER_WEBROOT}/${target_dir}" 167 fi 168 docker cp "${tmp_file}" "${AC_CONTAINER}:${AC_CONTAINER_WEBROOT}/${target_path}" 169done 170 171echo "${LATEST_SHA}" > "${STATE_FILE}" 172 173if [[ -n "${AC_HEALTH_URL}" ]]; then 174 echo "Running health check..." 175 curl -fsSL "${AC_HEALTH_URL}" >/dev/null 176fi 177 178echo "AT frontend sync complete (${LATEST_SHA:0:12})."