From 779cc6cb49c96130df67272530d8e0c99c2ce0b6 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 7 May 2026 03:57:34 +0000 Subject: [PATCH] CI(studio): always-upload artifacts + gate /api/system + path/health plumbing Three small but high-signal changes that came out of an audit of how much Studio surface CI actually exercises: 1. Every studio-*-smoke.yml workflow now uploads its artifacts on `if: always()` instead of `if: failure()`. On green runs the screenshots + studio.log are now reviewable in the Actions UI, which closes the "passed but the UI is silently broken" hole. SHA-pinned to actions/upload-artifact@v4.6.2 across all 7 upload steps (was a mix of @v4 unpinned + the SHA-pin). 2. /api/system and /api/system/hardware now require a Bearer token (Depends(get_current_subject)). Today they leak Python version, GPU name, total memory, and the ML package set without auth -- fine on a single-user Tauri box, not fine on -H 0.0.0.0 / Colab / a Tauri-relayed setup. /api/system/gpu-visibility was already gated; now /api/system + /api/system/hardware match it. 3. Path filters + health-wait plumbing: - studio-ui-smoke.yml now triggers on tests/studio/** so a PR that ONLY edits the Playwright test file actually runs UI CI. - studio-tauri-smoke.yml now triggers on unsloth_cli/** so a CLI rename or signature change that breaks Tauri's spawned `unsloth studio` actually runs Tauri CI. - The 60s `/api/health` wait loop in studio-ui-smoke.yml + studio-inference-smoke.yml (3 jobs) is now 180s. Cold runners with venv warm-up + lazy imports have been observed exceeding 60s, and the cost of a false-fail is much higher than two extra minutes of waiting. --- .github/workflows/studio-frontend-ci.yml | 10 +++++-- .github/workflows/studio-inference-smoke.yml | 29 +++++++++++--------- .github/workflows/studio-tauri-smoke.yml | 9 ++++-- .github/workflows/studio-ui-smoke.yml | 16 +++++++++-- .github/workflows/studio-update-smoke.yml | 8 ++++-- studio/backend/main.py | 23 +++++++++++++--- 6 files changed, 67 insertions(+), 28 deletions(-) diff --git a/.github/workflows/studio-frontend-ci.yml b/.github/workflows/studio-frontend-ci.yml index d4d0bddecb..14176bc071 100644 --- a/.github/workflows/studio-frontend-ci.yml +++ b/.github/workflows/studio-frontend-ci.yml @@ -102,9 +102,13 @@ jobs: continue-on-error: true run: npm run biome:check - - name: Upload built dist on failure - if: failure() - uses: actions/upload-artifact@v4 + - name: Upload built dist + # Always upload so a green run is reviewable too -- the dist + # output catches "tests passed but bundle changed unexpectedly" + # regressions that would be invisible if we only kept artifacts + # on failure. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: studio-frontend-dist path: studio/frontend/dist diff --git a/.github/workflows/studio-inference-smoke.yml b/.github/workflows/studio-inference-smoke.yml index 393a7b24b5..02f55ad1b0 100644 --- a/.github/workflows/studio-inference-smoke.yml +++ b/.github/workflows/studio-inference-smoke.yml @@ -121,14 +121,14 @@ jobs: - name: Wait for /api/health run: | - for i in $(seq 1 60); do + for i in $(seq 1 180); do if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then jq -e '.status == "healthy"' /tmp/health.json exit 0 fi sleep 1 done - echo "Studio did not become healthy in 60s" + echo "Studio did not become healthy in 180s" tail -200 logs/studio.log exit 1 @@ -270,9 +270,10 @@ jobs: sleep 2 ss -tln | grep ":${STUDIO_PORT}" || true - - name: Upload logs on failure - if: failure() - uses: actions/upload-artifact@v4 + - name: Upload logs + # Always upload so green runs are still reviewable. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: openai-anthropic-log path: | @@ -354,7 +355,7 @@ jobs: - name: Wait for /api/health, log in, change password, load model run: | - for i in $(seq 1 60); do + for i in $(seq 1 180); do if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then jq -e '.status == "healthy"' /tmp/health.json && break fi @@ -572,9 +573,10 @@ jobs: sleep 2 ss -tln | grep ":${STUDIO_PORT}" || true - - name: Upload logs on failure - if: failure() - uses: actions/upload-artifact@v4 + - name: Upload logs + # Always upload so green runs are still reviewable. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: tool-calling-log path: | @@ -657,7 +659,7 @@ jobs: - name: Wait for /api/health, log in, change password, load model run: | - for i in $(seq 1 60); do + for i in $(seq 1 180); do if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then jq -e '.status == "healthy"' /tmp/health.json && break fi @@ -834,9 +836,10 @@ jobs: sleep 2 ss -tln | grep ":${STUDIO_PORT}" || true - - name: Upload logs on failure - if: failure() - uses: actions/upload-artifact@v4 + - name: Upload logs + # Always upload so green runs are still reviewable. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: json-images-log path: | diff --git a/.github/workflows/studio-tauri-smoke.yml b/.github/workflows/studio-tauri-smoke.yml index 64f1d15ad2..0e12c5b371 100644 --- a/.github/workflows/studio-tauri-smoke.yml +++ b/.github/workflows/studio-tauri-smoke.yml @@ -19,6 +19,9 @@ on: paths: - 'studio/frontend/**' - 'studio/src-tauri/**' + # CLI rename / signature change can break Tauri's spawned + # `unsloth studio` -- include unsloth_cli in the trigger set. + - 'unsloth_cli/**' - '.github/workflows/studio-tauri-smoke.yml' push: branches: [main, pip] @@ -98,8 +101,10 @@ jobs: file "$BIN" du -h "$BIN" - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - if: failure() + - name: Upload Tauri debug build + # Always upload so a green run leaves the binary inspectable too. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: tauri-debug-build path: | diff --git a/.github/workflows/studio-ui-smoke.yml b/.github/workflows/studio-ui-smoke.yml index f3bfcdb6f0..1b7053b46a 100644 --- a/.github/workflows/studio-ui-smoke.yml +++ b/.github/workflows/studio-ui-smoke.yml @@ -24,6 +24,9 @@ on: - 'unsloth_cli/**' - 'install.sh' - 'pyproject.toml' + # The Playwright test files themselves -- a PR that ONLY edits + # the test must still trigger UI CI. + - 'tests/studio/**' - '.github/workflows/studio-ui-smoke.yml' push: branches: [main, pip] @@ -107,8 +110,11 @@ jobs: echo "STUDIO_PID=$!" >> "$GITHUB_ENV" - name: Wait for /api/health + # 180 s -- a cold runner with venv warm-up + lazy imports has + # been seen to exceed 60 s. Failing the wait is more expensive + # than waiting an extra two minutes. run: | - for i in $(seq 1 90); do + for i in $(seq 1 180); do if curl -fs "http://127.0.0.1:${STUDIO_PORT}/api/health" > /tmp/health.json; then jq -e '.status == "healthy"' /tmp/health.json && break fi @@ -158,8 +164,12 @@ jobs: kill "${STUDIO_PID}" 2>/dev/null || true sleep 2 - - name: Upload Playwright artifacts on failure - if: failure() + - name: Upload Playwright artifacts + # Always upload (not just failure) so a green run's screenshots + # are reviewable in the Actions UI -- catches "passed but the + # UI is silently broken" regressions that would be invisible + # otherwise. + if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: studio-ui-smoke-artifacts diff --git a/.github/workflows/studio-update-smoke.yml b/.github/workflows/studio-update-smoke.yml index ddc25e21b7..f6c3dd5c6b 100644 --- a/.github/workflows/studio-update-smoke.yml +++ b/.github/workflows/studio-update-smoke.yml @@ -135,9 +135,11 @@ jobs: kill "$PID" 2>/dev/null || true echo "post-update Studio /api/health OK" - - name: Upload update logs on failure - if: failure() - uses: actions/upload-artifact@v4 + - name: Upload update logs + # Always upload so a green run still leaves the install + two + # update logs reviewable. + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: studio-update-log path: | diff --git a/studio/backend/main.py b/studio/backend/main.py index cd901327db..633b112dc8 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -337,8 +337,17 @@ async def shutdown_server( @app.get("/api/system") -async def get_system_info(): - """Get system information""" +async def get_system_info( + current_subject: str = Depends(get_current_subject), +): + """Get system information. + + Gated behind auth: the response includes platform, Python version, + GPU name, memory total, and ML package set -- enough to fingerprint + a host. Studio's chat-only-mode design assumes only the local user + reaches /api/system; in -H 0.0.0.0 / Colab / Tauri-relayed setups + that assumption breaks unless we require a bearer. + """ import platform import psutil from utils.hardware import get_device @@ -378,8 +387,14 @@ async def get_gpu_visibility( @app.get("/api/system/hardware") -async def get_hardware_info(): - """Return GPU name, total VRAM, and key ML package versions.""" +async def get_hardware_info( + current_subject: str = Depends(get_current_subject), +): + """Return GPU name, total VRAM, and key ML package versions. + + Gated behind auth alongside /api/system -- same fingerprinting + concern. /api/system/gpu-visibility is also auth-gated already. + """ from utils.hardware import get_gpu_summary, get_package_versions return {