Skip to content

Commit 284ffdc

Browse files
authored
Merge pull request #4 from burtenshaw/openenv-cli/v1-ci
Openenv cli/v1 ci
2 parents 968176a + 1a76128 commit 284ffdc

35 files changed

+3384
-34
lines changed

.github/workflows/pr-new-env-health-check.yml renamed to .github/workflows/pr-new-env.yml

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ jobs:
103103
environment: ${{ fromJSON(needs.detect-new-envs.outputs.new_envs_json) }}
104104
env:
105105
HF_TOKEN: ${{ secrets.HF_PR_TOKEN }}
106+
HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HF_PR_TOKEN }}
106107
HF_NAMESPACE: ${{ vars.HF_PR_NAMESPACE }}
107108
SPACE_SUFFIX: -pr-${{ github.event.number }}
108109
steps:
@@ -127,18 +128,24 @@ jobs:
127128
exit 1
128129
fi
129130
130-
- name: Install Hugging Face CLI
131+
- name: Set up Python
132+
uses: actions/setup-python@v5
133+
with:
134+
python-version: '3.11'
135+
136+
- name: Install OpenEnv package
131137
shell: bash
132138
run: |
133-
curl -LsSf https://hf.co/cli/install.sh | bash
134-
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
139+
set -euo pipefail
140+
python -m pip install --upgrade pip
141+
pip install .
135142
136-
- name: Deploy environment to Hugging Face
143+
- name: Deploy environment with OpenEnv CLI
137144
shell: bash
138145
run: |
139146
set -euo pipefail
140-
chmod +x scripts/deploy_to_hf.sh
141-
./scripts/deploy_to_hf.sh --env "${{ matrix.environment }}" --space-suffix "${SPACE_SUFFIX}"
147+
repo_id="${HF_NAMESPACE}/${{ matrix.environment }}${SPACE_SUFFIX}"
148+
openenv push "${{ matrix.environment }}" --repo-id "$repo_id"
142149
143150
- name: Wait for deployment to stabilize
144151
shell: bash
@@ -161,13 +168,15 @@ jobs:
161168
health_url="https://${namespace_slug}-${space_slug}.hf.space/health"
162169
live_url="https://${namespace_slug}-${space_slug}.hf.space"
163170
space_repo_url="https://huggingface.co/spaces/${HF_NAMESPACE}/${space_name}"
171+
space_repo_id="${HF_NAMESPACE}/${space_name}"
164172
165173
echo "namespace_slug=${namespace_slug}" >> "$GITHUB_OUTPUT"
166174
echo "space_name=${space_name}" >> "$GITHUB_OUTPUT"
167175
echo "space_slug=${space_slug}" >> "$GITHUB_OUTPUT"
168176
echo "health_url=${health_url}" >> "$GITHUB_OUTPUT"
169177
echo "live_url=${live_url}" >> "$GITHUB_OUTPUT"
170178
echo "space_repo_url=${space_repo_url}" >> "$GITHUB_OUTPUT"
179+
echo "space_repo_id=${space_repo_id}" >> "$GITHUB_OUTPUT"
171180
172181
- name: Perform environment health check
173182
id: health_check
@@ -216,41 +225,100 @@ jobs:
216225
SPACE_NAME: ${{ steps.urls.outputs.space_name }}
217226
LIVE_URL: ${{ steps.urls.outputs.live_url }}
218227
SPACE_REPO_URL: ${{ steps.urls.outputs.space_repo_url }}
228+
SPACE_REPO_ID: ${{ steps.urls.outputs.space_repo_id }}
219229
ENV_NAME: ${{ matrix.environment }}
230+
COMMENT_TAG: "<!-- openenv-pr-preview -->"
220231
with:
221232
github-token: ${{ secrets.GITHUB_TOKEN }}
222233
script: |
223234
const status = process.env.HEALTH_CONCLUSION || 'failure';
224235
const spaceName = process.env.SPACE_NAME;
225236
const liveUrl = process.env.LIVE_URL;
226237
const repoUrl = process.env.SPACE_REPO_URL;
238+
const repoId = process.env.SPACE_REPO_ID;
227239
const envName = process.env.ENV_NAME;
240+
const marker = process.env.COMMENT_TAG;
228241
229242
const header = status === 'success'
230-
? `✅ Deployment succeeded for \`${envName}\``
231-
: `⚠️ Deployment failed for \`${envName}\``;
243+
? `✅ Deployment to Hugging Face succeeded for \`${envName}\``
244+
: `⚠️ Deployment Hugging Face failed for \`${envName}\``;
232245
233246
const summary = status === 'success'
234-
? 'Nice work! Wait for a code review and we\'re ready to go.'
235-
: 'Please resolve your environment.';
247+
? 'Nice work! Wait for a code review and we\'re ready to go. You can test it with the CLI:'
248+
: 'Please resolve your environment and test it with the CLI:';
236249
237250
const body = [
251+
marker,
252+
'',
238253
header,
239254
'',
240-
`- Space repo: [${repoUrl}](${repoUrl})`,
241-
`- Live URL: [${liveUrl}](${liveUrl})`,
242255
'',
243256
summary,
244257
'',
245-
'You can iterate locally or validate fixes by running `scripts/deploy_to_hf.sh --env "' + envName + '"`.'
258+
'- `openenv push ' + envName + ' --repo-id ' + repoId + '`',
246259
].join('\n');
247260
248-
await github.rest.issues.createComment({
249-
owner: context.repo.owner,
250-
repo: context.repo.repo,
251-
issue_number: context.payload.pull_request.number,
252-
body
253-
});
261+
const {owner, repo} = context.repo;
262+
const issue_number = context.payload.pull_request.number;
263+
264+
const existing = await github.paginate(
265+
github.rest.issues.listComments,
266+
{ owner, repo, issue_number, per_page: 100 },
267+
(response, done) => {
268+
const match = response.data.find(comment => comment.body && comment.body.includes(marker));
269+
if (match) {
270+
done();
271+
return [match];
272+
}
273+
return [];
274+
}
275+
);
276+
277+
if (existing.length > 0) {
278+
await github.rest.issues.updateComment({
279+
owner,
280+
repo,
281+
comment_id: existing[0].id,
282+
body,
283+
});
284+
} else {
285+
await github.rest.issues.createComment({
286+
owner,
287+
repo,
288+
issue_number,
289+
body,
290+
});
291+
}
292+
293+
- name: Delete preview space on Hugging Face
294+
if: always()
295+
continue-on-error: true
296+
shell: bash
297+
env:
298+
SPACE_REPO_ID: ${{ steps.urls.outputs.space_repo_id }}
299+
run: |
300+
set -euo pipefail
301+
if [ -z "${SPACE_REPO_ID:-}" ]; then
302+
echo "No space repo id; skipping deletion"
303+
exit 0
304+
fi
305+
306+
TOKEN="${HF_TOKEN:-${HUGGINGFACEHUB_API_TOKEN:-}}"
307+
if [ -z "$TOKEN" ]; then
308+
echo "HF token not available; cannot delete space"
309+
exit 0
310+
fi
311+
312+
set +e
313+
hf repo delete "$SPACE_REPO_ID" --repo-type space --yes --token "$TOKEN"
314+
status=$?
315+
set -e
316+
317+
if [ $status -eq 0 ]; then
318+
echo "Deleted preview space $SPACE_REPO_ID"
319+
else
320+
echo "Failed to delete space $SPACE_REPO_ID (exit $status)"
321+
fi
254322
255323
- name: Fail job if health check failed
256324
if: steps.health_check.conclusion == 'failure'

.github/workflows/publish-pypi-core.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ jobs:
3636
- name: Build package
3737
run: |
3838
cd src/core
39+
# Verify openenv_cli is accessible
40+
ls -la ../openenv_cli || echo "Warning: openenv_cli directory not found"
3941
python -m build
4042
4143
- name: Check package

README.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,118 @@ To use an environment:
143143

144144
See example scripts in `examples/` directory.
145145

146+
### Deploying Environments to Hugging Face Spaces
147+
148+
The OpenEnv CLI provides a self-service workflow for publishing environments to Hugging Face Spaces. This enables community members to share environments without requiring adding them as examples to this repo.
149+
150+
#### Installation
151+
152+
The CLI is installed as part of the OpenEnv package:
153+
154+
```bash
155+
pip install -e .
156+
```
157+
158+
#### Push Environment
159+
160+
Push an environment to Hugging Face Spaces:
161+
162+
```bash
163+
openenv push <env_name> [options]
164+
```
165+
166+
**Arguments:**
167+
- `env_name`: Name of the environment to push (e.g., `echo_env`, `coding_env`)
168+
169+
**Options:**
170+
- `--repo-id <repo_id>`: Hugging Face repository ID in format `namespace/space-name`. If not provided, uses `{username}/{env_name}`.
171+
- `--private`: Create a private space (default: public)
172+
- `--base-image <image>`: Base Docker image to use (default: `ghcr.io/meta-pytorch/openenv-base:latest`)
173+
- `--dry-run`: Prepare files but don't upload to Hugging Face
174+
175+
**Examples:**
176+
177+
```bash
178+
# Push echo_env to your personal namespace
179+
openenv push echo_env
180+
181+
# Push to a specific organization
182+
openenv push coding_env --repo-id my-org/coding_env
183+
184+
# Push with a custom space name
185+
openenv push echo_env --repo-id my-org/my-custom-space
186+
187+
# Create a private space
188+
openenv push echo_env --private
189+
190+
# Use a custom base image
191+
openenv push echo_env --base-image ghcr.io/my-org/custom-base:latest
192+
193+
# Prepare files without uploading
194+
openenv push echo_env --dry-run
195+
```
196+
197+
#### How It Works
198+
199+
The `openenv push` command performs the following steps:
200+
201+
1. **Validation**: Checks that the environment exists in `src/envs/<env_name>/`
202+
2. **Authentication**: Ensures you're authenticated with Hugging Face via interactive login (prompts if needed)
203+
3. **Space Provisioning**: Determines the target Space repository ID (uses `--repo-id` if provided, otherwise `{username}/{env_name}`). Creates the Docker Space if needed (using `exist_ok=True` to handle existing spaces automatically)
204+
4. **Build Process**:
205+
- Creates a staging directory
206+
- Copies core and environment files
207+
- Generates/modifies Dockerfile with web interface enabled
208+
- Prepares README: If the environment's README already has Hugging Face front matter (starts and ends with `---`), uses it as-is. Otherwise, generates front matter with random emoji and colors from approved options
209+
5. **Deployment**: Uploads all files to the Hugging Face Space
210+
6. **Cleanup**: Removes staging directory after successful upload
211+
212+
All pushed environments automatically include the web interface, available at `/web` on deployed spaces.
213+
214+
For more details on the CLI architecture and development, see [`src/openenv_cli/README.md`](src/openenv_cli/README.md).
215+
216+
## CLI Troubleshooting
217+
218+
### Authentication Issues
219+
220+
**Problem**: "Failed to retrieve token after login" or authentication errors
221+
222+
**Solution**:
223+
- Check that `huggingface_hub` is properly installed: `pip install --upgrade huggingface_hub`
224+
- Try logging in via the Hugging Face CLI: `huggingface-cli login`
225+
- Clear cached credentials if needed (credentials are stored by `huggingface_hub`)
226+
- Ensure you have "write" permissions on the namespace where you're pushing
227+
228+
### Space Creation Fails
229+
230+
**Problem**: "Failed to create space" or "Permission denied"
231+
232+
**Solution**:
233+
- Check that namespace/username is correct
234+
- Verify you have permission to create spaces in that namespace
235+
- If the space already exists, `exist_ok=True` handles it automatically (you may see a warning from the Hub CLI)
236+
- For authentication errors, see "Authentication Issues" above
237+
238+
### Upload Fails
239+
240+
**Problem**: "Failed to upload to space"
241+
242+
**Solution**:
243+
- Check internet connection
244+
- Verify you're still authenticated (may need to log in again)
245+
- Try `--dry-run` first to check file preparation
246+
- Check staging directory exists and has files
247+
- Verify you have write permissions on the target space
248+
249+
### Environment Not Found
250+
251+
**Problem**: "Environment 'xyz' not found"
252+
253+
**Solution**:
254+
- Verify environment exists in `src/envs/<env_name>/`
255+
- Check spelling of environment name
256+
- Ensure environment directory has required structure (models.py, server/, etc.)
257+
146258
## Design Principles
147259

148260
1. **Separation of Concerns**: Clear client-server boundaries

pyproject.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,23 @@ dependencies = [
1212
"requests>=2.25.0",
1313
"fastapi>=0.104.0",
1414
"uvicorn>=0.24.0",
15-
"smolagents>=1.22.0,<2"
15+
"smolagents>=1.22.0,<2",
16+
"huggingface_hub>=0.20.0",
17+
"rich>=13.0.0",
1618
]
1719

1820
[tool.setuptools]
1921
package-dir = {"" = "src"}
2022

2123
[tool.setuptools.packages.find]
2224
where = ["src"]
25+
26+
[project.scripts]
27+
openenv = "openenv_cli.__main__:main"
28+
29+
[project.optional-dependencies]
30+
dev = [
31+
"pytest>=7.0.0",
32+
"pytest-mock>=3.10.0",
33+
"pytest-cov>=4.0.0"
34+
]

scripts/deploy_to_hf.sh

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ while [[ $# -gt 0 ]]; do
7575
ENV_NAME="$2"
7676
shift 2
7777
;;
78+
--hub-tag)
79+
HUB_TAG="$2"
80+
shift 2
81+
;;
7882
--base-sha|--base-image-sha)
7983
BASE_IMAGE_SHA="$2"
8084
shift 2
@@ -127,6 +131,10 @@ while [[ $# -gt 0 ]]; do
127131
esac
128132
done
129133

134+
if [ -z "$HUB_TAG" ]; then
135+
HUB_TAG="openenv"
136+
fi
137+
130138
if [ -z "$ENV_NAME" ]; then
131139
echo "Error: Environment name is required" >&2
132140
usage
@@ -364,6 +372,8 @@ sdk: docker
364372
pinned: false
365373
app_port: 8000
366374
base_path: /web
375+
tags:
376+
- ${HUB_TAG}
367377
---
368378
369379
# ${env_title} Environment Server

src/core/pyproject.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,14 @@ dependencies = [
1818
"requests>=2.25.0",
1919
"fastapi>=0.104.0",
2020
"uvicorn>=0.24.0",
21+
"huggingface_hub>=0.20.0",
22+
"rich>=13.0.0",
23+
"typer>=0.12.0",
2124
]
2225

26+
[project.scripts]
27+
openenv = "openenv_cli.__main__:main"
28+
2329
[project.optional-dependencies]
2430
dev = [
2531
"pytest>=7.0.0",
@@ -41,6 +47,10 @@ packages = [
4147
"openenv_core.containers",
4248
"openenv_core.containers.runtime",
4349
"openenv_core.env_server",
44-
"openenv_core.tools"
50+
"openenv_core.tools",
51+
"openenv_cli",
52+
"openenv_cli.commands",
53+
"openenv_cli.core",
54+
"openenv_cli.utils",
4555
]
46-
package-dir = {"openenv_core" = "."}
56+
package-dir = {"openenv_core" = ".", "openenv_cli" = "../openenv_cli"}

src/envs/atari_env/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
22
title: Atari Environment Server
33
emoji: 🕹️
4-
colorFrom: '#FF6200'
5-
colorTo: '#D4151B'
4+
colorFrom: yellow
5+
colorTo: red
66
sdk: docker
77
pinned: false
88
app_port: 8000

0 commit comments

Comments
 (0)