#!/usr/bin/env bash # ============================================================================= # scripts/deploy_to_hf.sh — one-command deploy to Hugging Face Spaces. # ============================================================================= # Strategy: snapshot the current main commit into a fresh orphan branch, swap # README.md for the HF-flavored version (with the YAML frontmatter Spaces # needs), force-push that single-commit branch as HF's `main`, then clean up. # # Why an orphan branch (not a regular merge / push of main)? # 1. HF Spaces requires YAML frontmatter at the top of README.md so it # knows to launch a Docker container. GitHub's full README has none — # we only want the swap to live on the deploy branch, not on main. # 2. main's git history is ~1000+ commits. HF pushes carry the full # reachable history, and any historical commit that ever held a # binary >10 MB would fail HF's pre-receive hook. Orphan = 1 commit, # no history to litigate. # # Prerequisites: # * `hf` remote already configured (run once: # git remote add hf https://huggingface.co/spaces/messili/polyglot-alpha) # * `hf auth login` already done (token cached by huggingface_hub CLI). # * Working tree is clean on main, or untracked-only. # ----------------------------------------------------------------------------- set -euo pipefail # ---- color/log helpers ------------------------------------------------------ if [[ -t 1 ]]; then BLUE=$'\033[34m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; RED=$'\033[31m'; RESET=$'\033[0m' else BLUE=''; GREEN=''; YELLOW=''; RED=''; RESET='' fi log() { printf '%s[deploy]%s %s\n' "$BLUE" "$RESET" "$*"; } ok() { printf '%s[deploy]%s %s\n' "$GREEN" "$RESET" "$*"; } warn() { printf '%s[deploy]%s %s\n' "$YELLOW" "$RESET" "$*" >&2; } die() { printf '%s[deploy]%s %s\n' "$RED" "$RESET" "$*" >&2; exit 1; } REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" # ---- preflight checks ------------------------------------------------------- log "preflight checks" git remote get-url hf > /dev/null 2>&1 || die "hf remote not configured. Run: git remote add hf https://huggingface.co/spaces/messili/polyglot-alpha" [[ -f deploy/hf-readme.md ]] || die "deploy/hf-readme.md missing — HF needs the YAML frontmatter version" [[ -f Dockerfile ]] || die "Dockerfile missing — HF Spaces is a Docker SDK Space" [[ -f deploy/nginx.conf ]] || die "deploy/nginx.conf missing — required by Dockerfile" [[ -f deploy/entrypoint.sh ]] || die "deploy/entrypoint.sh missing — required by Dockerfile" # Working tree state: tracked changes block deploy, untracked are fine. if ! git diff-index --quiet HEAD --; then die "working tree has uncommitted tracked changes; commit or stash first" fi # Contract ABIs (contracts/out/*.json) are not tracked on main (excluded by # contracts/.gitignore from Foundry), but the backend NEEDS them at runtime # to know the deployed contract interfaces. Build them now so the orphan # snapshot includes a freshly compiled set. command -v forge >/dev/null 2>&1 || die "forge not installed — install Foundry: https://book.getfoundry.sh/getting-started/installation" log "running forge build to refresh contracts/out/ (compiled ABIs)" ( cd contracts && forge build --silent ) || die "forge build failed — fix contracts before deploying" [[ -d contracts/out ]] || die "forge build succeeded but contracts/out/ missing — something is very wrong" ORIGINAL_BRANCH="$(git rev-parse --abbrev-ref HEAD)" if [[ "$ORIGINAL_BRANCH" != "main" ]]; then warn "currently on '$ORIGINAL_BRANCH' (not main) — will checkout main" git checkout main fi MAIN_SHA_SHORT="$(git rev-parse --short HEAD)" TMP_BRANCH="hf-deploy-$(date +%Y%m%d-%H%M%S)" log "deploying main @ $MAIN_SHA_SHORT → hf:main (via temp orphan '$TMP_BRANCH')" # ---- cleanup trap ----------------------------------------------------------- # If anything below fails, restore the user to their starting branch, restore # contracts/.git if we moved it, and delete the temp orphan branch so they # don't end up stuck on a half-built orphan. CONTRACTS_GIT_MOVED=0 cleanup() { local exit_code=$? # Restore contracts/.git if we renamed it. if [[ "$CONTRACTS_GIT_MOVED" -eq 1 && -d contracts/.git.deploy-tmp ]]; then mv contracts/.git.deploy-tmp contracts/.git fi # Switch off temp branch and delete it. local current current="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)" if [[ "$current" == "$TMP_BRANCH" || "$current" == "HEAD" ]]; then git checkout --force "$ORIGINAL_BRANCH" 2>/dev/null || git checkout --force main 2>/dev/null || true fi if [[ -n "${TMP_BRANCH:-}" ]] && git show-ref --verify --quiet "refs/heads/$TMP_BRANCH"; then git branch -D "$TMP_BRANCH" > /dev/null 2>&1 || true fi if [[ $exit_code -ne 0 ]]; then warn "deploy aborted (exit $exit_code)" fi exit $exit_code } trap cleanup EXIT # ---- build the orphan snapshot --------------------------------------------- log "creating orphan branch '$TMP_BRANCH'" git checkout --orphan "$TMP_BRANCH" # `git checkout --orphan` keeps the working tree but stages everything. We # want to start from a clean stage, then re-add everything filtered by # .gitignore — so a stray binary in working tree doesn't sneak through. git rm -rf --cached . > /dev/null 2>&1 || true # Foundry leaves a nested `.git` in `contracts/` (its own submodule lockfile # tracking). When the outer repo runs `git add -A`, it sees this as a # (broken) submodule and aborts with "does not have a commit checked out". # Temporarily rename it; the cleanup trap restores it on the way out. if [[ -d contracts/.git ]]; then log "temporarily renaming contracts/.git (foundry nested) so git add -A can recurse" mv contracts/.git contracts/.git.deploy-tmp CONTRACTS_GIT_MOVED=1 fi git add -A log "swapping README for HF-flavored version (deploy/hf-readme.md → README.md)" cp deploy/hf-readme.md README.md git add README.md # contracts/out (compiled ABI JSON) is in .gitignore on main, but HF Spaces # needs the ABIs at runtime — force-add the subdir into the orphan snapshot. if [[ -d contracts/out ]]; then log "force-adding contracts/out/ (ABIs needed by backend, ignored on main)" git add -f contracts/out/ fi # Drop dev-facing assets HF Spaces doesn't need and whose binaries trip # HF's pre-receive hook (e.g. submission/diagrams/*.png are GitHub hackathon # docs, never served by the Docker container). We `git rm --cached` them # from the orphan index — the working tree copies stay so main is untouched # when the cleanup trap checks us back out. HF_EXCLUDE_PATHS=( submission # hackathon write-up — GitHub-only docs # design docs — GitHub-only examples # operator example scripts — GitHub-only contracts/lib # foundry submodules — only needed for forge build contracts/cache # foundry compile cache contracts/broadcast # foundry deploy logs ) for path in "${HF_EXCLUDE_PATHS[@]}"; do if git ls-files --cached "$path" 2>/dev/null | head -1 | grep -q .; then git rm -r --cached --quiet "$path" > /dev/null 2>&1 || true fi done # Single commit — HF only ever sees this one. COMMIT_MSG="deploy: main@${MAIN_SHA_SHORT} → HF Spaces ($(date -u +%Y-%m-%dT%H:%MZ))" git commit --quiet -m "$COMMIT_MSG" ok "snapshot committed: $(git rev-parse --short HEAD)" # ---- push to HF ------------------------------------------------------------- log "force-pushing to hf:main (HF Space will auto-rebuild on receipt)" if ! git push hf "$TMP_BRANCH:main" --force 2>&1; then die "push to HF failed — see error above. Common causes: stale auth ('hf auth login'), Space settings (sdk: docker required), or large files." fi ok "pushed → https://huggingface.co/spaces/messili/polyglot-alpha" # ---- restore main, swap README back ----------------------------------------- log "restoring '$ORIGINAL_BRANCH' (your README.md never moved on main)" git checkout --force "$ORIGINAL_BRANCH" # Cleanup trap deletes the temp branch on exit. ok "deploy complete" echo echo " HF Space: https://messili-polyglot-alpha.hf.space/" echo " Build progress: https://huggingface.co/spaces/messili/polyglot-alpha" echo " Cold rebuild ~3-5 min; warm rebuild ~30-60 s."