Make the interrupted-install legs able to fail

The probe treated a present .desktop-install-in-progress marker as proof of a
repairable state, but the drivers seed it unconditionally and never clear it, so
REPAIRABLE was unconditional and FALSE_READY unreachable. The Windows leg had no
kill-landed guard, blanket continue-on-error, and no repair re-run; -SkipTorch
was silently dropped, since install.ps1 parses only --no-torch.

Judge the re-run by whether the backend boots, on both platforms. The log grep
matched the frontend build printing "up to date" and failed a leg whose venv
was fine.
This commit is contained in:
danielhanchen 2026-07-28 13:46:26 +00:00
commit fe846fb43d
2 changed files with 81 additions and 18 deletions

View file

@ -118,7 +118,11 @@ def main(argv: list[str]) -> int:
(out / f"{label}.log").write_text(log, encoding = "utf-8", errors = "replace")
say(label, "ok" if rc == 0 else "failed")
# The in-progress marker #7490 writes before spawning the installer.
# The in-progress marker #7490 writes before spawning the installer. RECORDED ONLY,
# never used as repair evidence: both interrupt drivers seed it before every install
# and deliberately never clear it, so it is true on every leg by construction. Using
# it in the verdict below would make REPAIRABLE unconditional and FALSE_READY -- the
# single outcome this workflow exists to catch -- unreachable.
home = Path(os.environ.get("UNSLOTH_STUDIO_HOME") or (Path.home() / ".unsloth" / "studio"))
say("install_in_progress_marker", (home / ".desktop-install-in-progress").exists())
@ -198,7 +202,6 @@ def main(argv: list[str]) -> int:
facts.get("verify_install") == "failed"
or facts.get("desktop_runtime_check") == "failed"
or facts.get("capabilities.studio_install_ok") is False
or facts.get("install_in_progress_marker") is True
or not facts.get("cli_h_ok")
or not facts.get("capabilities_ok")
):

View file

@ -31,8 +31,12 @@ on:
- 'studio/src-tauri/src/preflight.rs'
- 'studio/src-tauri/src/preflight/**'
- 'unsloth_cli/commands/studio.py'
- '.github/scripts/interrupt*-install*'
- '.github/scripts/interrupted-install-probe.sh'
# `*` never matches `/`, and it is a literal `-install` that follows, so
# `interrupt*-install*` matches interrupt-install.sh / .ps1 but NOT the
# underscored probe. List the probe explicitly rather than rely on a glob.
- '.github/scripts/interrupt-install.sh'
- '.github/scripts/interrupt-install.ps1'
- '.github/scripts/interrupted_install_probe.py'
- '.github/workflows/interrupted-install-ci.yml'
workflow_dispatch:
@ -141,19 +145,22 @@ jobs:
rc=0
bash install.sh --tauri --local < /dev/null 2>&1 | tee logs/repair.log || rc=$?
echo "repair exit: $rc"
if grep -qiE "up to date|already current" logs/repair.log \
&& ! grep -qiE "forcing dependency pass|incomplete|repair" logs/repair.log; then
echo "::error::re-run reported the venv up to date without repairing it"
exit 1
fi
BIN="$HOME/.unsloth/studio/unsloth_studio/bin/unsloth"
[ -x "$BIN" ] || BIN="$HOME/.unsloth/studio/bin/unsloth"
python3 .github/scripts/interrupted_install_probe.py "$BIN" --out probe-after || true
v="$(python3 -c "import json;print(json.load(open('probe-after/verdict.json'))['verdict'])")"
if [ "$v" != "HEALTHY" ]; then
echo "::error::after a full re-run the backend still does not boot (verdict=$v)"
exit 1
# A booting backend IS the repair, whatever the log narrated. Judging by
# log text instead failed a leg whose venv was fine: the only match was
# the frontend build printing "up to date".
if [ "$v" = "HEALTHY" ]; then
echo "re-run repaired the install (verdict=HEALTHY)"
exit 0
fi
echo "::error::after a full re-run the backend still does not boot (verdict=$v)"
if grep -qiE "(venv|dependenc|python stack)[^|]*(up to date|already current)" logs/repair.log; then
echo "::error::and the re-run treated the venv as current instead of repairing it"
fi
exit 1
- name: Upload logs
if: always()
@ -171,18 +178,21 @@ jobs:
interrupt-windows:
name: windows kill@${{ matrix.label }}
env:
# install.ps1 has no equivalent of install.sh's --tauri guard, so the
# workspace-scoped root still works here.
# These legs run install.ps1 WITHOUT --tauri (install.ps1:189-215 rejects a custom
# root under --tauri exactly like install.sh:100-147), so the workspace-scoped
# root is usable here.
UNSLOTH_STUDIO_HOME: ${{ github.workspace }}/.studio-home
runs-on: windows-latest
timeout-minutes: 60
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
- {label: studio-deps, marker: 'studio deps'}
- {label: torch, marker: 'Installing PyTorch'}
# install.ps1 parses `--no-torch` (install.ps1:121); `-SkipTorch` matches no
# case in that switch and is silently dropped. The torch leg must NOT skip
# torch, or its marker never appears.
- {label: studio-deps, marker: 'studio deps', installArgs: '--no-torch --local'}
- {label: torch, marker: 'Installing PyTorch', installArgs: '--local'}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@ -194,13 +204,32 @@ jobs:
run: |
pwsh -NoProfile -File .github/scripts/interrupt-install.ps1 `
-Marker '${{ matrix.marker }}' -LogPath logs/install.log `
-InstallArgs '-SkipTorch --local' -KillAtSeconds 1500
-InstallArgs '${{ matrix.installArgs }}' -KillAtSeconds 1500
- name: The kill must have landed where it was aimed
shell: pwsh
run: |
$vals = @{}
foreach ($line in (Get-Content logs/interrupt.env)) {
$kv = $line -split '=', 2
if ($kv.Count -eq 2) { $vals[$kv[0]] = $kv[1] }
}
Write-Host "reason=$($vals['interrupt_reason']) killed=$($vals['interrupt_killed']) exit=$($vals['installer_exit'])"
if ($vals['interrupt_reason'] -ne 'marker-hit') {
Write-Host "::error::installer never reached '${{ matrix.marker }}' (reason=$($vals['interrupt_reason']))."
Write-Host '::error::This leg proves nothing: without this check it passes via the'
Write-Host '::error::probe NO_CLI safe path, exactly as the POSIX legs once did.'
Get-Content logs/install.log -Tail 30 -ErrorAction SilentlyContinue
exit 1
}
- name: What state is the install in?
id: probe
shell: pwsh
run: |
$bin = Join-Path $env:UNSLOTH_STUDIO_HOME 'unsloth_studio\Scripts\unsloth.exe'
if (-not (Test-Path $bin)) {
"verdict=NO_CLI" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
Write-Host '[probe] no unsloth CLI -> preflight reports NotInstalled (safe)'
exit 0
}
@ -210,6 +239,36 @@ jobs:
# desktop-runtime-check -- it would have failed the very PRs that add them,
# no matter how well they worked.
python .github/scripts/interrupted_install_probe.py $bin --out probe
$rc = $LASTEXITCODE
$v = (Get-Content probe/verdict.json -Raw | ConvertFrom-Json).verdict
"verdict=$v" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
exit $rc
- name: A re-run must repair, not short-circuit
# Same assertion the POSIX legs make. Without it a Windows leg proves only that
# the break was DETECTED, never that install.ps1's version fast path does not
# short-circuit over it -- which is the half of the bug that strands the user.
if: always() && steps.probe.outputs.verdict != 'NO_CLI' && steps.probe.outputs.verdict != 'HEALTHY'
shell: pwsh
run: |
pwsh -NoProfile -NonInteractive -File install.ps1 ${{ matrix.installArgs }} *>&1 |
Tee-Object -FilePath logs/repair.log
$bin = Join-Path $env:UNSLOTH_STUDIO_HOME 'unsloth_studio\Scripts\unsloth.exe'
python .github/scripts/interrupted_install_probe.py $bin --out probe-after
$v = (Get-Content probe-after/verdict.json -Raw | ConvertFrom-Json).verdict
# A booting backend IS the repair, whatever the log narrated. Judging by
# log text instead failed a POSIX leg whose venv was fine: the only match
# was the frontend build printing "up to date".
if ($v -eq 'HEALTHY') {
Write-Host 're-run repaired the install (verdict=HEALTHY)'
exit 0
}
Write-Host "::error::after a full re-run the backend still does not boot (verdict=$v)"
$log = Get-Content logs/repair.log -Raw -ErrorAction SilentlyContinue
if ($log -match '(?i)(venv|dependenc|python stack)[^|]*(up to date|already current)') {
Write-Host '::error::and the re-run treated the venv as current instead of repairing it'
}
exit 1
- name: Upload logs
if: always()
@ -219,5 +278,6 @@ jobs:
path: |
logs/
probe/
probe-after/
retention-days: 7
if-no-files-found: warn