Monorepo for Aesthetic.Computer
aesthetic.computer
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})."