Windows Studio install (install.ps1 --local --no-torch) is the
second-biggest cost on PR 5312 after the Recents-step fix:
Windows Studio UI CI: 414s install (of 24m47s wallclock)
Windows Studio Update: 414s install (of 9m28s)
Windows Studio API: 379s install (of 7m48s)
Windows Studio GGUF (x3): 353s..429s install
Of that 6-7 min, ~3.5 min is uv pip install of the studio venv,
~45s is npm ci + vite build of studio/frontend/dist, ~30s is the
llama.cpp prebuilt fetch+extract; ~90s is winget bringing system
tools in (Python, uv, Node, git, cmake, VS, bun) which sits at
the runner-image layer and isn't cacheable from a workflow.
Add three actions/cache@v4 entries before the install step in
each Windows workflow:
- ~/.unsloth/studio/unsloth_studio (the studio venv)
keyed on hashFiles(pyproject.toml, studio/backend/requirements/**,
install.ps1, studio/setup.ps1, studio/install_python_stack.py)
- ~/.unsloth/llama.cpp (the prebuilt llama.cpp tree)
keyed on hashFiles(studio/install_llama_prebuilt.py)
- studio/frontend/dist (the vite build output)
keyed on hashFiles(studio/frontend/package-lock.json,
studio/frontend/src/**, studio/frontend/index.html,
studio/frontend/vite.config.*, studio/frontend/tsconfig*.json,
studio/frontend/components.json)
Security:
* Cache keys are content-addressable hashes of every input file
that meaningfully changes the produced artefact. A malicious
PR that modifies any of those triggers a fresh build; the
cache cannot mask a real dependency change.
* GitHub Actions cache is branch-partitioned -- a PR cache
cannot poison main's cache. Only a successful build on main
can populate the main-branch cache.
* No restore-keys: prefix-matched fallback would resurrect a
venv whose lockfile no longer matches; uv pip install would
then silently keep the old packages. We want all-or-nothing
on lockfile hash.
* The cache version salt (-v1-) lets us invalidate every entry
immediately if a future advisory or build-system change
requires it.
setup.ps1 already takes the "reusing existing virtual environment"
fast-path when ~/.unsloth/studio/unsloth_studio exists, and the
"prebuilt up to date and validated" fast-path when llama.cpp is
already laid down -- no setup.ps1 changes needed.
Estimated saving: ~5 min per Windows job, ~30 min compute per PR
when caches hit. First run on each lockfile change still pays the
full install cost (the cache-miss path is unchanged).
229 lines
8.9 KiB
YAML
229 lines
8.9 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Windows counterpart to studio-api-smoke.yml / studio-mac-api-smoke.yml.
|
|
# Same tests/studio/studio_api_smoke.py exercise (CORS hardening, auth
|
|
# state machine, JWT expiry, API key lifecycle, /v1/models /
|
|
# /v1/embeddings / /v1/responses, endpoint-by-endpoint auth audit) but
|
|
# on the FREE windows-latest runner. The file-mode hardening section
|
|
# (Section 6) is Linux-only and short-circuits on non-POSIX; the rest
|
|
# is platform-portable.
|
|
|
|
name: Windows Studio API CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'studio/**'
|
|
- 'unsloth/**'
|
|
- 'unsloth_cli/**'
|
|
- 'install.ps1'
|
|
- 'pyproject.toml'
|
|
- 'tests/studio/**'
|
|
- '.github/workflows/studio-windows-api-smoke.yml'
|
|
push:
|
|
branches: [main, pip]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
api-smoke:
|
|
name: Studio API & Auth Tests
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
GGUF_REPO: unsloth/gemma-3-270m-it-GGUF
|
|
GGUF_VARIANT: UD-Q4_K_XL
|
|
GGUF_FILE: gemma-3-270m-it-UD-Q4_K_XL.gguf
|
|
STUDIO_PORT: '18895'
|
|
HF_HOME: ${{ github.workspace }}/hf-cache
|
|
# Force UTF-8 for stdio (Windows defaults to cp1252; hf
|
|
# download prints a "✓" checkmark and crashes otherwise).
|
|
PYTHONIOENCODING: utf-8
|
|
PYTHONUTF8: '1'
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: '22'
|
|
cache: 'npm'
|
|
cache-dependency-path: studio/frontend/package-lock.json
|
|
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- name: Cache HF_HOME for ${{ env.GGUF_REPO }}
|
|
id: cache-hf
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: hf-cache
|
|
key: ${{ runner.os }}-hf-${{ env.GGUF_REPO }}-${{ env.GGUF_VARIANT }}-v1
|
|
|
|
- name: Prime HF_HOME with the GGUF
|
|
if: steps.cache-hf.outputs.cache-hit != 'true'
|
|
run: |
|
|
python -m pip install --upgrade huggingface_hub hf_transfer
|
|
mkdir -p hf-cache
|
|
HF_HUB_ENABLE_HF_TRANSFER=1 \
|
|
hf download "$GGUF_REPO" "$GGUF_FILE"
|
|
|
|
# Caches around install.ps1 -- restore the heavy artefacts the
|
|
# installer would otherwise rebuild from scratch every run.
|
|
# Keys hash every input file that would meaningfully change
|
|
# the produced artefact, so a stale cache cannot mask a real
|
|
# dependency change.
|
|
- name: Cache Studio venv (~/.unsloth/studio/unsloth_studio)
|
|
id: cache-studio-venv
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.unsloth/studio/unsloth_studio
|
|
key: ${{ runner.os }}-studio-venv-v1-${{ hashFiles('pyproject.toml', 'studio/backend/requirements/**', 'install.ps1', 'studio/setup.ps1', 'studio/install_python_stack.py') }}
|
|
- name: Cache prebuilt llama.cpp (~/.unsloth/llama.cpp)
|
|
id: cache-llama-prebuilt
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: ~/.unsloth/llama.cpp
|
|
key: ${{ runner.os }}-llama-cpp-prebuilt-v1-${{ hashFiles('studio/install_llama_prebuilt.py') }}
|
|
- name: Cache frontend dist (studio/frontend/dist)
|
|
id: cache-frontend-dist
|
|
uses: actions/cache@v4
|
|
with:
|
|
path: studio/frontend/dist
|
|
key: ${{ runner.os }}-studio-frontend-dist-v1-${{ hashFiles('studio/frontend/package-lock.json', 'studio/frontend/src/**', 'studio/frontend/index.html', 'studio/frontend/vite.config.*', 'studio/frontend/tsconfig*.json', 'studio/frontend/components.json') }}
|
|
|
|
- name: Install Studio (--local, --no-torch)
|
|
shell: pwsh
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
New-Item -ItemType Directory -Force -Path logs | Out-Null
|
|
# *>&1 captures Write-Host (Information stream) output;
|
|
# plain 2>&1 does not. setup.ps1 emits "prebuilt installed
|
|
# and validated" via Write-Host, and we grep for that.
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
& ./install.ps1 --local --no-torch *>&1 | Tee-Object -FilePath logs/install.log
|
|
|
|
- name: Assert install.ps1 used the Windows llama.cpp prebuilt
|
|
run: |
|
|
# Filesystem-based check (setup.ps1's stream output isn't
|
|
# captured back through this parent step's pipeline; see
|
|
# studio-windows-ui-smoke.yml for full explanation).
|
|
LLAMA_DIR=~/.unsloth/llama.cpp
|
|
INFO="$LLAMA_DIR/UNSLOTH_PREBUILT_INFO.json"
|
|
BIN="$LLAMA_DIR/build/bin/Release/llama-server.exe"
|
|
if grep -q "falling back to source build" logs/install.log; then
|
|
echo "::error::install.ps1 fell back to source-build llama.cpp on Windows."
|
|
grep -E "llama-prebuilt|llama.cpp" logs/install.log | tail -60
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$INFO" ]; then
|
|
echo "::error::no UNSLOTH_PREBUILT_INFO.json at $INFO."
|
|
ls -la "$LLAMA_DIR" || true
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$BIN" ]; then
|
|
echo "::error::no llama-server.exe at $BIN."
|
|
ls -la "$LLAMA_DIR/build/bin" || true
|
|
exit 1
|
|
fi
|
|
echo "install.ps1 installed the Windows prebuilt llama.cpp:"
|
|
cat "$INFO"
|
|
|
|
- name: Add Studio shim to GITHUB_PATH
|
|
# install.ps1's User-PATH update doesn't propagate to a
|
|
# running Git Bash session; export the shim dir so the
|
|
# next `unsloth ...` invocation finds it.
|
|
run: |
|
|
SHIM_DIR=~/.unsloth/studio/bin
|
|
if [ ! -f "$SHIM_DIR/unsloth.exe" ]; then
|
|
echo "::error::unsloth.exe shim not found at $SHIM_DIR"
|
|
ls -la ~/.unsloth/studio/ || true
|
|
exit 1
|
|
fi
|
|
cygpath -w "$SHIM_DIR" >> "$GITHUB_PATH"
|
|
|
|
- name: Patch Studio venv with full typer / pydantic dep trees
|
|
# Belt-and-suspenders: install.ps1's --no-deps install of
|
|
# no-torch-runtime.txt drops typer's and pydantic's runtime
|
|
# deps unless explicitly pinned. Re-install the ones whose
|
|
# deps don't pull torch.
|
|
run: |
|
|
STUDIO_PY=~/.unsloth/studio/unsloth_studio/Scripts/python.exe
|
|
if [ ! -f "$STUDIO_PY" ]; then
|
|
echo "::error::Studio venv python not at $STUDIO_PY"
|
|
ls -la ~/.unsloth/studio/ || true
|
|
exit 1
|
|
fi
|
|
"$STUDIO_PY" -m pip install --upgrade typer pydantic huggingface_hub
|
|
|
|
- name: Install pyjwt for the JWT-expiry forge test
|
|
run: python -m pip install 'pyjwt>=2.6'
|
|
|
|
- name: Reset auth + boot Studio (API-only)
|
|
run: |
|
|
unsloth studio reset-password
|
|
mkdir -p logs
|
|
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p "$STUDIO_PORT" \
|
|
> logs/studio.log 2>&1 &
|
|
echo "STUDIO_PID=$!" >> "$GITHUB_ENV"
|
|
|
|
- name: Wait for /api/health
|
|
run: |
|
|
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
|
|
sleep 1
|
|
done
|
|
jq -e '.status == "healthy"' /tmp/health.json
|
|
|
|
- name: Pass bootstrap password + rotated targets to the test
|
|
run: |
|
|
OLD=$(cat ~/.unsloth/studio/auth/.bootstrap_password)
|
|
NEW="ApiSmoke-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
|
|
NEW2="ApiSmoke-$(python -c 'import secrets; print(secrets.token_urlsafe(16))')"
|
|
echo "::add-mask::$OLD"
|
|
echo "::add-mask::$NEW"
|
|
echo "::add-mask::$NEW2"
|
|
echo "STUDIO_OLD_PW=$OLD" >> "$GITHUB_ENV"
|
|
echo "STUDIO_NEW_PW=$NEW" >> "$GITHUB_ENV"
|
|
echo "STUDIO_NEW2_PW=$NEW2" >> "$GITHUB_ENV"
|
|
|
|
- name: Run Studio API & Auth tests
|
|
# Do NOT pin STUDIO_AUTH_DIR here. The Mac/Linux mirrors
|
|
# hardcode runner-specific paths (/Users/runner/...,
|
|
# /home/runner/...), but on Windows the path is
|
|
# C:\Users\runneradmin\.unsloth\studio\auth and varies by
|
|
# runner image. studio_api_smoke.py defaults to
|
|
# Path.home()/".unsloth"/"studio"/"auth" when the env is
|
|
# unset, which is correct on every OS.
|
|
env:
|
|
BASE_URL: http://127.0.0.1:18895
|
|
run: python tests/studio/studio_api_smoke.py
|
|
|
|
- name: Stop Studio
|
|
if: always()
|
|
run: |
|
|
kill "${STUDIO_PID}" 2>/dev/null || true
|
|
sleep 2
|
|
|
|
- name: Upload API smoke logs
|
|
if: always()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
|
with:
|
|
name: windows-studio-api-smoke-log
|
|
path: |
|
|
logs/install.log
|
|
logs/studio.log
|
|
retention-days: 7
|