Fix Studio custom folders on Linux external drives (#6799) #3006
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 }} | |
| # Withheld on PR: this step runs checked-out PR code; public GGUF still downloads. | |
| HF_TOKEN: ${{ github.event_name != 'pull_request' && 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 }} | |
| # Withheld on PR: this step runs checked-out PR code; public GGUF still downloads. | |
| HF_TOKEN: ${{ github.event_name != 'pull_request' && 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 }} | |
| # Withheld on PR: this step runs checked-out PR code; public GGUF still downloads. | |
| HF_TOKEN: ${{ github.event_name != 'pull_request' && 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 |