Audit item 14. THE DEPLOY VERIFIED THE ONE URL A PLAYER NEVER LOADS. Every check in deploy.sh appends ?v=$BUST — which is exactly what makes it bypass Cloudflare's edge — so all six could pass green while every actual player was still being served the previous build from a POP. The script's answer was a line of prose at the end asking a human to remember to purge. It now fetches js/boot.js the way a browser does, with no query string, and byte-compares it to what was just shipped; a stale edge fails the deploy and prints the purge steps. No credentials needed, and it asks the question the prose was only gesturing at. This is the same mistake as the `assets` bug and the unwinnable saliva tide: verifying the mechanism rather than the path the player actually takes through it. Third time, so it is written into the tool this time rather than into a comment. QUALITY SWITCH. renderer.setPixelRatio was hardcoded to min(dpr, 2), so a retina laptop drew four times the pixels of a 1x one with no way for its owner to decline — and this game is fill-bound, not geometry-bound (fogged tube, additive glows, a full-screen damage layer), so that is the lever that moves. `performance` caps it at 1. On the title beside difficulty, saved, same reload path (the pixel ratio is set at boot, before any card exists). save.js's two one-line setters collapsed into setOpt(key, value) now that there are two options and a third is obvious. Note on verification: the chip persists and boot reads it, confirmed live. The resolution change itself is not observable in the harness — that browser pane runs at devicePixelRatio 1, where min(1,1) and min(1,2) are the same number. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
119 lines
6.1 KiB
Bash
Executable File
119 lines
6.1 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/"
|
|
|
|
# ---------------------------------------------------------------------
|
|
# THE CHECK ABOVE VERIFIES A URL NOBODY VISITS.
|
|
# Every check() appends ?v=$BUST, which is precisely what makes it bypass Cloudflare's edge
|
|
# cache — so all six can pass while every actual player is still being served the PREVIOUS
|
|
# build from a POP. The script's answer to that was a line of prose at the end telling a human
|
|
# to remember. This asks the question properly: fetch a module the way a browser does, with no
|
|
# query string, and compare it to the bytes we just shipped.
|
|
# ---------------------------------------------------------------------
|
|
echo "[5/5] Checking what a PLAYER gets (no cache-buster) ..."
|
|
EDGE="$(curl -fsS --max-time 20 "$LIVE/js/boot.js" 2>/dev/null || true)"
|
|
if [ -z "$EDGE" ]; then
|
|
echo " ⚠ could not fetch $LIVE/js/boot.js un-busted — cannot tell whether the edge is stale"
|
|
elif [ "$EDGE" = "$(cat "$ROOT/web/js/boot.js")" ]; then
|
|
echo " ✓ edge is serving this build"
|
|
else
|
|
echo " ✗ STALE EDGE — players are still being served the previous build."
|
|
echo " Cloudflare has partly.party/gutsy/js/*.js cached. Purge it:"
|
|
echo " Cloudflare dashboard -> partly.party -> Caching -> Configuration -> Purge Everything"
|
|
echo " (or Custom Purge on $LIVE/js/). Then re-run this script to confirm."
|
|
exit 1
|
|
fi
|