#!/usr/bin/env bash ############################################################################### # ship_space.sh — one-shot shipper for Phantom Grid -> Hugging Face Space. # # Copy this whole folder to a Linux machine (or run under WSL) and run: # # export HF_TOKEN=hf_xxx_your_WRITE_token # must be a WRITE token # ./ship_space.sh # # It will: # 1. Preflight: check tooling + that the HF token can WRITE. # 2. (optional) Build the Docker image locally and smoke-test it. # 3. Create the Space build-small-hackathon/phantom-grid (SDK: docker). # 4. Upload the app (excluding .venv/runtime/etc) + the Space README. # 5. Print the Space URL and the remaining manual steps. # # Flags: # --build Build the Docker image locally before pushing. # --smoke Build + run the container and probe health (implies --build). # --dry-run Do everything EXCEPT create_repo / upload (no network writes). # --no-push Alias for --dry-run. # -h | --help Show this help. # # Env overrides: # HF_TOKEN write token (required to push) # HF_ORG default: build-small-hackathon # HF_SPACE default: phantom-grid ############################################################################### set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$HERE" HF_ORG="${HF_ORG:-build-small-hackathon}" HF_SPACE="${HF_SPACE:-phantom-grid}" REPO_ID="${HF_ORG}/${HF_SPACE}" IMAGE_TAG="phantom-grid:local" DO_BUILD=0 DO_SMOKE=0 DRY_RUN=0 for arg in "$@"; do case "$arg" in --build) DO_BUILD=1 ;; --smoke) DO_SMOKE=1; DO_BUILD=1 ;; --dry-run|--no-push) DRY_RUN=1 ;; -h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "Unknown flag: $arg" >&2; exit 2 ;; esac done log() { printf '\033[1;36m[ship]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[ship] WARN:\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31m[ship] ERROR:\033[0m %s\n' "$*" >&2; exit 1; } ############################################################################### # 1. Preflight ############################################################################### log "Preflight checks..." command -v python3 >/dev/null || die "python3 is required." # huggingface_hub is needed to create + upload the Space. if ! python3 -c "import huggingface_hub" 2>/dev/null; then log "Installing huggingface_hub..." python3 -m pip install --quiet --upgrade "huggingface_hub>=1.2" fi # Validate required deliverable files exist. for f in Dockerfile entrypoint.sh .dockerignore requirements-space.txt README_SPACE.md app.py; do [ -f "$f" ] || die "Missing required file: $f" done log "All required deployment files present." # Validate the app + entrypoint are syntactically sound before shipping. python3 -m py_compile app.py config/settings.py grid_map/map_loader.py llm/omni_client.py \ || die "Python syntax check failed." bash -n entrypoint.sh || die "entrypoint.sh has a syntax error." log "Syntax checks passed." ############################################################################### # 2. Optional local Docker build / smoke test ############################################################################### if [ "$DO_BUILD" = 1 ]; then command -v docker >/dev/null || die "--build requested but docker is not installed." log "Building Docker image ${IMAGE_TAG} (downloads the GGUF — this is slow)..." docker build -t "$IMAGE_TAG" . log "Docker build succeeded." fi if [ "$DO_SMOKE" = 1 ]; then log "Smoke-testing the container..." CID="$(docker run -d -p 7860:7860 "$IMAGE_TAG")" cleanup_smoke() { docker rm -f "$CID" >/dev/null 2>&1 || true; } trap cleanup_smoke EXIT log "Container ${CID:0:12} started; waiting for the app to answer on :7860..." ok=0 for i in $(seq 1 150); do if curl -sf "http://127.0.0.1:7860/api/snapshot" >/dev/null 2>&1; then ok=1; break; fi if ! docker ps -q --no-trunc | grep -q "$CID"; then docker logs "$CID" | tail -40 >&2 die "Container exited during smoke test." fi sleep 4 done [ "$ok" = 1 ] || { docker logs "$CID" | tail -40 >&2; die "App did not become healthy in time."; } log "Smoke test passed: /api/snapshot responded." cleanup_smoke trap - EXIT fi ############################################################################### # 3 + 4. Create the Space and upload ############################################################################### if [ "$DRY_RUN" = 1 ]; then log "--dry-run: skipping create_repo + upload. Everything else passed." log "Would push to: https://huggingface.co/spaces/${REPO_ID}" exit 0 fi [ -n "${HF_TOKEN:-}" ] || die "HF_TOKEN is not set. Export a WRITE token: export HF_TOKEN=hf_..." log "Creating + uploading Space ${REPO_ID} ..." HF_ORG="$HF_ORG" HF_SPACE="$HF_SPACE" python3 - <<'PY' import os, sys from huggingface_hub import HfApi token = os.environ["HF_TOKEN"] org = os.environ["HF_ORG"] space = os.environ["HF_SPACE"] repo_id = f"{org}/{space}" api = HfApi(token=token) # Verify the token can write. who = api.whoami() perm = who.get("auth", {}).get("accessToken", {}).get("role") or who.get("auth", {}).get("type") print(f"[ship] Logged in as: {who.get('name')} (token role: {perm})") if perm == "read": sys.exit("[ship] ERROR: this is a READ token. Create a WRITE token at " "https://huggingface.co/settings/tokens and re-export HF_TOKEN.") api.create_repo(repo_id=repo_id, repo_type="space", space_sdk="docker", exist_ok=True) print(f"[ship] Space ready: https://huggingface.co/spaces/{repo_id}") ignore = [ ".git*", ".venv/*", "venv/*", "runtime/*", "tools/*", "**/__pycache__/*", "*.pyc", "tmp/*", "*.log", "*.err.log", "data/backups/*", "data/games/*", "data/archives/*", "data/raw/archives/*", "data/*.log", "data/*.png", "run_game.ps1", "run_game.cmd", "README.md", "README_SPACE.md", "C:*", ] api.upload_folder( repo_id=repo_id, repo_type="space", folder_path=".", ignore_patterns=ignore, commit_message="Ship Phantom Grid Docker Space", ) # The Space README (with the docker frontmatter + track tag) goes in as README.md. api.upload_file( path_or_fileobj="README_SPACE.md", path_in_repo="README.md", repo_id=repo_id, repo_type="space", commit_message="Add Space README", ) print(f"[ship] Upload complete. Build will start automatically.") print(f"[ship] Watch the build: https://huggingface.co/spaces/{repo_id}?logs=build") PY log "Done. Remaining manual steps:" log " - Record the demo video and paste its link into README_SPACE.md (then re-run, or edit on the Space)." log " - Publish the social post and add its link too." log " - Confirm the Space build goes RUNNING and a new case starts."