#!/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