I shipped a working tree earlier and had to tell John "live matches no commit in the repo" — which is exactly the sentence a deploy should make impossible. deploy.sh now writes the HEAD short-sha to web root as VERSION, and verifies it came back. `curl https://partly.party/gutsy/VERSION` answers "what is online right now" without trusting anyone's memory of the last deploy. The dirty check is scoped to `git status --porcelain -- web/`, not the whole tree, because only web/ ships: editing docs or this script does not make the payload unreproducible, and a stamp that cried wolf on every NOTES edit would get ignored. A dirty payload ships as `<sha>-dirty` and says so, loudly, rather than refusing — mid-round lanes need to push a look at something without committing first, and a deploy tool that blocks that will just get bypassed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
4.9 KiB
Bash
Executable File
99 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =====================================================================
|
|
# GUTS — deploy to https://partly.party/gutsy/
|
|
# =====================================================================
|
|
# Usage: bash tools/deploy.sh
|
|
#
|
|
# 1. QA gate (never ship red)
|
|
# 2. rsync web/ to VPS staging — MINUS web/dev/ (ruling #5: never shipped)
|
|
# 3. docker cp into forum-nginx web root
|
|
# 4. verify index AND data endpoints (see "the 200 that isn't" below)
|
|
#
|
|
# No build step: GUTS is vanilla ES modules + an importmap with three r175 vendored, so
|
|
# web/ ships as-is. All paths are relative (index.html) or resolved from import.meta.url
|
|
# (core/assets.js), which is why it runs from a subdirectory with no base rewrite.
|
|
#
|
|
# THE 200 THAT ISN'T. nginx.conf serves `root /usr/share/nginx/html` with a catch-all
|
|
# `try_files $uri $uri/ /index.html`. That's what routes /gutsy/ with no location block of
|
|
# its own — but it also means ANY missing file returns the forum's index.html with HTTP 200.
|
|
# A deploy that lost every module would still curl 200 on all of them. So each check below
|
|
# asserts on CONTENT, never on the status code alone. (This is the failure the deploy-map
|
|
# skill records as ".bin data URLs serving arcade HTML".)
|
|
#
|
|
# DURABILITY. This docker-cp's into the container's own layer, matching blobbo/glytch. It
|
|
# survives restarts and reboots (restart: unless-stopped) but NOT `docker compose up
|
|
# --force-recreate` or an nginx image bump — those wipe it, and re-running this script is
|
|
# the fix. The durable alternative is a bind mount in forum/docker-compose.yml like
|
|
# cratewars/roguelike have, which costs a forum-nginx recreate (brief partly.party downtime)
|
|
# and so is John's call to schedule, not this script's to take.
|
|
#
|
|
# Requirements: ssh humanjing@100.71.119.27 (tailscale), container forum-nginx.
|
|
# =====================================================================
|
|
set -euo pipefail
|
|
|
|
VPS="humanjing@100.71.119.27"
|
|
CONTAINER="forum-nginx"
|
|
REMOTE_WEB_ROOT="/usr/share/nginx/html/gutsy"
|
|
STAGING="/tmp/gutsy_deploy"
|
|
LIVE="https://partly.party/gutsy"
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
echo "[1/4] QA gate ..."
|
|
bash "$ROOT/tools/qa.sh" > /tmp/gutsy_qa.log 2>&1 || { echo " ✗ QA RED — not shipping. See /tmp/gutsy_qa.log"; exit 1; }
|
|
echo " ✓ QA green"
|
|
|
|
echo "[2/4] Syncing web/ to VPS staging (excluding dev harnesses) ..."
|
|
COMMIT="$(git -C "$ROOT" rev-parse --short HEAD)"
|
|
if [ -n "$(git -C "$ROOT" status --porcelain -- web/)" ]; then
|
|
STAMP="$COMMIT-dirty"
|
|
echo " ⚠ web/ has uncommitted changes — what ships will match NO commit in the repo"
|
|
else
|
|
STAMP="$COMMIT"
|
|
fi
|
|
ssh "$VPS" "rm -rf '$STAGING' && mkdir -p '$STAGING'"
|
|
rsync -az --delete --exclude='.DS_Store' --exclude='dev/' "$ROOT/web/" "$VPS:$STAGING/"
|
|
# Provenance, curl-able: `curl https://partly.party/gutsy/VERSION` answers "what is actually
|
|
# live right now" without trusting anyone's memory of the last deploy.
|
|
ssh "$VPS" "printf '%s\n' '$STAMP' > $STAGING/VERSION"
|
|
echo " $(ssh "$VPS" "du -sh $STAGING | cut -f1") staged @ $STAMP"
|
|
|
|
echo "[3/4] Replacing container content ..."
|
|
ssh "$VPS" "docker exec $CONTAINER sh -c 'rm -rf ${REMOTE_WEB_ROOT:?} && mkdir -p ${REMOTE_WEB_ROOT:?}' && docker cp $STAGING/. $CONTAINER:$REMOTE_WEB_ROOT"
|
|
|
|
echo "[4/4] Verifying live endpoints (content, not status) ..."
|
|
BUST="$(date +%s)"
|
|
fail=0
|
|
|
|
check() { # check <label> <url-suffix> <grep-pattern>
|
|
local label="$1" url="$LIVE$2?v=$BUST" pat="$3" body
|
|
body="$(curl -fsS --max-time 20 "$url" 2>/dev/null || true)"
|
|
if [ -z "$body" ]; then
|
|
echo " ✗ $label — empty/failed response"; fail=1; return
|
|
fi
|
|
# The arcade index is what try_files falls through TO. Match its real title — checked
|
|
# against the live box, not guessed; the guard is worthless if the marker is wrong.
|
|
if grep -q 'M0NST3R R0B0T P4RTY 4RC4D3' <<<"$body" 2>/dev/null; then
|
|
echo " ✗ $label — served the ARCADE index (file is missing; try_files fell through)"; fail=1; return
|
|
fi
|
|
if ! grep -q "$pat" <<<"$body" 2>/dev/null; then
|
|
echo " ✗ $label — 200 but content unrecognised"; fail=1; return
|
|
fi
|
|
echo " ✓ $label"
|
|
}
|
|
|
|
check "VERSION stamp" "/VERSION" "$STAMP"
|
|
check "index.html" "/" 'id="game"'
|
|
check "js/boot.js" "/js/boot.js" 'Lane F'
|
|
check "ui/hud.js" "/js/ui/hud.js" 'createHUD'
|
|
check "vendor/three r175" "/vendor/three.module.js" 'REVISION'
|
|
check "assets/manifest" "/assets/manifest.json" 'textures'
|
|
|
|
# the no-trailing-slash form John will actually type
|
|
REDIR="$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$LIVE?v=$BUST")"
|
|
echo " · $LIVE (no slash) -> HTTP $REDIR (301/302 to /gutsy/ expected)"
|
|
|
|
[ "$fail" = "0" ] || { echo " ✗ DEPLOY UNVERIFIED"; exit 1; }
|
|
echo " ✓ deployed: $LIVE/"
|
|
echo
|
|
echo "Cloudflare fronts partly.party — purge the cache if you redeploy over existing files."
|