# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. # Mac counterpart to studio-update-smoke.yml. Verifies that on a real # Apple Silicon (macos-14, M1) runner: # # 1. install.sh --local --no-torch installs Studio AND auto-fetches # the prebuilt llama.cpp Mac binary (llama-bNNNN-bin-macos-arm64 # from ggml-org/llama.cpp). Hitting the source-build fallback is # treated as an Unsloth bug -- Studio must always pick the # prebuilt on Mac. # 2. unsloth studio update --local is idempotent. Two consecutive # runs both report "prebuilt up to date and validated", no # source-build fallback. # 3. The installed Studio still boots and /api/health returns # healthy after the update path. name: Mac 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-mac-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: macos-14 timeout-minutes: 30 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - 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' cache: 'pip' - name: Install Studio (--local, --no-torch) 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: Assert llama.cpp loads on this macOS run: bash .github/scripts/assert-llama-loads.sh - name: First update should be a no-op (prebuilt already validated) 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 Mac." 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." 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 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 on Mac" 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 run: | mkdir -p logs UNSLOTH_API_ONLY=1 unsloth studio -H 127.0.0.1 -p 18891 \ > logs/studio.log 2>&1 & PID=$! HEALTHY="" for i in $(seq 1 60); do if curl -fs http://127.0.0.1:18891/api/health > /tmp/health.json; then if python3 -c "import json,sys; d=json.load(open('/tmp/health.json')); sys.exit(0 if d.get('status')=='healthy' else 1)"; then HEALTHY=1 break fi fi sleep 1 done if [ -z "$HEALTHY" ]; 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 through scripts/uninstall.sh on real macOS. As a side # effect this exercises the macOS-only .app bundle + Launch Services # removal path (~/Applications/Unsloth Studio.app, lsregister -u) # which is not testable from a Linux runner. 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/Applications/Unsloth Studio.app" \ "$HOME/Desktop/Unsloth Studio.app" \ "$HOME/.local/bin/unsloth"; do if [ -e "$p" ] || [ -L "$p" ]; then echo "::error::leak: $p" leak=$((leak + 1)) fi done [ "$leak" -eq 0 ] || exit 1 sh scripts/uninstall.sh 2>&1 | tail -5 sh scripts/uninstall.sh 2>&1 | tail -5 echo "PASS: mac install -> update -> uninstall round-trip clean" - name: Upload update logs if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: mac-studio-update-log path: | logs/install.log logs/update.log logs/update2.log logs/studio.log logs/uninstall.log retention-days: 7