github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
8.94 kB
# Deploying the Co-Study4Grid game to a HuggingFace Docker Space
This scaffolds a **single-container** deployment: one uvicorn process serves the
FastAPI backend *and* the built React frontend (same origin) on port **7860**,
with the sample grids bundled so Game Mode works out of the box.
It is sized for **one player per running Space** (the backend holds a single
active study — see the caveat at the bottom). For more players, each person
clicks **Duplicate this Space** to get an isolated instance.
## What was wired up
| File | Role |
|---|---|
| `Dockerfile` (repo root) | Multi-stage build: Vite SPA → Python runtime serving API + SPA + grids on `:7860`. |
| `.dockerignore` (repo root) | Trims the build context (no `node_modules`, `.git`, `Overflow_Graph`, …). |
| `deploy/huggingface/README.md` | The **Space README** (YAML frontmatter `sdk: docker`, `app_port: 7860`) + landing page. |
| `frontend/src/api.ts` | API base URL is now `VITE_API_BASE_URL ?? http://127.0.0.1:8000` → empty in the image = same-origin. |
| `frontend/src/game/gameBridge.ts` | `isGameMode()` also honors `VITE_GAME_MODE=1`, so the Space boots straight into the game. |
| `expert_backend/main.py` | Serves the built SPA at `/` when `COSTUDY4GRID_FRONTEND_DIST` exists; honors `$PORT`. |
| `.gitattributes` (repo root) | Tracks `*.zip` / `*.png` / `*.jpg` via Git LFS so binaries can be pushed to the HF Space (its git endpoint rejects non-LFS binaries). |
| `config.default.json` (repo root) | Seeded into the image; the backend copies it to `config.json` on first boot (recommender defaults). |
The Dockerfile sets `VITE_API_BASE_URL=""` and `VITE_GAME_MODE="1"` for the
frontend build. Local dev (`npm run dev`) and the Vitest suite are unaffected
(both variables are unset there, so the old `:8000` + `?game=1` behavior holds).
## Large binaries: Git LFS / Xet
HuggingFace's git endpoint rejects files **> 10 MiB** *and* non-LFS **binary**
files. The European grid (`pypsa_eur_eur220_225_380_400/network.xiidm`, ~22 MB)
therefore travels as a Git-LFS **`.zip`** (~2 MB), and the doc images travel as
LFS too. `.gitattributes` (repo root) declares this. The Dockerfile decompresses
the `.zip` back to `network.xiidm` at build time.
One-time, on your machine:
```bash
git lfs install
```
If your existing binaries were committed before `.gitattributes` existed,
migrate them once so they become LFS objects:
```bash
git add --renormalize . && git commit -m "migrate binaries to LFS"
```
## Deploy steps
1. **Create the Space** — on huggingface.co: *New → Space → Docker → Blank*.
2. **Push a single orphan snapshot to the Space.** The branch *history* still
contains the > 10 MiB raw `.xiidm` from older commits, which HF would reject,
so push **one squashed commit** of the current tree (binaries ride along via
LFS — no need to delete them anymore):
```bash
git lfs install # once
git remote add space https://huggingface.co/spaces/<user>/<space> # once
git checkout --orphan hf-deploy
cp deploy/huggingface/README.md README.md # HF needs the frontmatter at root
git add -A
git commit -m "Deploy Co-Study4Grid game"
git log --oneline hf-deploy # MUST be a single commit
git -c protocol.version=0 push -f space hf-deploy:main
git checkout -f claude/cool-bell-783agy
git branch -D hf-deploy
```
(`protocol.version=0` works around a `fatal: expected 'acknowledgments'`
negotiation error some networks hit against HF.) HuggingFace reads the root
`Dockerfile` + the README frontmatter and builds; the first build is long
(heavy scientific wheels), later builds reuse layers.
3. **Play** — open the Space. It boots straight into the game shell (the
`VITE_GAME_MODE=1` build flag); the default session is the three European
reference studies (Medium difficulty). Build without that flag for the bare
workspace.
## Automated redeploy on merge to `main` (GitHub Action)
`.github/workflows/deploy-huggingface.yml` runs the orphan-snapshot push
automatically. It is **test-gated (D7)**: it fires on `workflow_run` when the
**Tests** workflow completes *successfully* on `main` (not merely on a merge),
checks out that exact tested commit with LFS, squashes it to one history-free
commit, and force-pushes it to the Space's `main`. A manual `workflow_dispatch`
always runs (the rollback path — see below).
Opt in by setting, in the GitHub repo **Settings → Secrets and variables →
Actions**:
| Kind | Name | Value |
|---|---|---|
| Secret | `HF_TOKEN` | a HuggingFace **write** access token with access to the Space |
| Variable | `HF_SPACE` | the Space path, e.g. `your-user/co-study4grid-game` |
| Variable | `HF_USERNAME` | *(optional)* the token owner's HF username — only when the Space is under an **org** and so differs from the owner part of `HF_SPACE` |
The job is inert (it logs a notice and exits cleanly) until both `HF_TOKEN` and
`HF_SPACE` are set, so merging the workflow doesn't break anything before you
opt in. The push reuses LFS objects already on the Space, so repeat deploys only
upload what changed.
### Rolling back a bad deploy
The Space push is force-pushed and history-free, so the Space's own git log is
not a rollback trail. Instead, every successful deploy tags the exact commit it
shipped on **origin** as `space-deploy-<UTC-timestamp>-<shortsha>`. To roll back:
1. Find the last good tag: `git tag --list 'space-deploy-*' | sort | tail`.
2. Re-deploy that commit: **Actions → Deploy to HuggingFace Space → Run
workflow**, and pick the good tag (or its commit) as the ref. The manual
`workflow_dispatch` path is not test-gated, so it redeploys immediately.
### Reproducible Python closure (tracked follow-up)
The image still resolves the Python dependency tree at build time (floors in
`pyproject.toml` + the recommender floor), so a rebuild can pick up newer
transitive releases. To make the build reproducible, generate a lockfile **on
Python 3.10** (matching the image base) and have both the `Dockerfile` and the
**Tests** workflow install from it:
```bash
# on Python 3.10:
pip install pip-tools
pip-compile --output-file requirements.lock pyproject.toml
```
Commit `requirements.lock` and `pip install -r requirements.lock` in both places
so "the Docker image mirrors CI" becomes literally true. (Not generated in-repo
yet: a lockfile must be resolved on the deployment's 3.10 interpreter, not a
dev 3.11, or it pins the wrong wheels.)
## Lockdown profile (hosted deployments)
The Docker image sets `COSTUDY4GRID_LOCKDOWN=1` (see the `Dockerfile`). On the
public Space the desktop-era filesystem RPCs — custom config-file path, session
save / list / load, and the native file picker — are **disabled** with a
`403 {code: "LOCKED_DOWN"}`, because they assume "the client is the operator on
their own machine" and would otherwise give an anonymous visitor read/write
access to the container filesystem. The read-only app config (`GET
/api/user-config`, `GET /api/config-file-path`) stays available so the SPA
boots. Local/dev installs leave the variable unset and behave exactly as before.
See [`docs/architecture/deployment-trust.md`](../../docs/architecture/deployment-trust.md).
## Test the image locally first (recommended)
```bash
docker build -t costudy4grid-game .
docker run --rm -p 7860:7860 costudy4grid-game
# → open http://localhost:7860
```
The build has been validated end to end: the frontend bundle, the full
scientific stack (`pypowsybl`, `ExpertOp4Grid`, `grid2op`, `pandapower`,
`lightsim2grid`), backend import (42 routes), uvicorn on `:7860`, same-origin
SPA + `/api/models`, and the bundled fr225_400 grid loading via pypowsybl all
work. Image is ~370 MB.
Notes if you reproduce the build:
- The recommender is installed with `--no-deps` on purpose — its own dependency
tree is self-conflicting (`numpy>=2` vs its transitive `pypowsybl2grid`'s
`numpy==1.26.4`). The working runtime deps come from `pip install .` +
`overrides.txt`. This mirrors CI.
- If `docker pull` of the base images hits Docker Hub's anonymous rate limit,
either `docker login` or point the daemon at a pull-through mirror
(`/etc/docker/daemon.json`: `{"registry-mirrors": ["https://mirror.gcr.io"]}`)
and restart it.
- If a dependency lacks a prebuilt wheel on the build host, add `build-essential`
to the `apt-get install` line in the runtime stage and rebuild.
## Caveat: one active study per instance
`network_service` and `recommender_service` are module-level singletons holding
one study's state, so a single Space is effectively **single-tenant**. Two
players hitting the same Space concurrently will clobber each other's network
load. For a public competition, prefer **Duplicate this Space** per player, or
refactor the backend to be session-scoped before a shared multi-player launch.