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.
This commit is contained in:
parent
246f54d05a
commit
779cc6cb49
6 changed files with 67 additions and 28 deletions
10
.github/workflows/studio-frontend-ci.yml
vendored
10
.github/workflows/studio-frontend-ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
29
.github/workflows/studio-inference-smoke.yml
vendored
29
.github/workflows/studio-inference-smoke.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
9
.github/workflows/studio-tauri-smoke.yml
vendored
9
.github/workflows/studio-tauri-smoke.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
16
.github/workflows/studio-ui-smoke.yml
vendored
16
.github/workflows/studio-ui-smoke.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/workflows/studio-update-smoke.yml
vendored
8
.github/workflows/studio-update-smoke.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue