From 9b8ee6c7736d58f72c70bf8631fab065ef236b46 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 18 May 2026 02:11:52 -0700 Subject: [PATCH] Studio update CI: round-trip install -> update -> uninstall (#5536) * Studio update CI: round-trip install -> update -> uninstall Adds an "Uninstall and verify clean" step to the three existing studio-{,-mac-,-windows-}update-smoke.yml workflows so each one ends by running uninstall.sh / uninstall.ps1 against the install it just produced, then asserting that the install dir, launcher data dir, desktop shortcut, CLI shim (and on Mac, the .app bundle) are all gone. Two trailing reruns confirm idempotency. The uninstall log is added to the existing artifact bundle. Catches regressions where install.sh / install.ps1 starts writing to a new path (registry key, Start Menu entry, %APPDATA% subdir, etc.) and uninstall.{sh,ps1} has not been updated to match. Safety-guard scenarios (refuse-\$HOME, refuse-non-Studio, tilde expansion, etc.) are intentionally NOT exercised here -- those belong in a dedicated fast smoke job that does not have to wait on a 5-15 min install. Wall-clock overhead is ~30-45 s on each runner. Path filters extended to include uninstall.sh / uninstall.ps1 so a pure uninstaller change also triggers the round-trip check. * Skip round-trip step when uninstall.{sh,ps1} are not in tree --------- Co-authored-by: Daniel Han --- .github/workflows/studio-mac-update-smoke.yml | 34 ++++++++++++++++ .github/workflows/studio-update-smoke.yml | 39 ++++++++++++++++++- .../workflows/studio-windows-update-smoke.yml | 35 +++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/.github/workflows/studio-mac-update-smoke.yml b/.github/workflows/studio-mac-update-smoke.yml index 07d26b9ab3..cfa192b470 100644 --- a/.github/workflows/studio-mac-update-smoke.yml +++ b/.github/workflows/studio-mac-update-smoke.yml @@ -21,6 +21,7 @@ on: pull_request: paths: - 'install.sh' + - 'uninstall.sh' - 'studio/setup.sh' - 'studio/install_python_stack.py' - 'studio/install_llama_prebuilt.py' @@ -137,6 +138,38 @@ jobs: kill "$PID" 2>/dev/null || true echo "post-update Studio /api/health OK" + - name: Uninstall and verify clean + # Round-trip through 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 + # uninstall.sh has not landed yet (lets this workflow merge + # before #5497). + run: | + set -o pipefail + if [ ! -f uninstall.sh ]; then + echo "uninstall.sh not present in this tree; skipping round-trip" + : > logs/uninstall.log + exit 0 + fi + sh 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 uninstall.sh 2>&1 | tail -5 + sh 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 @@ -147,4 +180,5 @@ jobs: logs/update.log logs/update2.log logs/studio.log + logs/uninstall.log retention-days: 7 diff --git a/.github/workflows/studio-update-smoke.yml b/.github/workflows/studio-update-smoke.yml index 1c353e933a..b28e2bf0bd 100644 --- a/.github/workflows/studio-update-smoke.yml +++ b/.github/workflows/studio-update-smoke.yml @@ -15,6 +15,7 @@ on: pull_request: paths: - 'install.sh' + - 'uninstall.sh' - 'studio/setup.sh' - 'studio/install_python_stack.py' - 'studio/install_llama_prebuilt.py' @@ -139,9 +140,44 @@ jobs: kill "$PID" 2>/dev/null || true echo "post-update Studio /api/health OK" + - name: Uninstall and verify clean + # Round-trip the installer through 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 uninstall.sh hasn't caught up. + # Skips gracefully if uninstall.sh has not landed yet (lets this + # workflow merge before #5497). + run: | + set -o pipefail + if [ ! -f uninstall.sh ]; then + echo "uninstall.sh not present in this tree; skipping round-trip" + : > logs/uninstall.log + exit 0 + fi + sh 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 uninstall.sh 2>&1 | tail -5 + sh 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 reviewable. + # update logs + uninstall log reviewable. if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -151,4 +187,5 @@ jobs: logs/update.log logs/update2.log logs/studio.log + logs/uninstall.log retention-days: 7 diff --git a/.github/workflows/studio-windows-update-smoke.yml b/.github/workflows/studio-windows-update-smoke.yml index 157874d404..b412d60921 100644 --- a/.github/workflows/studio-windows-update-smoke.yml +++ b/.github/workflows/studio-windows-update-smoke.yml @@ -23,6 +23,7 @@ on: pull_request: paths: - 'install.ps1' + - 'uninstall.ps1' - 'studio/setup.ps1' - 'studio/setup.bat' - 'studio/install_python_stack.py' @@ -266,6 +267,39 @@ jobs: kill "$PID" 2>/dev/null || true echo "post-update Studio /api/health OK" + - name: Uninstall and verify clean + # Round-trip through 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 uninstall.ps1 has not been updated + # to match. Skips gracefully if 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\uninstall.ps1")) { + Write-Host "uninstall.ps1 not present in this tree; skipping round-trip" + "" | Set-Content logs/uninstall.log + exit 0 + } + pwsh -NoProfile -File "$PWD\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\uninstall.ps1" *>&1 | Select-Object -Last 5 + pwsh -NoProfile -File "$PWD\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 @@ -276,4 +310,5 @@ jobs: logs/update.log logs/update2.log logs/studio.log + logs/uninstall.log retention-days: 7