* Stop HF 429 rate limits from sinking the llama.cpp prebuilt path in Studio CI The Windows Studio API smoke job failed when anonymous huggingface.co fetches of the tiny GGUF validation model (stories260K.gguf) hit HTTP 429 on the shared runner IP. The installer correctly refused the unvalidated prebuilt and fell back to a source build, which the prebuilt assert then flags. Three layers fix this: 1. Installer: auth_headers sends HF_TOKEN (or HUGGING_FACE_HUB_TOKEN) to huggingface.co hosts, mirroring the existing GH_TOKEN handling for the GitHub API rate limit. A redirect handler strips Authorization when a download is redirected off-host (CDN signed URLs reject foreign auth; urllib forwards headers on redirect, unlike requests/huggingface_hub). 2. Workflows: the HF_HOME prime steps also prefetch the validation model so the install's hf_hub_download resolves from the local cache even when the Hub is rate limiting; cache keys bumped v1 to v2 to repopulate. This also covers fork PRs, which cannot see secrets. 3. Workflows: every Install Studio / update step that already passes GH_TOKEN now also passes HF_TOKEN, so both the huggingface_hub path and the direct URL fallback are authenticated. Tests: tests/studio/install/test_hf_auth.py covers token-to-host routing, the cross-host redirect strip, and the download_bytes wiring (offline). Verified live: authenticated download of the validation model through the new opener (CDN redirect exercised, pinned sha matches) and an offline hf_hub_download cache hit against an HF_HOME primed by the new step. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
194 lines
7.9 KiB
YAML
194 lines
7.9 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Verifies that `unsloth studio update --local` is idempotent: a fresh
|
|
# install via install.sh, followed by `unsloth studio update --local`,
|
|
# succeeds and is a no-op for the llama.cpp prebuilt (it should report
|
|
# "prebuilt up to date and validated", not re-run the source build).
|
|
#
|
|
# This catches regressions in setup.sh's update path that the existing
|
|
# GGUF / wheel jobs would miss because they only invoke install.sh once.
|
|
|
|
name: Studio Update CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'install.sh'
|
|
- 'scripts/uninstall.sh'
|
|
- 'studio/setup.sh'
|
|
- 'studio/install_python_stack.py'
|
|
- 'studio/install_llama_prebuilt.py'
|
|
- 'studio/backend/requirements/**'
|
|
- 'unsloth_cli/commands/studio.py'
|
|
- 'pyproject.toml'
|
|
- '.github/workflows/studio-update-smoke.yml'
|
|
push:
|
|
branches: [main, pip]
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
update-idempotency:
|
|
name: Studio Updating Tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Linux deps for llama.cpp prebuilt
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
libcurl4-openssl-dev libssl-dev jq
|
|
|
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: '22'
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.12'
|
|
# Don't cache pip: this job runs `bash install.sh` and
|
|
# `unsloth studio update --local` which both go through
|
|
# `uv` and never populate ~/.cache/pip. setup-python's
|
|
# post-step then fatal-errors with "Cache folder path is
|
|
# retrieved for pip but doesn't exist on disk".
|
|
|
|
- name: Install Studio (--local, --no-torch)
|
|
# Pass the workflow token so the llama.cpp prebuilt installer's
|
|
# GitHub-API call to list releases isn't rate-limited (60/hr
|
|
# unauthenticated). Without this, three consecutive install +
|
|
# update + update calls in this job exceed the limit and the
|
|
# prebuilt path falls back to source build.
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
run: |
|
|
mkdir -p logs
|
|
set -o pipefail
|
|
bash install.sh --local --no-torch 2>&1 | tee logs/install.log
|
|
|
|
- name: First update should be a no-op (prebuilt already validated)
|
|
# `unsloth studio update --local` runs studio/setup.sh against
|
|
# the local repo. Right after install.sh the llama.cpp prebuilt
|
|
# has just been installed and validated, so the second run must
|
|
# take the "prebuilt up to date and validated" code path. Any
|
|
# source-build fallback or re-download here means setup.sh's
|
|
# idempotency regressed.
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
run: |
|
|
set -o pipefail
|
|
unsloth studio update --local 2>&1 | tee logs/update.log
|
|
if grep -q "falling back to source build" logs/update.log; then
|
|
echo "::error::studio update fell back to source-build llama.cpp on a fresh install. setup.sh idempotency regressed."
|
|
grep -E "llama-prebuilt|llama.cpp" logs/update.log | tail -60
|
|
exit 1
|
|
fi
|
|
if ! grep -qE "prebuilt up to date and validated|prebuilt installed and validated" logs/update.log; then
|
|
echo "::error::no prebuilt up-to-date marker in update.log. Did setup.sh skip the prebuilt path on update?"
|
|
grep -E "llama-prebuilt|llama.cpp" logs/update.log | tail -60
|
|
exit 1
|
|
fi
|
|
echo "update path took the prebuilt fast path"
|
|
|
|
- name: Second update must also be a no-op
|
|
# Two consecutive `update`s back-to-back is the usual desktop
|
|
# flow (auto-update, then user-triggered update). Asserting the
|
|
# second run is also clean rules out hidden state changes from
|
|
# the first one.
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
|
run: |
|
|
set -o pipefail
|
|
unsloth studio update --local 2>&1 | tee logs/update2.log
|
|
grep -q "falling back to source build" logs/update2.log && {
|
|
echo "::error::second update fell back to source build"
|
|
tail -60 logs/update2.log; exit 1; } || true
|
|
grep -qE "prebuilt up to date and validated|prebuilt installed and validated" logs/update2.log
|
|
echo "second update was clean"
|
|
|
|
- name: Boot Studio briefly to confirm the install is still usable
|
|
# If `update --local` accidentally broke the venv or wiped the
|
|
# llama-server binary, the server would fail to start here.
|
|
run: |
|
|
mkdir -p logs
|
|
UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18891 \
|
|
> logs/studio.log 2>&1 &
|
|
PID=$!
|
|
for i in $(seq 1 60); do
|
|
if curl -fs http://127.0.0.1:18891/api/health > /tmp/health.json; then
|
|
jq -e '.status == "healthy"' /tmp/health.json
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if ! jq -e '.status == "healthy"' /tmp/health.json 2>/dev/null; then
|
|
echo "Studio failed to come up after `update`"
|
|
tail -200 logs/studio.log
|
|
kill "$PID" 2>/dev/null || true
|
|
exit 1
|
|
fi
|
|
kill "$PID" 2>/dev/null || true
|
|
echo "post-update Studio /api/health OK"
|
|
|
|
- name: Uninstall and verify clean
|
|
# Round-trip the installer through scripts/uninstall.sh: confirms the
|
|
# uninstaller actually finds and removes everything install.sh +
|
|
# update wrote. Safety-guard scenarios (refuse-$HOME etc.) belong
|
|
# in a separate fast smoke job; this is the happy-path cleanup
|
|
# assertion that catches regressions where install.sh starts
|
|
# writing to a new location and scripts/uninstall.sh hasn't caught up.
|
|
# Skips gracefully if scripts/uninstall.sh has not landed yet (lets
|
|
# this workflow merge before #5497).
|
|
run: |
|
|
set -o pipefail
|
|
if [ ! -f scripts/uninstall.sh ]; then
|
|
echo "scripts/uninstall.sh not present in this tree; skipping round-trip"
|
|
: > logs/uninstall.log
|
|
exit 0
|
|
fi
|
|
sh scripts/uninstall.sh 2>&1 | tee logs/uninstall.log
|
|
leak=0
|
|
for p in \
|
|
"$HOME/.unsloth/studio" \
|
|
"$HOME/.local/share/unsloth" \
|
|
"$HOME/Desktop/Unsloth Studio.desktop" \
|
|
"$HOME/.local/bin/unsloth"; do
|
|
if [ -e "$p" ] || [ -L "$p" ]; then
|
|
echo "::error::leak: $p"
|
|
ls -la "$p" 2>&1 | head -3
|
|
leak=$((leak + 1))
|
|
fi
|
|
done
|
|
[ "$leak" -eq 0 ] || exit 1
|
|
# Idempotent: re-runs exit 0 on an empty $HOME.
|
|
sh scripts/uninstall.sh 2>&1 | tail -5
|
|
sh scripts/uninstall.sh 2>&1 | tail -5
|
|
echo "PASS: install -> update -> uninstall round-trip clean"
|
|
|
|
- name: Upload update logs
|
|
# Always upload so a green run still leaves the install + two
|
|
# update logs + uninstall log reviewable.
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: studio-update-log
|
|
path: |
|
|
logs/install.log
|
|
logs/update.log
|
|
logs/update2.log
|
|
logs/studio.log
|
|
logs/uninstall.log
|
|
retention-days: 7
|