The purge silently did nothing. `sed -n 's/.*"id":"\([a-f0-9]\{32\}\)".*/\1/p'`
looks right, but the zones response carries several 32-hex "id" fields
(account, plan, ...) and the leading `.*` is greedy, so it returned the
LAST one — a valid-looking id for the wrong object. The API then rejected
the purge and the old grep-for-success check reported a bare "purge
failed" with no reason.
Parse with python3 (already a dep — serve.py) and surface the API's own
error message when it fails.
Verified end to end against the live zone: cf-cache-status HIT -> purge
-> MISS -> HIT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
4.2 KiB
Bash
Executable File
95 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Deploy Molly Cool to monsterrobot.games/mollycool
|
|
#
|
|
# The lander lives on the botchat/games VPS and forum-nginx serves it from a
|
|
# bind mount of /home/humanjing/monsterrobot.games. That mount is READ-ONLY
|
|
# inside the container, so `docker cp` fails silently — always write the HOST
|
|
# path. (Same trap as beyondmorp V2.)
|
|
#
|
|
# monsterrobot.games -> /home/humanjing/monsterrobot.games (nginx root)
|
|
# monsterrobot.games/mollycool -> games/mollycool via a root symlink
|
|
#
|
|
# nginx needs no config change: the server block's `location /` try_files
|
|
# resolves the symlink, which is the same convention not-tonight uses.
|
|
set -euo pipefail
|
|
|
|
HOST="humanjing@100.71.119.27"
|
|
LANDER="/home/humanjing/monsterrobot.games"
|
|
DEST="$LANDER/games/mollycool"
|
|
URL="https://monsterrobot.games/mollycool/"
|
|
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
echo "==> shipping $SRC -> $HOST:$DEST"
|
|
# Static game only. serve.py is the dev server, test/ is the headless suite,
|
|
# and neither belongs on a public web root.
|
|
rsync -az --delete \
|
|
--include='index.html' \
|
|
--include='src/' --include='src/**' \
|
|
--exclude='*' \
|
|
"$SRC/" "$HOST:$DEST/"
|
|
|
|
echo "==> ensuring the /mollycool route"
|
|
ssh "$HOST" "ln -sfn games/mollycool '$LANDER/mollycool' && ls -ld '$LANDER/mollycool'"
|
|
|
|
# ---- Cloudflare purge --------------------------------------------------
|
|
# CF fronts monsterrobot.games and serves the modules with max-age=14400, so
|
|
# without this a redeploy leaves you running four-hour-old code — the exact
|
|
# bug the dev server's /v<timestamp>/ prefix exists to prevent, just moved
|
|
# upstream. Token is read from disk at run time and never printed.
|
|
if [ -z "${CF_API_TOKEN:-}" ] && [ -f "$SRC/deploy/.env" ]; then
|
|
# shellcheck disable=SC1091
|
|
set +u; . "$SRC/deploy/.env"; set -u
|
|
CF_API_TOKEN="${CF_API_TOKEN:-${CLOUDFLARE_API_TOKEN:-}}"
|
|
fi
|
|
|
|
if [ -n "${CF_API_TOKEN:-}" ]; then
|
|
echo "==> purging cloudflare"
|
|
API=https://api.cloudflare.com/client/v4
|
|
# Parse with python3 (already a dep — serve.py), NOT sed. The zone response
|
|
# carries several 32-hex "id" fields (account, plan, ...) and a greedy
|
|
# `.*"id":"..."` grabs the LAST one, which silently purges nothing.
|
|
ZONE=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
|
|
"$API/zones?name=monsterrobot.games" \
|
|
| python3 -c 'import sys,json;r=json.load(sys.stdin).get("result") or [];print(r[0]["id"] if r else "")')
|
|
if [ -n "$ZONE" ]; then
|
|
files=$(cd "$SRC" && find index.html src -type f \
|
|
| python3 -c 'import sys,json;print(json.dumps({"files":["https://monsterrobot.games/mollycool/"+l.strip() for l in sys.stdin if l.strip()]}))')
|
|
resp=$(curl -s -X POST "$API/zones/$ZONE/purge_cache" \
|
|
-H "Authorization: Bearer $CF_API_TOKEN" -H 'Content-Type: application/json' \
|
|
--data "$files")
|
|
echo "$resp" | python3 -c '
|
|
import sys,json
|
|
d=json.load(sys.stdin)
|
|
if d.get("success"): print(" purged")
|
|
else:
|
|
print(" !! purge failed:", "; ".join(e.get("message","?") for e in d.get("errors") or []))
|
|
print(" hard-reload to be sure")'
|
|
else
|
|
echo " !! could not resolve zone id — check the token has Zone:Read"
|
|
echo " hard-reload to be sure"
|
|
fi
|
|
else
|
|
echo "==> NO CF_API_TOKEN — skipping purge."
|
|
echo " Cloudflare holds the old JS for up to 4h. Hard-reload (cmd+shift+R),"
|
|
echo " or put CF_API_TOKEN in deploy/.env so this is automatic."
|
|
fi
|
|
|
|
echo "==> verifying live"
|
|
STAMP="$(date +%s)"
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' "$URL?cb=$STAMP")
|
|
title=$(curl -s "$URL?cb=$STAMP" | grep -o '<title>[^<]*</title>' || true)
|
|
# The real test is the module: a broken subpath serves the LANDER's html here
|
|
# with content-type text/html, which the browser silently refuses to execute.
|
|
mime=$(curl -s -o /dev/null -w '%{content_type}' "${URL}src/main.js?cb=$STAMP")
|
|
mcode=$(curl -s -o /dev/null -w '%{http_code}' "${URL}src/main.js?cb=$STAMP")
|
|
|
|
echo " page $code $title"
|
|
echo " module $mcode $mime"
|
|
|
|
if [ "$code" != "200" ] || [ "$mcode" != "200" ]; then
|
|
echo "!! NOT DEPLOYED (non-200)"; exit 1
|
|
fi
|
|
case "$title" in *"Molly Cool"*) ;; *) echo "!! wrong page served — check the symlink"; exit 1;; esac
|
|
case "$mime" in *javascript*) ;; *) echo "!! module served as '$mime', not javascript"; exit 1;; esac
|
|
echo "==> OK $URL"
|