diff --git a/deploy.sh b/deploy.sh index 3463f62..9041977 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Deploy latest code from git and rebuild containers. -# Usage: ./deploy.sh (rebuilds site, api, workers) -# ./deploy.sh site (rebuilds only site) -# ./deploy.sh api (rebuilds only api) +# Usage: ./deploy.sh (rebuilds site, api, workers) +# ./deploy.sh site (rebuilds only site) +# ./deploy.sh api (rebuilds only api) +# ./deploy.sh erpnext (rebuild + migrate ERPNext, re-extract assets) +# ./deploy.sh api workers (rebuild a custom set) set -euo pipefail cd /opt/performancewest @@ -19,11 +21,44 @@ echo "" echo "=== Restarting: $SERVICES ===" docker compose up -d $SERVICES +# ── ERPNext: migrate, then ALWAYS re-extract the host asset copy ───────────── +# Frappe emits content-hashed asset filenames; an ERPNext rebuild/migrate +# changes the hashes. If we don't re-sync the host copy that nginx serves for +# portal.performancewest.net, every asset 404s and the portal loses all CSS. +# So any time erpnext is (re)built we run bench migrate + re-extract assets. +case " $SERVICES " in + *" erpnext "*) + echo "" + echo "=== ERPNext: bench migrate ===" + docker compose exec -T erpnext bench --site performancewest.net migrate || \ + docker compose exec -T erpnext bench migrate || true + echo "" + echo "=== ERPNext: re-extracting static assets for the portal ===" + sudo ./extract-erpnext-assets.sh + ;; +esac + echo "" echo "=== Clearing nginx cache ===" sudo rm -rf /var/cache/nginx/* 2>/dev/null || true sudo nginx -s reload 2>/dev/null || true +# ── Portal asset drift guard ──────────────────────────────────────────────── +# Cheap safety net on EVERY deploy: if the portal's manifest references a CSS +# bundle that is missing from the host copy, the portal CSS is broken — detect +# it and auto-heal by re-extracting. This catches drift from any source +# (out-of-band ERPNext restarts, image pulls, etc.). +if docker inspect performancewest-erpnext-1 >/dev/null 2>&1; then + LOGIN_HASH="$(docker exec performancewest-erpnext-1 sh -c \ + "grep -o 'login.bundle.[A-Z0-9]*.css' /home/frappe/frappe-bench/sites/assets/assets.json | head -1" 2>/dev/null || true)" + if [ -n "$LOGIN_HASH" ] && \ + [ ! -f "/opt/erpnext-assets/assets/frappe/dist/css/${LOGIN_HASH}" ]; then + echo "" + echo "=== Portal asset drift detected (${LOGIN_HASH} missing) — re-extracting ===" + sudo ./extract-erpnext-assets.sh + fi +fi + echo "" echo "=== Done ===" git log --oneline -1 diff --git a/extract-erpnext-assets.sh b/extract-erpnext-assets.sh new file mode 100755 index 0000000..70912ca --- /dev/null +++ b/extract-erpnext-assets.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# extract-erpnext-assets.sh — Sync ERPNext/Frappe static assets to the host +# directory that nginx serves for portal.performancewest.net. +# +# WHY THIS EXISTS +# The portal serves Frappe's static assets from a host copy at +# /opt/erpnext-assets/assets/ (fast, long-cache, off-loads ERPNext). Frappe +# emits content-hashed filenames (e.g. login.bundle.SSJHYWGF.css) and a +# manifest (sites/assets/assets.json). Every `bench build`/`bench migrate` +# (which runs on an ERPNext image rebuild) changes those hashes. If the host +# copy is not re-synced, the HTML references new hashes that 404 on the host +# → the portal renders with NO CSS. +# +# This script re-extracts the current assets so the host copy and the +# manifest always match. It is idempotent and safe to run any time. +# +# RUN IT after: an ERPNext image rebuild, `bench build`, or `bench migrate`. +# deploy.sh calls it automatically for the `erpnext` target. +set -euo pipefail + +CONTAINER="${ERPNEXT_CONTAINER:-performancewest-erpnext-1}" +DEST="${ERPNEXT_ASSETS_DIR:-/opt/erpnext-assets/assets}" + +# docker may need sudo depending on the host; honor a DOCKER override. +DOCKER="${DOCKER:-sudo -n docker}" + +echo "=== Extracting ERPNext assets from ${CONTAINER} -> ${DEST} ===" + +if ! $DOCKER inspect "$CONTAINER" >/dev/null 2>&1; then + echo "ERROR: container ${CONTAINER} not found" >&2 + exit 1 +fi + +# Make sure the in-container manifest matches the built bundles. A bare image +# rebuild can leave assets.json stale relative to dist/, which also breaks CSS. +# `bench build` regenerates both consistently; cheap no-op if already current. +echo "--- Ensuring assets are built (bench build) ---" +$DOCKER exec "$CONTAINER" bench build >/tmp/erpnext-build.log 2>&1 || { + echo "WARN: bench build returned non-zero; see /tmp/erpnext-build.log" >&2 +} + +sudo rm -rf "${DEST}/frappe" "${DEST}/erpnext" +sudo mkdir -p "$DEST" + +# frappe + erpnext public/ dirs (sites/assets/ symlinks here). +$DOCKER exec "$CONTAINER" tar cf - \ + -C /home/frappe/frappe-bench/apps/frappe/frappe public \ + | sudo tar xf - -C "$DEST" --transform='s|^public|frappe|' +$DOCKER exec "$CONTAINER" tar cf - \ + -C /home/frappe/frappe-bench/apps/erpnext/erpnext public \ + | sudo tar xf - -C "$DEST" --transform='s|^public|erpnext|' + +# The manifest nginx/Frappe read for hash->file lookups. +$DOCKER exec "$CONTAINER" cat \ + /home/frappe/frappe-bench/sites/assets/assets.json \ + | sudo tee "${DEST}/assets.json" >/dev/null + +sudo chown -R www-data:www-data /opt/erpnext-assets +sudo nginx -s reload 2>/dev/null || true + +# Verify the manifest's login bundle actually exists on the host now. +LOGIN_HASH="$($DOCKER exec "$CONTAINER" sh -c \ + "grep -o 'login.bundle.[A-Z0-9]*.css' /home/frappe/frappe-bench/sites/assets/assets.json | head -1" || true)" +if [ -n "$LOGIN_HASH" ] && [ ! -f "${DEST}/frappe/dist/css/${LOGIN_HASH}" ]; then + echo "ERROR: manifest references ${LOGIN_HASH} but it is missing on host." >&2 + echo " Portal CSS would be broken. Aborting." >&2 + exit 2 +fi + +echo "=== Done. Assets at ${DEST} (login bundle: ${LOGIN_HASH:-unknown}) ==="