* Replace standalone Studio wording with Unsloth Replace the single word Studio with Unsloth wherever it is used as shorthand for Unsloth Studio in docs, CLI output, UI strings, i18n locales, workflow display names, comments and docstrings. Kept unchanged: the full name Unsloth Studio, third party product names (LM Studio, Visual Studio, Mac Studio), feature names (Recipe Studio, Fine-tuning Studio and its translations), and all identifiers such as env vars, commands, paths and filenames. * Address review feedback on the Studio wording rename Use "an" before Unsloth where the rename left the article as "a". Restore the split brand where Unsloth and Studio render as two halves of the full product name: the onboarding sidebar subtitle and the IPv6 localhost warning. Scope two messages to the full name Unsloth Studio where plain Unsloth was misleading: the AMD README bullet and the CLI studio setup error.
295 lines
13 KiB
YAML
295 lines
13 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Windows counterpart to studio-update-smoke.yml /
|
|
# studio-mac-update-smoke.yml. Verifies that on the FREE
|
|
# windows-latest runner:
|
|
#
|
|
# 1. install.ps1 --local --no-torch installs Unsloth AND auto-fetches
|
|
# the prebuilt llama.cpp Windows binary (app-<tag>-windows-x64-cpu
|
|
# from unslothai/llama.cpp). Hitting the source-build fallback is
|
|
# treated as an Unsloth bug -- Unsloth must always pick the
|
|
# prebuilt on Windows.
|
|
# 2. unsloth studio update --local is idempotent. Two consecutive
|
|
# runs both report "prebuilt up to date and validated", no
|
|
# source-build fallback. The CLI's _find_setup_script picks
|
|
# setup.ps1 on Windows automatically.
|
|
# 3. The installed Unsloth still boots and /api/health returns
|
|
# healthy after the update path.
|
|
|
|
name: Windows Unsloth Update CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'install.ps1'
|
|
- 'scripts/uninstall.ps1'
|
|
- 'studio/setup.ps1'
|
|
- 'studio/setup.bat'
|
|
- 'studio/install_python_stack.py'
|
|
- 'studio/install_llama_prebuilt.py'
|
|
- 'studio/backend/requirements/**'
|
|
- 'unsloth_cli/commands/studio.py'
|
|
- 'pyproject.toml'
|
|
- '.github/workflows/studio-windows-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: Unsloth Updating Tests
|
|
runs-on: windows-latest
|
|
timeout-minutes: 30
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
env:
|
|
# Force UTF-8 for stdio (Windows defaults to cp1252; hf
|
|
# download / Unsloth CLI print "✓" checkmarks and crash
|
|
# otherwise).
|
|
PYTHONIOENCODING: utf-8
|
|
PYTHONUTF8: '1'
|
|
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'
|
|
# Don't cache pip: install.ps1 + setup.ps1 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: Pre-install Windows tweaks (npm 11 + Defender exclusions)
|
|
shell: pwsh
|
|
# Two surgical fixes against measured Windows-only install
|
|
# waste (vs Mac/Linux on the same SHA):
|
|
#
|
|
# (1) npm. setup.ps1's Get-NodeDecision requires Node 22.12+
|
|
# (or 20.19+ / 23+) AND npm >=11 because Vite 8 needs both.
|
|
# actions/setup-node@v4 with `node-version: '22'` lands
|
|
# Node 22.22.2 + the npm 10.9.7 it bundles, so the decision
|
|
# is "bundled" and setup.ps1 downloads an isolated Node (~30
|
|
# MB) we don't need on a runner that already has a fine Node.
|
|
# `npm install -g npm@^11` updates the runner's npm in-place
|
|
# in ~5 s, flipping the decision to "system" so setup.ps1
|
|
# reuses the existing Node with no download.
|
|
#
|
|
# (2) Defender. windows-latest's real-time scan opens / hashes
|
|
# every file Unsloth writes during install (Vite output =
|
|
# thousands of small chunks, uv pip = wheel-extraction =
|
|
# thousands of small files). The latency dominates the
|
|
# 200 s frontend build and the 90 s deps install. Adding
|
|
# ExclusionPath entries for the directories the install
|
|
# writes to drops per-file open latency from ~ms to ~us.
|
|
# Add-MpPreference needs admin; the runneradmin user has
|
|
# it, but wrap in try/catch so a permission flake leaves
|
|
# the install otherwise unaffected.
|
|
run: |
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
Write-Host "npm version before upgrade: $(npm -v)"
|
|
npm install -g 'npm@^11' 2>&1 | Out-Host
|
|
Write-Host "npm version after upgrade: $(npm -v)"
|
|
# NOTE: do NOT pre-create these directories before adding the
|
|
# exclusion -- creating an empty studio/frontend/dist trips
|
|
# setup.ps1 line 1281-1296's mtime-based "is the frontend
|
|
# stale?" check into "up to date, skip rebuild", because the
|
|
# newly-created dist's mtime is younger than every source
|
|
# file. Unsloth then boots with an empty dist and 500s on
|
|
# GET / with FileNotFoundError: dist\index.html. See run
|
|
# 25546676715 / job 74984469728.
|
|
# Add-MpPreference accepts paths that do not yet exist; the
|
|
# exclusion is registered and applies when the path
|
|
# materialises.
|
|
foreach ($p in @(
|
|
"$env:USERPROFILE\.unsloth",
|
|
"$env:USERPROFILE\AppData\Local\uv",
|
|
"$env:GITHUB_WORKSPACE\studio\frontend\node_modules",
|
|
"$env:GITHUB_WORKSPACE\studio\frontend\dist"
|
|
)) {
|
|
try {
|
|
Add-MpPreference -ExclusionPath $p -ErrorAction Stop
|
|
Write-Host "Defender exclusion added: $p"
|
|
} catch {
|
|
Write-Host "Defender exclusion skipped ($($_.Exception.Message)): $p"
|
|
}
|
|
}
|
|
|
|
- name: Install Unsloth (--local, --no-torch)
|
|
shell: pwsh
|
|
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: |
|
|
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 the parent pipeline).
|
|
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 Unsloth shim to GITHUB_PATH
|
|
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: 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 Windows."
|
|
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 Windows"
|
|
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 Unsloth 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=""
|
|
# Use jq (a Git Bash builtin) instead of `python -c
|
|
# open('/tmp/health.json')` to read the saved health
|
|
# response. Bash on windows-latest is MSYS Git Bash, which
|
|
# resolves `/tmp/...` against the MSYS root, while the
|
|
# python interpreter is Windows-native and resolves it
|
|
# against the current drive's root. The two paths don't
|
|
# agree, so python never finds the file curl just wrote.
|
|
# jq reads through MSYS, so the path matches. Mirrors what
|
|
# studio-windows-api-smoke.yml and the other Windows smoke
|
|
# workflows already do.
|
|
for i in $(seq 1 60); do
|
|
if curl -fs http://127.0.0.1:18891/api/health > /tmp/health.json; then
|
|
if jq -e '.status == "healthy"' /tmp/health.json >/dev/null; then
|
|
HEALTHY=1
|
|
break
|
|
fi
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ -z "$HEALTHY" ]; then
|
|
echo "Unsloth 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 Unsloth /api/health OK"
|
|
|
|
- name: Uninstall and verify clean
|
|
# Round-trip through scripts/uninstall.ps1 against the default
|
|
# install tree at %USERPROFILE%\.unsloth\studio. Catches
|
|
# regressions where install.ps1 starts writing under a new key
|
|
# (registry, Start Menu, %APPDATA%) and scripts/uninstall.ps1 has
|
|
# not been updated to match. Skips gracefully if
|
|
# scripts/uninstall.ps1 has not landed yet (lets this workflow
|
|
# merge before #5513).
|
|
shell: pwsh
|
|
run: |
|
|
New-Item -ItemType Directory -Force -Path logs | Out-Null
|
|
if (-not (Test-Path "$PWD\scripts\uninstall.ps1")) {
|
|
Write-Host "scripts/uninstall.ps1 not present in this tree; skipping round-trip"
|
|
"" | Set-Content logs/uninstall.log
|
|
exit 0
|
|
}
|
|
pwsh -NoProfile -File "$PWD\scripts\uninstall.ps1" *>&1 | Tee-Object -FilePath logs/uninstall.log
|
|
$leak = 0
|
|
foreach ($p in @(
|
|
"$env:USERPROFILE\.unsloth\studio",
|
|
"$env:USERPROFILE\.unsloth\studio\unsloth_studio",
|
|
"$env:USERPROFILE\.unsloth\studio\bin\unsloth.exe"
|
|
)) {
|
|
if (Test-Path -LiteralPath $p) {
|
|
Write-Host "::error::leak: $p"
|
|
$leak++
|
|
}
|
|
}
|
|
if ($leak -gt 0) { exit 1 }
|
|
# Idempotency.
|
|
pwsh -NoProfile -File "$PWD\scripts\uninstall.ps1" *>&1 | Select-Object -Last 5
|
|
pwsh -NoProfile -File "$PWD\scripts\uninstall.ps1" *>&1 | Select-Object -Last 5
|
|
Write-Host "PASS: windows install -> update -> uninstall round-trip clean"
|
|
|
|
- name: Upload update logs
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: windows-studio-update-log
|
|
path: |
|
|
logs/install.log
|
|
logs/update.log
|
|
logs/update2.log
|
|
logs/studio.log
|
|
logs/uninstall.log
|
|
retention-days: 7
|