ops: add deploy-artifact-to-eric-turner.sh to force Hugo rebuild

Fix a recurring gotcha where scp'd static file changes to the Hugo site
weren't being picked up by the live URL. The interactive HTML artifact
at eric-turner.com/memex/signal-and-noise.html reverted to a stale
version TWICE because Hugo's `--poll 1s` file watcher doesn't reliably
detect changes to files in new static/ subdirectories — the file on
disk updates, but Hugo's in-memory cache stays stuck.

Root cause: Hugo runs as `hugo server --poll 1s` with a Docker bind
mount on the showcase directory. The file watcher catches top-level
changes but misses subsequent updates to subdirectories that weren't
present when Hugo first scanned static/. Result: first scp works
(Hugo discovers the new subdir), later scp updates are silently
ignored.

The durable fix: touch hugo.yaml after any scp. Hugo's config-file
watcher is separate from the static/ poll watcher and is reliable —
touching hugo.yaml triggers a full rebuild which invalidates the
stale static cache.

This script wraps scp + touch + an optional --verify that fetches the
live URL and compares the title and tab-count against the local file.
Use this instead of raw scp for any future deployments of the
interactive artifact.

The lesson is also recorded in ~/.claude/CLAUDE.md under "Deploying
to eric-turner.com (Hugo + Strapi site)" so future sessions don't
repeat the same mistake.
This commit is contained in:
Eric Turner
2026-04-12 22:42:24 -06:00
parent 997aa837de
commit 9bfed1582b

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail
# deploy-artifact-to-eric-turner.sh
#
# Deploy the Signal & Noise interactive HTML artifact to the eric-turner.com
# Hugo site, then force Hugo to pick up the change by touching hugo.yaml.
#
# Why touch hugo.yaml? Hugo runs as `hugo server --poll 1s` in a Docker
# bind-mount. The poll watcher detects changes in the top-level file tree
# reliably, but NEW subdirectories under static/ (added via scp after the
# server started) don't always get picked up by the watcher's in-memory
# state. The config-file watch is more reliable — touching hugo.yaml
# triggers a full rebuild, which re-reads all static/ subdirectories and
# invalidates any stale file cache.
#
# This is the durable fix for the "I scp'd the file but the live URL still
# shows the old version" problem we hit twice. Always use this script
# (or `touch hugo.yaml` after any manual scp) instead of raw scp.
#
# Usage:
# bash scripts/deploy-artifact-to-eric-turner.sh
# bash scripts/deploy-artifact-to-eric-turner.sh --verify
#
# Requires SSH access to root@10.13.0.254 and Hugo running in
# /mnt/user/docker/showcase.
REMOTE_HOST="root@10.13.0.254"
REMOTE_DIR="/mnt/user/docker/showcase"
REMOTE_STATIC_DIR="${REMOTE_DIR}/static/memex"
REMOTE_HUGO_CONFIG="${REMOTE_DIR}/hugo.yaml"
PUBLIC_URL="https://eric-turner.com/memex/signal-and-noise.html"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_HTML="${REPO_ROOT}/docs/artifacts/signal-and-noise.html"
VERIFY=false
while [[ $# -gt 0 ]]; do
case "$1" in
--verify) VERIFY=true; shift ;;
-h|--help)
sed -n '3,27p' "$0" | sed 's/^# \?//'
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
if [[ ! -f "${LOCAL_HTML}" ]]; then
echo "Local file not found: ${LOCAL_HTML}" >&2
exit 1
fi
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; }
log "uploading $(basename "${LOCAL_HTML}") ($(wc -c < "${LOCAL_HTML}") bytes) to ${REMOTE_HOST}:${REMOTE_STATIC_DIR}/"
ssh "${REMOTE_HOST}" "mkdir -p ${REMOTE_STATIC_DIR}"
scp -q "${LOCAL_HTML}" "${REMOTE_HOST}:${REMOTE_STATIC_DIR}/signal-and-noise.html"
log "touching ${REMOTE_HUGO_CONFIG} to trigger Hugo config reload"
ssh "${REMOTE_HOST}" "touch ${REMOTE_HUGO_CONFIG}"
log "waiting 3s for Hugo to rebuild..."
sleep 3
if ${VERIFY}; then
log "verifying live URL content..."
live_content="$(curl -sS --max-time 10 "${PUBLIC_URL}")"
local_title="$(grep -o '<title>[^<]*</title>' "${LOCAL_HTML}" | head -1)"
live_title="$(echo "${live_content}" | grep -o '<title>[^<]*</title>' | head -1)"
if [[ "${local_title}" == "${live_title}" ]]; then
log "✓ title matches: ${live_title}"
else
log "✗ title mismatch!"
log " local: ${local_title}"
log " live: ${live_title}"
log " Hugo may still be rebuilding. Retry in a few seconds or check ssh ${REMOTE_HOST} 'docker logs showcase-hugo-1'"
exit 1
fi
# Count tab buttons — match both "tab-btn" and "tab-btn active"
local_tabs="$(grep -cE 'class="tab-btn[^"]*".*onclick' "${LOCAL_HTML}" || true)"
live_tabs="$(echo "${live_content}" | grep -cE 'class="tab-btn[^"]*".*onclick' || true)"
if [[ "${local_tabs}" == "${live_tabs}" ]]; then
log "✓ tab count matches: ${live_tabs} tabs"
else
log "✗ tab count mismatch: local=${local_tabs} live=${live_tabs}"
exit 1
fi
fi
log "deployed → ${PUBLIC_URL}"