polats Claude Opus 5 (1M context) commited on
Commit
e85a4d4
Β·
1 Parent(s): 5bd2d41

Sync the HF Space from CI on every push to main

Browse files

Builds the image on a runner, boots it, and asserts the three things that
matter β€” it exits nonzero without OPENCODE_SERVER_PASSWORD, answers
/api/health with basic auth, and serves the web UI on / β€” before pushing
anything. A Dockerfile that only breaks on the Space would otherwise leave it
parked in BUILD_ERROR with no signal here.

The push is forced because GitHub is the source of truth; a hand-edit in the
Space's web UI should not be able to block a deploy. The HF username is derived
from the token via whoami rather than stored, which also turns a bad token into
a clear error instead of an opaque git failure. Finally the job polls the Space
until it reports RUNNING so a red check means a broken deploy, not just a
failed push.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. .github/workflows/sync-to-hf-space.yml +130 -0
  2. README.md +20 -0
.github/workflows/sync-to-hf-space.yml ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face Space
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ # Never let two syncs race each other onto the same Space.
9
+ concurrency:
10
+ group: hf-space-sync
11
+ cancel-in-progress: false
12
+
13
+ jobs:
14
+ # Build and boot the image here first. A Dockerfile that fails on the Space
15
+ # leaves it sitting in BUILD_ERROR, so it is much cheaper to find out on a
16
+ # runner than in production.
17
+ verify:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Build image
23
+ run: docker build -t opencode-cloud:ci .
24
+
25
+ - name: Refuses to start without a password
26
+ run: |
27
+ if docker run --rm opencode-cloud:ci; then
28
+ echo "::error::Container started without OPENCODE_SERVER_PASSWORD set"
29
+ exit 1
30
+ fi
31
+ echo "OK: container refused to start unauthenticated"
32
+
33
+ - name: Serves an authenticated API and the web UI
34
+ run: |
35
+ PW="ci-$RANDOM-$RANDOM"
36
+ docker run -d --name oc-ci -p 7860:7860 -e OPENCODE_SERVER_PASSWORD="$PW" opencode-cloud:ci
37
+
38
+ for i in $(seq 1 40); do
39
+ code=$(curl -s -m 3 -o /dev/null -w "%{http_code}" http://127.0.0.1:7860/api/health || true)
40
+ [ "$code" = "401" ] && break
41
+ sleep 3
42
+ done
43
+
44
+ echo "--- container log ---"
45
+ docker logs oc-ci
46
+
47
+ if [ "$code" != "401" ]; then
48
+ echo "::error::Server never came up (last status: ${code:-none})"
49
+ exit 1
50
+ fi
51
+ echo "OK: unauthenticated request rejected with 401"
52
+
53
+ body=$(curl -s -m 10 -u "opencode:$PW" http://127.0.0.1:7860/api/health)
54
+ if [ "$body" != '{"healthy":true}' ]; then
55
+ echo "::error::Authenticated health check returned: $body"
56
+ exit 1
57
+ fi
58
+ echo "OK: authenticated health check returned $body"
59
+
60
+ if ! curl -s -m 30 -u "opencode:$PW" http://127.0.0.1:7860/ | grep -q "<title>"; then
61
+ echo "::error::Web UI was not served on /"
62
+ exit 1
63
+ fi
64
+ echo "OK: web UI served on /"
65
+
66
+ - name: Stop container
67
+ if: always()
68
+ run: docker rm -f oc-ci || true
69
+
70
+ sync:
71
+ needs: verify
72
+ runs-on: ubuntu-latest
73
+ env:
74
+ # Defaults to this GitHub repo's owner/name, which is also the Space id.
75
+ # Override with a repo variable (or secret) named HF_SPACE.
76
+ HF_SPACE: ${{ vars.HF_SPACE || secrets.HF_SPACE || github.repository }}
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+ with:
80
+ # Full history: the force-push below cannot be made from a shallow clone.
81
+ fetch-depth: 0
82
+ lfs: true
83
+
84
+ - name: Push to the Space
85
+ env:
86
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
87
+ run: |
88
+ if [ -z "$HF_TOKEN" ]; then
89
+ echo "::error::HF_TOKEN secret is not set (Settings -> Secrets and variables -> Actions)"
90
+ exit 1
91
+ fi
92
+
93
+ # Any HF username works alongside a token; derive it so there is one
94
+ # less secret to keep in sync, and so a bad token fails here with a
95
+ # clear message instead of a confusing git error.
96
+ user=$(curl -s -f -H "Authorization: Bearer $HF_TOKEN" \
97
+ https://huggingface.co/api/whoami-v2 | jq -r '.name // empty')
98
+ if [ -z "$user" ]; then
99
+ echo "::error::HF_TOKEN was rejected by huggingface.co (expired, or not a write token?)"
100
+ exit 1
101
+ fi
102
+ echo "Authenticated to Hugging Face as $user; syncing to spaces/$HF_SPACE"
103
+
104
+ # GitHub is the source of truth: force-push so a hand-edit made in the
105
+ # Space's web UI cannot block the sync. Anything committed only on the
106
+ # Space side is discarded.
107
+ git push --force "https://${user}:${HF_TOKEN}@huggingface.co/spaces/${HF_SPACE}" \
108
+ "HEAD:refs/heads/main" 2>&1 | sed "s/${HF_TOKEN}/***/g"
109
+
110
+ - name: Wait for the Space to come back up
111
+ env:
112
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
113
+ run: |
114
+ for i in $(seq 1 60); do
115
+ stage=$(curl -s -m 20 -H "Authorization: Bearer $HF_TOKEN" \
116
+ "https://huggingface.co/api/spaces/${HF_SPACE}" | jq -r '.runtime.stage // "UNKNOWN"')
117
+ echo "[$i] $stage"
118
+ case "$stage" in
119
+ RUNNING)
120
+ echo "Space is running."
121
+ exit 0
122
+ ;;
123
+ BUILD_ERROR|RUNTIME_ERROR|CONFIG_ERROR)
124
+ echo "::error::Space ended in $stage β€” check the build logs at https://huggingface.co/spaces/${HF_SPACE}"
125
+ exit 1
126
+ ;;
127
+ esac
128
+ sleep 15
129
+ done
130
+ echo "::warning::Space did not reach RUNNING within 15 minutes; last stage: $stage"
README.md CHANGED
@@ -74,6 +74,26 @@ with mount path `/data` for persistence. Details and caveats: [docs/RAILWAY.md](
74
 
75
  ---
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  ## Connecting the desktop app or a local web app
78
 
79
  You don't have to use the in-browser UI. In the app, **Settings β†’ Servers β†’ Add**
 
74
 
75
  ---
76
 
77
+ ## Keeping the Space in sync with GitHub
78
+
79
+ `.github/workflows/sync-to-hf-space.yml` builds the image, boots it, checks that
80
+ it refuses to start unauthenticated and that it serves an authenticated API and
81
+ the web UI β€” and only then force-pushes `main` to the Space and waits for it to
82
+ report `RUNNING`. A broken Dockerfile fails on the runner instead of leaving the
83
+ Space stuck in `BUILD_ERROR`.
84
+
85
+ Configure it under **Settings β†’ Secrets and variables β†’ Actions**:
86
+
87
+ | Name | Kind | Notes |
88
+ |---|---|---|
89
+ | `HF_TOKEN` | **secret** | A Hugging Face **write** token. The HF username is derived from it, so it is the only secret needed. |
90
+ | `HF_SPACE` | variable | `owner/space-name`. Optional β€” defaults to this GitHub repo's `owner/name`. A public Space id isn't sensitive, so a variable keeps it readable in logs; a secret of the same name also works. |
91
+
92
+ GitHub is the source of truth: the sync **force-pushes**, so a commit made only
93
+ in the Space's web UI will be discarded. Edit here, not there.
94
+
95
+ ---
96
+
97
  ## Connecting the desktop app or a local web app
98
 
99
  You don't have to use the in-browser UI. In the app, **Settings β†’ Servers β†’ Add**