polyglot-alpha / scripts /deploy_to_hf.sh
licaomeng
deploy: main@8970ffb β†’ HF Spaces (2026-05-27T05:19Z)
88d2f2a
#!/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."