Exercise the bundled Windows installer, and stop mislabelling installer sources

Four things that let a leg go green while proving nothing.

The desktop Windows job installed the bundle and launched it, and that was all.
On a fresh profile preflight reports not_installed and the app sits on the
install screen waiting for a click, so the process happily stays alive for 90
seconds without the bundled install.ps1 ever running. A bundle that shipped no
install.ps1 resource, or a broken one, passed this job -- which is the packaged
app failure the workflow exists to catch. macOS and Linux already invoke their
bundled script directly; Windows now does the same, via the resource NSIS laid
down next to the exe, invoked the way install.rs invokes it, then asserts the
managed venv exists and can import torch. Its timeout goes to 60 minutes
because a full torch install on a Windows runner is the slowest of the three.

A manual run that selects installer_source: published only redirected the macOS
and Linux jobs. WSL kept copying the checked-out install.sh and Windows kept
running the checked-out install.ps1, so a run asking whether the script on
unsloth.ai works reported on this ref under the published label. Both now honor
the selection; install.ps1 advertises its own unsloth.ai URL, so published has a
meaning on Windows too. Both branches stay empty on pull_request and push, so
automatic runs are unchanged.

The push-to-main filter listed only install.sh, install.ps1 and this workflow,
while the PR filter also covers setup.sh, setup.ps1, install_python_stack.py and
the clean-machine helpers. A direct push touching those skipped the workflow
entirely, so the post-merge backstop never ran for the files the source overlay
was added to cover. The two lists now match.

Neither filter covered studio/backend/requirements, even though the overlay
exists precisely so a constraints change is resolved on a machine with no
compiler and no cached wheels. The update-smoke workflows cannot stand in: they
start from a preinstalled Python and full developer tooling.
This commit is contained in:
danielhanchen 2026-07-28 23:18:06 +00:00
commit 06d2725e09
2 changed files with 96 additions and 7 deletions

View file

@ -57,13 +57,28 @@ on:
- 'studio/setup.sh'
- 'studio/setup.ps1'
- 'studio/install_python_stack.py'
# The overlay exists so a constraints or requirements change is actually
# exercised here (see the header). Without these paths the one workflow that
# resolves them on a machine with no compiler and no wheels cached never runs
# for the PR that changes them, and the update-smoke jobs cannot stand in:
# they start from a preinstalled Python and full developer tooling.
- 'studio/backend/requirements/**'
- '.github/scripts/clean-machine-*.sh'
- '.github/workflows/clean-machine-install-ci.yml'
push:
branches: [main]
# Same list as the PR filter. A direct push to main that changed setup.sh,
# setup.ps1, install_python_stack.py, a requirements file or a clean-machine
# helper skipped this workflow entirely, so the post-merge run that is supposed
# to be the backstop for exactly those files never happened.
paths:
- 'install.sh'
- 'install.ps1'
- 'studio/setup.sh'
- 'studio/setup.ps1'
- 'studio/install_python_stack.py'
- 'studio/backend/requirements/**'
- '.github/scripts/clean-machine-*.sh'
- '.github/workflows/clean-machine-install-ci.yml'
workflow_dispatch:
inputs:
@ -579,11 +594,25 @@ jobs:
# Only ca-certificates + curl: the advertised one-liner cannot start without
# a transport. Everything else must come from the installer.
wsl -d unsloth-ci -u root -- sh -c 'apt-get update -qq && apt-get install -y -qq --no-install-recommends ca-certificates curl' 2>&1 | Tee-Object -FilePath logs/wsl-bootstrap.log
# Copy the script in rather than reaching across /mnt/c: a DrvFs path brings
# Windows permissions and CRLF risk, neither of which a real WSL user has.
$wslPath = (wsl -d unsloth-ci -- wslpath -a "$($env:GITHUB_WORKSPACE -replace '\\','/')/install.sh").Trim()
Write-Host "installer source in WSL: $wslPath"
wsl -d unsloth-ci -u root -- cp "$wslPath" /root/install.sh
# A dispatch that selects `published` is asking whether the script on
# unsloth.ai works; running the checked-out one here and reporting the leg
# green answered a different question under the same name. Empty on
# pull_request/push, so automatic runs stay on this ref.
if ('${{ inputs.installer_source }}' -eq 'published') {
Write-Host 'installer: published (unsloth.ai)'
wsl -d unsloth-ci -u root -- sh -c 'curl -fsSL https://unsloth.ai/install.sh -o /root/install.sh'
if ($LASTEXITCODE -ne 0) {
Write-Host '::error::could not fetch the published installer inside WSL'
exit 1
}
} else {
# Copy the script in rather than reaching across /mnt/c: a DrvFs path
# brings Windows permissions and CRLF risk, neither of which a real WSL
# user has.
$wslPath = (wsl -d unsloth-ci -- wslpath -a "$($env:GITHUB_WORKSPACE -replace '\\','/')/install.sh").Trim()
Write-Host "installer source in WSL: $wslPath"
wsl -d unsloth-ci -u root -- cp "$wslPath" /root/install.sh
}
# Piped, same shape as `curl ... | sh`, so an early exit still exposes the
# broken pipe, but the script under test is this ref not production's.
wsl -d unsloth-ci -u root -- sh -c 'cd /root && cat install.sh | sh' 2>&1 | Tee-Object -FilePath logs/wsl-install.log
@ -854,11 +883,24 @@ jobs:
UNSLOTH_CI_SOURCE_OVERLAY: ${{ matrix.overlay && inputs.installer_source != 'published' && github.workspace || '' }}
run: |
$ErrorActionPreference = 'Continue'
# Windows ships its own published script (install.ps1:3), so `published`
# has a meaning here too. Running the checked-out one regardless made a
# dispatch that asked about unsloth.ai report on this ref instead. Empty on
# pull_request/push, so automatic runs stay on this ref.
$script = './install.ps1'
if ('${{ inputs.installer_source }}' -eq 'published') {
Invoke-WebRequest -Uri https://unsloth.ai/install.ps1 `
-OutFile published-install.ps1 -UseBasicParsing -TimeoutSec 300
$script = './published-install.ps1'
Write-Host 'installer: published (unsloth.ai)'
} else {
Write-Host "installer: this ref ($env:GITHUB_SHA)"
}
# No -SkipTorch: install.ps1 has no param block and its parser matches
# `--no-torch` only (112-142), so the token was silently dropped and every
# Windows leg installed torch anyway. The assert below needs torch, so ask
# for it explicitly rather than by accident.
& ./install.ps1 *>&1 | Tee-Object -FilePath logs/install.log
& $script *>&1 | Tee-Object -FilePath logs/install.log
$rc = $LASTEXITCODE
Write-Host "installer exit code: $rc"
exit $rc

View file

@ -390,7 +390,9 @@ jobs:
windows:
name: desktop windows
runs-on: windows-latest
timeout-minutes: 45
# 60, not 45: this job now runs the bundled installer, and a full torch install
# on a Windows runner is the slowest of the three platforms.
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@ -448,6 +450,51 @@ jobs:
Write-Host "installed: $($found.FullName)"
"APP_EXE=$($found.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Run the bundled installer, the path first launch takes
shell: pwsh
run: |
# The launch step below only proves the process stayed alive. On a fresh
# profile preflight reports not_installed and the app waits for the user to
# click Install (use-tauri-backend.ts:252-254, startup-screen.tsx:388-389),
# so this job passed on a bundle whose embedded install.ps1 was missing or
# broken -- the packaged-app failure the workflow exists to catch, and the
# one thing the macOS and Linux rows now check and Windows did not.
# tauri.conf.json:56-59 ships install.ps1 as a bundle resource, so find it
# where NSIS put it and invoke it as install.rs:326-341 does.
$root = Split-Path -Parent $env:APP_EXE
$ps1 = Get-ChildItem -Path $root -Recurse -Filter 'install.ps1' -ErrorAction SilentlyContinue |
Select-Object -First 1
if (-not $ps1) {
Write-Host '::error::the bundle ships no install.ps1 resource'
exit 1
}
Write-Host "bundled installer: $($ps1.FullName)"
# --tauri rejects a custom studio home (install.ps1:189-215), so drop the
# workspace-scoped override the same way install.rs scrubs it (354-357).
Remove-Item Env:UNSLOTH_STUDIO_HOME -ErrorAction SilentlyContinue
& powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass `
-File $ps1.FullName --tauri *>&1 | Tee-Object -FilePath logs/bundled-install.log
$rc = $LASTEXITCODE
Write-Host "bundled installer exit: $rc"
if ($rc -ne 0) {
Write-Host "::error::bundled installer exited $rc"
exit $rc
}
# The exit code alone is not enough: it is the venv the app then boots from.
$py = Join-Path $env:USERPROFILE '.unsloth\studio\unsloth_studio\Scripts\python.exe'
if (-not (Test-Path $py)) {
Write-Host "::error::bundled installer left no venv at $py"
exit 1
}
& $py -V
# install.rs passes only --tauri, so torch is part of first launch, and a
# venv that cannot import it is the unbootable environment from the report.
& $py -c "import torch; print('torch', torch.__version__)"
if ($LASTEXITCODE -ne 0) {
Write-Host '::error::the bundled install produced a venv with no working torch'
exit 1
}
- name: Launch and prove it stays up
shell: pwsh
run: |