#!/usr/bin/env bash # Deploy commands: guide:next (dev guide to mc.next.black.local). # # This module is Tourguide-owned (see .project/team-leads/tourguide.md). # # Host configuration (from .env or shell rc): # NEXT_DEPLOY_HOST — SSH target for mc.next.black.local static root. # Default: "lilith@black.local". # NEXT_DEPLOY_PATH — destination directory on the remote host. # Default: "/bigdisk/next/mc/". # # The target host-nginx vhost + bind-mount are set up in # `/bigdisk/nginx/nginx.conf` + `/bigdisk/nginx/docker-compose.yml` on # black.local. Infra + objective: p1-15. : "${NEXT_DEPLOY_HOST:=lilith@black.local}" : "${NEXT_DEPLOY_PATH:=/bigdisk/next/mc/}" # Bake-simcache scenarios. Empty = skip bake step entirely in `deploy:guide:next`. # Comma-separated list = bake those scenarios after `pnpm build`. `all` = every # canonical scenario (1.1 GB × 6 ≈ 6.6 GB; minutes to bake). Default: empty # (deploy ships only the client-WASM fallback path). : "${DEPLOY_BAKE_SCENARIOS:=}" cmd_bake_simcache() { # `./run bake:simcache [ids…]` — pre-compute sim-cache frames into dist/. # Pass scenario ids (space- or comma-separated) or the string `all`. # Used standalone (post-`pnpm build`) or invoked from cmd_deploy_guide_next. local scenarios="${1:-all}"; shift || true if [ "$scenarios" = "all" ]; then scenarios="" # empty arg list = bake-simcache defaults to ALL_SCENARIO_IDS else scenarios="${scenarios//,/ }" fi if [ ! -d "$GUIDE_DIR/dist" ]; then echo -e "${RED}✗ $GUIDE_DIR/dist missing — run \`pnpm build\` first, then \`./run bake:simcache\`.${NC}" return 1 fi echo -e "${BLUE}Baking sim-cache frames into $GUIDE_DIR/dist/__sim-cache/${NC}" # shellcheck disable=SC2086 (cd "$GUIDE_DIR" && node --import tsx/esm tools/bake-simcache.ts $scenarios) } cmd_deploy_guide_next() { # Build the dev bundle (all EpisodeGate subtrees visible) + rsync to black. # Safe to run repeatedly — rsync --delete replaces the target dir with dist/. # # **Bake delegation**: when DEPLOY_BAKE_SCENARIOS is set AND this is NOT # already running on apricot, SSH-delegate the entire pipeline to apricot # so the heavy sim compute happens on the run host (CPU to spare, WASM # already built). Apricot pulls latest main from forge, then invokes this # same script there — which runs locally (no further delegation) and # rsyncs to black via apricot's `Host black` SSH config alias. local local_hostname local_hostname="$(hostname -s 2>/dev/null || hostname)" if [ -n "$DEPLOY_BAKE_SCENARIOS" ] && [ "$local_hostname" != "apricot" ]; then echo -e "${BLUE}[delegate] DEPLOY_BAKE_SCENARIOS=$DEPLOY_BAKE_SCENARIOS — offloading bake+deploy to $AUTOPLAY_HOST (apricot has processing power to spare)${NC}" if ! ssh -o ConnectTimeout=5 "$AUTOPLAY_HOST" "test -d $PROJECT_ROOT_REMOTE/.git" 2>/dev/null; then echo -e "${RED}✗ $AUTOPLAY_HOST:$PROJECT_ROOT_REMOTE is not a git clone — check PROJECT_ROOT_REMOTE in .env.${NC}" return 1 fi ssh "$AUTOPLAY_HOST" " set -e cd $PROJECT_ROOT_REMOTE git fetch --prune origin git checkout main git reset --hard origin/main export DEPLOY_BAKE_SCENARIOS='$DEPLOY_BAKE_SCENARIOS' export NEXT_DEPLOY_HOST='${NEXT_DEPLOY_HOST:-black}' export NEXT_DEPLOY_PATH='$NEXT_DEPLOY_PATH' # Keep WASM fresh — apricot is the canonical build host. (cd src/simulator && bash build-wasm.sh) ./run deploy:guide:next " return $? fi # Prerequisite: WASM artifact present locally. The Vite build imports from # the @magic-civ/physics-rs alias which resolves to .local/build/wasm/. if [ ! -f "$REPO_ROOT/.local/build/wasm/magic_civ_physics.js" ]; then echo -e "${RED}✗ .local/build/wasm/magic_civ_physics.js missing — can't build guide.${NC}" echo -e "${RED} Build locally: (cd src/simulator && bash build-wasm.sh)${NC}" echo -e "${RED} Or rsync from apricot:${NC}" echo -e "${RED} rsync -a \"\$AUTOPLAY_HOST\":\"\$PROJECT_ROOT_REMOTE\"/.local/build/wasm/ .local/build/wasm/${NC}" return 1 fi echo -e "${BLUE}[1/5] Building dev bundle (VITE_DEV_GUIDE=1 pnpm build)...${NC}" if ! (cd "$GUIDE_DIR" && VITE_DEV_GUIDE=1 pnpm build 2>&1); then echo -e "${RED}✗ pnpm build failed${NC}" return 1 fi local dist="$GUIDE_DIR/dist" if [ ! -f "$dist/index.html" ]; then echo -e "${RED}✗ $dist/index.html not produced — build reported success but emitted no bundle.${NC}" return 1 fi local size size="$(du -sh "$dist" | cut -f1)" echo -e "${GREEN}✓ dist/ ready ($size)${NC}" if [ -n "$DEPLOY_BAKE_SCENARIOS" ]; then echo -e "${BLUE}[2/5] Baking sim-cache scenarios: $DEPLOY_BAKE_SCENARIOS${NC}" if ! cmd_bake_simcache "$DEPLOY_BAKE_SCENARIOS"; then echo -e "${RED}✗ bake-simcache failed${NC}" return 1 fi size="$(du -sh "$dist" | cut -f1)" echo -e "${GREEN}✓ dist/ with baked frames ($size)${NC}" else echo -e "${YELLOW}[2/5] DEPLOY_BAKE_SCENARIOS unset — skipping sim-cache bake (client-WASM fallback in browser).${NC}" fi echo -e "${BLUE}[3/5] Verifying SSH to $NEXT_DEPLOY_HOST...${NC}" if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "$NEXT_DEPLOY_HOST" "test -d $NEXT_DEPLOY_PATH && echo ok" >/dev/null 2>&1; then echo -e "${RED}✗ can't reach $NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH (VPN or permissions).${NC}" return 1 fi echo -e "${GREEN}✓ reachable${NC}" echo -e "${BLUE}[4/5] Rsyncing dist/ → $NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH${NC}" if ! rsync -az --delete "$dist/" "$NEXT_DEPLOY_HOST:$NEXT_DEPLOY_PATH"; then echo -e "${RED}✗ rsync failed${NC}" return 1 fi echo -e "${GREEN}✓ deployed${NC}" echo -e "${BLUE}[5/5] Probing https://mc.next.black.local ...${NC}" local http_status http_status="$(curl -sk -o /dev/null -w "%{http_code}" --max-time 10 https://mc.next.black.local)" if [ "$http_status" = "200" ]; then echo -e "${GREEN}✓ https://mc.next.black.local → HTTP 200${NC}" else echo -e "${YELLOW}! https://mc.next.black.local → HTTP $http_status (expected 200)${NC}" echo -e "${YELLOW} Deploy may still be correct; check the vhost config on $NEXT_DEPLOY_HOST.${NC}" return 1 fi # MIME sanity: if the vhost's http{} block is missing `include mime.types;` # nginx serves .js as text/plain and browsers refuse to run the ES-module # shell (blank page, "disallowed MIME type" console errors). Catch that # regression here rather than on the next user load. local js_asset js_mime js_asset="$(ls "$dist"/assets/index-*.js 2>/dev/null | head -1)" if [ -n "$js_asset" ]; then js_mime="$(curl -sk --max-time 10 -o /dev/null -w '%{content_type}' "https://mc.next.black.local/assets/$(basename "$js_asset")")" case "$js_mime" in application/javascript*|text/javascript*) echo -e "${GREEN}✓ JS MIME: $js_mime${NC}" ;; *) echo -e "${RED}✗ JS asset served as '$js_mime' (expected application/javascript).${NC}" echo -e "${RED} Add 'include /etc/nginx/mime.types; default_type application/octet-stream;' to the http{} block of /bigdisk/nginx/nginx.conf on $NEXT_DEPLOY_HOST, then:${NC}" echo -e "${RED} docker exec host-nginx nginx -t && docker exec host-nginx nginx -s reload${NC}" return 1 ;; esac fi echo -e "${GREEN}Deployed dev guide to https://mc.next.black.local${NC}" }