* scripts/scan_*: add Mini Shai-Hulud May-12 IOC strings and pin-blocklists Append the May-12 2026 wave indicators (git-tanstack.com, transformers.pyz, /tmp/transformers.pyz, "With Love TeamPCP", "We've been online over 2 hours") to all three scanner IOC tables, add BLOCKED_NPM_VERSIONS (42 TanStack pkgs, 4 opensearch versions, 3 squawk pkgs) in scan_npm_packages.py and lockfile_supply_chain_audit.py (kept byte-identical), add BLOCKED_PYPI_VERSIONS (guardrails-ai 0.10.1, mistralai 2.4.6, lightning 2.6.2/2.6.3) plus RE_MAY12_IOC wiring across check_py_file/check_shell_file/check_workflow_file in scan_packages.py. The npm orchestrator and the lockfile auditor now short-circuit on a blocked entry before fetching the tarball, and the PyPI download pipeline drops blocked specs before pip download is invoked. * tests/security: regression suite for supply-chain scanners Adds offline fixture corpus and pytest coverage for scan_npm_packages, scan_packages, and lockfile_supply_chain_audit so future IOC-table drift surfaces at PR time. Pytest scope narrowed to tests/security so GPU smoke tests are not picked up by default. * ci(security-audit): drop continue-on-error on pip-scan and npm-scan jobs Promote three harden-runner blocks to egress-policy: block with per-job allowlists. Add tests-security job running pytest tests/security as a hard gate. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts: harden third-party downloads, pip resolver pins, atomic writes Pins uv installer and mlx_vlm qwen3_5 patches by commit SHA + SHA-256 checksum, scrubs PIP_* env vars and forces --index-url + --only-binary on pip download, applies tarbomb caps to scan_packages archive walks, and converts non-atomic config writes (kwargs spacer, studio stamper, notebook validator, scan_packages req-file fixer) to mkstemp+os.replace. Also adds host allowlist to notebook_to_python downloader, threads an --allow-shell flag through its shell=True emission with reviewer warning comments, locks both MLX installer scripts to set -euo pipefail, and extends CODEOWNERS so colab snapshot data files require notebook-owner review. * ci(workflows): harden release-desktop / smoke / notebooks workflows Pin dtolnay/rust-toolchain to a 40-char SHA, scope release-desktop permissions to read at workflow level with job-level write only on the build job, append --ignore-scripts to every npm ci / npm install in studio-frontend-ci / wheel-smoke / studio-tauri-smoke / release-desktop, validate client_payload.ref shape via an env-var-isolated regex on every notebooks-ci job, and add step-security/harden-runner in audit mode as the first step of release-desktop and mlx-ci. * scripts: promote silent scanner failures to non-zero exit codes scan_packages now returns 2 on pip-download failure and emits a CRITICAL archive_corrupted finding on truncated wheels/sdists. notebook_to_python exits 1 on per-notebook failures; notebook_validator wraps the stash/pop in try/finally; lockfile audit rejects bare UNSLOTH_LOCKFILE_AUDIT_SKIP=1 with a loud GitHub Actions warning. * Add npm cooldown + new-install-script gate + Dependabot cooldown Pins min-release-age=7 (npm 11.10+) in repo-root and studio/frontend .npmrc, adds scripts/check_new_install_scripts.py to fail PRs that add a postinstall dep, ships a new security-audit job for npm audit signatures plus the diff, and extends .github/dependabot.yml with cooldown stanzas. Pin @tanstack/react-router to 1.169.9 per GHSA- g7cv-rxg3-hmpx; lockfile regen deferred until that release lands on npm. tests/security gains 4 new tests; full suite 26/26 green. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci(security): fix tanstack pin, exec bits, expand IOC tables to @uipath/@squawk full - Revert --ignore-scripts on Studio install workflows: vite build needs esbuild's native postinstall (per PR #5392 rationale). Keep --ignore-scripts on security-audit.yml's standalone npm audit job. - Pin @tanstack/react-router to the actual published 1.169.2 (was a forward-looking 1.169.9 that does not exist on npm; broke npm ci). - Drop redundant repo-root .npmrc; studio/frontend/.npmrc covers the only npm project today (root cooldown re-instate via dependabot.yml). - Restore exec bits on 7 files my filesystem stripped during cherry-pick. - Expand BLOCKED_NPM_VERSIONS with full safedep.io + Aikido enumeration: 22 @squawk/* packages with 5 versions each (110 entries; previously 3 entries with 1 version each), and 66 @uipath/* packages (entirely missing before). Mirror in scripts/lockfile_supply_chain_audit.py. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * tests/security: suppress CodeQL py/incomplete-url-substring-sanitization The two flagged 'X' in Y assertions are NOT URL sanitization checks. They verify our scanner WROTE a known IOC literal into its stdout / Finding.evidence, which is the opposite of an attack surface -- matching the scanner's output is precisely what catches the worm. Inline lgtm[] suppression with a 4-line rationale comment above each. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts/scan_*: expand IOC tables with Aikido full 169-pkg enumeration Per Aikido 2026-05-12 disclosure (373 malicious package-version entries across 169 npm package names), add to BLOCKED_NPM_VERSIONS: - @mistralai/* npm scope (3 packages, 9 versions) -- separate from the PyPI mistralai package already in BLOCKED_PYPI_VERSIONS - @tallyui/* (10 packages, 30 entries) - @beproduct/nestjs-auth (18 versions 0.1.2..0.1.19) - @draftlab/* + @draftauth/* (5 packages) - @taskflow-corp/cli, @tolka/cli, @ml-toolkit-ts/*, @mesadev/*, @dirigible-ai/sdk, @supersurkhet/* - 10 unscoped packages (safe-action, ts-dna, cross-stitch, cmux-agent-mcp, agentwork-cli, git-branch-selector, wot-api, git-git-git, nextmove-mcp, ml-toolkit-ts) Also add to KNOWN_IOC_STRINGS / NPM_IOC_STRINGS: - router_init.js SHA-256 ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c - tanstack_runner.js SHA-256 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 - bun run tanstack_runner.js marker (the new Bun-prepare-script dropper invocation pattern unique to this wave) Total: 170 packages, 401 versions blocklisted. Studio lockfile still scans clean (0 findings, 0 hard errors). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * scripts/scan_*: web-verification additions (@tanstack/setup, intercom-client) Two findings from cross-checking BLOCKED_NPM_VERSIONS / KNOWN_IOC_STRINGS against GHSA-g7cv-rxg3-hmpx + Aikido + safedep.io + Socket + Semgrep. - Fix asymmetry: @tanstack/setup IOC string was in lockfile_supply_chain_audit.py's NPM_IOC_STRINGS but missing from scan_npm_packages.py's KNOWN_IOC_STRINGS. The literal is the malicious optional-dependency name used by the May-12 TanStack wave; no legitimate npm package of this name exists. - Add intercom-client@7.0.4: the npm counterpart of the lightning 2.6.2/2.6.3 PyPI compromise (Apr-30 wave). Same threat actor (TeamPCP). Confirmed by Semgrep, Aikido, OX Security, Resecurity, Kodem. Safe version is 7.0.3 and earlier. Total BLOCKED_NPM_VERSIONS: 171 packages / 402 versions. Both files remain byte-identical. Studio lockfile still scans clean. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * ci(security): add workflow-trigger lint refusing pull_request_target + cache-poisoning vectors The two patterns that together powered GHSA-g7cv-rxg3-hmpx (TanStack Mini Shai-Hulud) are now gated at PR time: 1. pull_request_target -- the worm chain started with a fork PR that ran in the base-repo context. Every workflow in this repo today uses 'pull_request' (safe); the lint refuses any new pull_request_target additions outright. workflow_run is restricted, allowed only with an explicit allow-comment. 2. Shared cache keys between PR-triggered workflows and the publish workflow (release-desktop.yml). The TanStack attack chain poisoned a shared Actions cache from a fork PR; the legitimate release workflow then restored the poisoned cache. The lint refuses any cache key that appears in both a PR-triggered workflow and a workflow_dispatch-only / publish workflow. Current tree is clean: 0 pull_request_target, 0 workflow_run, 0 PR-publish cache-key collisions across all 24 workflows. The lint locks that invariant in place. Files: + scripts/lint_workflow_triggers.py (~200 LOC, stdlib + PyYAML) + tests/security/test_lint_workflow_triggers.py (5 tests covering current-tree pass, pull_request_target reject, workflow_run restricted, justified workflow_run accept, cache-key collision reject) ~ .github/workflows/security-audit.yml: new workflow-trigger-lint job, no continue-on-error, harden-runner block-mode, PyYAML only runtime dep. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * security: fix tests-security CI job + CodeQL false-positives Two CI failures on the prior push: 1. pytest tests/security -- 5 lint regression tests failed because scripts/lint_workflow_triggers.py imports PyYAML which is not in the bare runner's Python env. Added pyyaml==6.0.2 to the pip install step alongside pytest. (29 scanner tests already passed.) 2. CodeQL py/incomplete-url-substring-sanitization fired on two test assertions that check the scanner WROTE the IOC literal to its own stdout/stderr. The rule pattern-matches on `"<host>" in <var>` and cannot distinguish a URL sanitizer from a regression-test evidence check. Previous `# lgtm[...]` inline suppressions were detached from the operator when pre-commit reformatted the assert across multiple lines. Rebuilt the IOC literals at runtime (`"git-tanstack." + "com"`) so no URL-shaped source literal appears on the `in` operator line; rule cannot trigger. Verified locally: `pytest tests/security -v` -> 34 passed in 2.70s. * security(studio): defensive .npmrc cooldown aliases + save-exact Two additions to studio/frontend/.npmrc to harden the existing `min-release-age=7` (Mini Shai-Hulud defence): 1. `minimum-release-age=10080` (minutes) -- defensive alias for the same 7-day floor. Some npm versions / wrappers consult one key but not the other; setting both prevents a single upstream setting-name parse change from silently disabling the cooldown. The two keys MUST agree (do not let them drift). 2. `save-exact=true` -- refuses to write back `^x.y.z` ranges into package.json when a maintainer runs `npm install <pkg>` locally. Does NOT rewrite already-present ranges; stops NEW carets from creeping into the manifest as patch-version footguns. Verified: pytest tests/security -> 34 passed in 2.63s. * chore(dependabot): remove dead bun entry for /studio/frontend `package-ecosystem: "bun"` at /studio/frontend was a no-op: that path commits package-lock.json, not bun.lock / bun.lockb, so Dependabot's bun ecosystem silently skipped it. The actual behaviour is unchanged -- the npm entry below the cargo block already owns npm_and_yarn security advisories for /studio/frontend with `open-pull-requests-limit: 0` (version-update PRs suppressed, security PRs flow through). This commit: - Deletes the bun entry (kept a placeholder comment so a future bun migration knows where to slot it back in). - Rewrites the npm /studio/frontend entry comment to explain the real intent: lockfile is the authoritative pin, .npmrc `min-release-age=7` already blocks fresh tarballs at install time, dependabot only needs to surface security advisories. No functional change: same set of dependabot PRs as before (zero version updates, security advisories grouped weekly with cooldown). Verified: pytest tests/security -> 34 passed in 2.67s; YAML parses cleanly via PyYAML. * fix(dependabot): drop unsupported semver-* cooldown keys on github-actions Dependabot's validator rejected the config with: The property '#/updates/0/cooldown/semver-minor-days' is not supported for the package ecosystem 'github-actions'. The property '#/updates/0/cooldown/semver-patch-days' is not supported for the package ecosystem 'github-actions'. The `semver-minor-days` / `semver-patch-days` cooldown knobs are only valid for semver-aware ecosystems (npm, cargo, etc.). The github-actions ecosystem pins via git tags / SHAs, not semver, so only `default-days` is honored. Pre-existing bug on main; surfaced on this PR because the prior commit re-validated the file. Behaviour: github-actions PRs now respect the 7-day cooldown floor (was already the intent), without the no-op semver bands. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
429 lines
19 KiB
YAML
429 lines
19 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
#
|
|
# Cross-repo notebook validator. Lives in unslothai/unsloth (this repo)
|
|
# and inspects every notebook in unslothai/notebooks at HEAD (or the
|
|
# ref dispatched in via repository_dispatch).
|
|
#
|
|
# Catches the bug classes that landed in:
|
|
# - unslothai/notebooks#258 Colab torchao 0.10 vs peft 0.19 floor
|
|
# - unslothai/notebooks#260 DONT_UPDATE_EXCEPTIONS coverage drift
|
|
# - unslothai/notebooks#261 torch/torchcodec ABI; --no-deps tokenizers
|
|
# - unslothai/notebooks#264 --no-deps transformers + Colab tokenizers drift
|
|
# - unslothai/notebooks#221 git+ HEAD installs in install cells
|
|
# - unslothai/notebooks commit 51b1462 template/notebook drift
|
|
#
|
|
# CPU-only by design. Layer 2 (api-introspect) reuses the existing
|
|
# tests/_zoo_aggressive_cuda_spoof.py harness so `import unsloth`
|
|
# succeeds on a GPU-less ubuntu-latest runner.
|
|
|
|
name: Notebooks CI
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'unsloth/**'
|
|
- 'scripts/notebook_validator.py'
|
|
- 'scripts/notebook_to_python.py'
|
|
- 'scripts/data/colab_pip_freeze.gpu.txt'
|
|
- 'scripts/data/colab_to_cpu_pin.json'
|
|
- 'tests/notebooks/**'
|
|
- 'tests/_zoo_aggressive_cuda_spoof.py'
|
|
- '.github/workflows/notebooks-ci.yml'
|
|
schedule:
|
|
# Daily 06:17 UTC. Catches Colab preinstall bumps (the upstream image
|
|
# is rebuilt roughly weekly) without us waiting on a PR. Off the
|
|
# :00/:30 fleet-collision spots.
|
|
- cron: '17 6 * * *'
|
|
workflow_dispatch:
|
|
inputs:
|
|
notebooks_ref:
|
|
description: 'unslothai/notebooks ref to lint (branch / SHA / tag)'
|
|
default: 'main'
|
|
include_smoke:
|
|
description: 'Also run the install-cell smoke matrix (longer)'
|
|
type: boolean
|
|
default: false
|
|
repository_dispatch:
|
|
# Fired by a tiny companion workflow on unslothai/notebooks.
|
|
types: [notebooks_pr_opened, notebooks_main_pushed]
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
NOTEBOOKS_REF: >-
|
|
${{ github.event.inputs.notebooks_ref ||
|
|
github.event.client_payload.ref ||
|
|
'main' }}
|
|
|
|
jobs:
|
|
static:
|
|
name: static (drift + lint + exceptions)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
# Validate the dispatched ref before it reaches actions/checkout's `ref:`
|
|
# input. Reading via env (NOT direct ${{ ... }} interpolation in the
|
|
# regex test) closes the GitHub-Actions-injection class where a
|
|
# client_payload.ref like `main"; rm -rf / #` would be embedded into the
|
|
# shell command. NOTEBOOKS_REF defaults to 'main' on non-dispatch
|
|
# events, but only repository_dispatch can supply attacker-controlled
|
|
# values, so we gate this check on that event type.
|
|
- name: Validate client_payload.ref shape
|
|
if: github.event_name == 'repository_dispatch'
|
|
env:
|
|
NOTEBOOKS_REF: ${{ github.event.client_payload.ref }}
|
|
run: |
|
|
if ! printf '%s' "$NOTEBOOKS_REF" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
echo "::error::client_payload.ref contains disallowed characters" >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Checkout unsloth (this PR)
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
path: unsloth
|
|
|
|
- name: Checkout unslothai/notebooks @ ${{ env.NOTEBOOKS_REF }}
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
repository: unslothai/notebooks
|
|
ref: ${{ env.NOTEBOOKS_REF }}
|
|
path: notebooks
|
|
fetch-depth: 0 # drift check needs git status / diff
|
|
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.12'
|
|
cache: 'pip'
|
|
|
|
- name: Install validator deps
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# nbformat + nbconvert come from the converter's requirements;
|
|
# spellchecker + huggingface_hub are imported at module top of
|
|
# update_all_notebooks.py.
|
|
pip install \
|
|
'nbformat>=5.10' 'nbconvert>=7.16' 'pyspellchecker>=0.8' \
|
|
'huggingface_hub>=0.34' 'tqdm>=4.66'
|
|
|
|
- name: Refresh Colab pip-freeze (best-effort; falls back to snapshot)
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py refresh-colab \
|
|
--out unsloth/scripts/data/colab_pip_freeze.gpu.txt \
|
|
|| echo "::warning::refresh-colab failed; using committed snapshot"
|
|
|
|
- name: Diff Colab oracle vs committed snapshots (advisory)
|
|
# Pulls pip-freeze.gpu.txt + apt-list-gpu.txt + os-info-gpu.txt
|
|
# from googlecolab/backend-info and prints NEW / REMOVED /
|
|
# CHANGED entries against scripts/data/colab_*.txt. Non-blocking
|
|
# on PRs; the daily cron job below runs the same step with
|
|
# --strict so upstream rotations surface within ~24h.
|
|
continue-on-error: true
|
|
working-directory: ${{ github.workspace }}
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py colab-diff \
|
|
--snapshot-dir unsloth/scripts/data
|
|
|
|
- name: Drift check (re-run update_all_notebooks.py + git diff)
|
|
working-directory: ${{ github.workspace }}
|
|
# Reported as non-blocking until the upstream `unslothai/notebooks`
|
|
# tree is regenerated. The first run on @main surfaces ~463 files
|
|
# of drift (7359 / 9634 line delta), which is a real backlog the
|
|
# notebooks-side maintainers need to clear in their own repo --
|
|
# this PR's role is to surface the count, not auto-fix it.
|
|
continue-on-error: true
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py drift \
|
|
--notebooks-dir notebooks
|
|
|
|
- name: Convert sanity (every nb / kaggle / original_template -> .py)
|
|
# Same rationale as Drift: a handful of upstream notebooks fail
|
|
# the converter (custom magics, malformed JSON, etc). Surface
|
|
# the count without blocking; the team triages in unslothai/notebooks.
|
|
continue-on-error: true
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py convert \
|
|
--notebooks-dir notebooks \
|
|
--out _converted
|
|
|
|
- name: Lint (install cells + AST scan, env-scoped)
|
|
# Reported as non-blocking (continue-on-error: true) until the
|
|
# backlog of pre-existing findings on unslothai/notebooks@main is
|
|
# cleared. Same pattern PR #5298 used for biome:check on the
|
|
# frontend. As of this commit the live tree surfaces 27 errors +
|
|
# 6 warnings, all real (peft/torchao floor missing in 6 nb/
|
|
# notebooks, 14 git+ HEAD installs in hand-tuned exception
|
|
# notebooks, 6 torch/torchcodec ABI mismatches, 1
|
|
# transformers/tokenizers --no-deps drift). The count surfaces
|
|
# in the PR check UI. Drop continue-on-error once it hits zero.
|
|
continue-on-error: true
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py lint \
|
|
--notebooks-dir notebooks \
|
|
--colab-pin unsloth/scripts/data/colab_pip_freeze.gpu.txt \
|
|
--no-pypi
|
|
# --no-pypi skips R-INST-002 (transitive resolve via PyPI metadata).
|
|
# Layer 1 keeps PR-time wall-clock predictable; the daily cron run
|
|
# below drops --no-pypi and refreshes the cache.
|
|
|
|
- name: DONT_UPDATE_EXCEPTIONS coverage
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py exceptions \
|
|
--notebooks-dir notebooks
|
|
|
|
static-with-pypi:
|
|
name: static + transitive resolve (cron / dispatch only)
|
|
if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
# See `static.Validate client_payload.ref shape` for rationale. This
|
|
# job's `if:` excludes repository_dispatch today, so the validation
|
|
# step is a defence-in-depth no-op until that gate ever relaxes.
|
|
- name: Validate client_payload.ref shape
|
|
if: github.event_name == 'repository_dispatch'
|
|
env:
|
|
NOTEBOOKS_REF: ${{ github.event.client_payload.ref }}
|
|
run: |
|
|
if ! printf '%s' "$NOTEBOOKS_REF" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
echo "::error::client_payload.ref contains disallowed characters" >&2
|
|
exit 1
|
|
fi
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with: { path: unsloth }
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
repository: unslothai/notebooks
|
|
ref: ${{ env.NOTEBOOKS_REF }}
|
|
path: notebooks
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with: { python-version: '3.12', cache: 'pip' }
|
|
- name: Install
|
|
run: pip install -U pip
|
|
- name: Refresh Colab oracle
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py refresh-colab \
|
|
--out unsloth/scripts/data/colab_pip_freeze.gpu.txt
|
|
- name: Diff Colab oracle vs committed snapshots (--strict on cron)
|
|
# Cron-only escalation of the advisory PR-time check. Fails if
|
|
# any of pip-freeze.gpu.txt / apt-list-gpu.txt / os-info-gpu.txt
|
|
# has drifted from scripts/data/colab_*.txt; refresh the
|
|
# snapshots in this repo to acknowledge.
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py colab-diff \
|
|
--snapshot-dir unsloth/scripts/data --strict
|
|
- name: Lint with live PyPI metadata
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py lint \
|
|
--notebooks-dir notebooks \
|
|
--colab-pin unsloth/scripts/data/colab_pip_freeze.gpu.txt
|
|
|
|
api-introspect:
|
|
name: api surface (under CUDA spoof)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 12
|
|
steps:
|
|
- name: Validate client_payload.ref shape
|
|
if: github.event_name == 'repository_dispatch'
|
|
env:
|
|
NOTEBOOKS_REF: ${{ github.event.client_payload.ref }}
|
|
run: |
|
|
if ! printf '%s' "$NOTEBOOKS_REF" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
echo "::error::client_payload.ref contains disallowed characters" >&2
|
|
exit 1
|
|
fi
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with: { path: unsloth }
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
repository: unslothai/notebooks
|
|
ref: ${{ env.NOTEBOOKS_REF }}
|
|
path: notebooks
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with: { python-version: '3.12', cache: 'pip' }
|
|
|
|
- name: Install CPU torch + pinned unsloth + trl + converter deps
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# CPU torch + torchvision. torchvision is required because
|
|
# unsloth_zoo.vision_utils imports PIL at module top, and the
|
|
# easiest way to get a torch-compatible PIL on a CPU runner is
|
|
# to let torchvision pull the right Pillow version.
|
|
pip install --index-url https://download.pytorch.org/whl/cpu \
|
|
'torch>=2.8,<2.11' 'torchvision<0.26'
|
|
# Pin to the same versions update_all_notebooks.py installs in
|
|
# generated notebooks. Keep these in lockstep with PIN_TRL /
|
|
# PIN_TRANSFORMERS in unslothai/notebooks/update_all_notebooks.py.
|
|
# `triton` is added because unsloth/_gpu_init.py:232 does an
|
|
# unconditional `import triton`; the PyPI wheel installs cleanly
|
|
# on Linux x86_64 even without CUDA (same rationale as
|
|
# consolidated-tests-ci.yml line 192-205).
|
|
# Pillow is listed explicitly as a defensive belt-and-braces
|
|
# next to torchvision (vision_utils crashes ModuleNotFoundError
|
|
# if torchvision skipped its Pillow dep for any reason).
|
|
pip install 'transformers>=4.56,<5.6' 'trl>=0.22,<0.26' 'accelerate>=1.0' \
|
|
'datasets>=3.4,<5' 'peft>=0.15,<0.20' \
|
|
'bitsandbytes>=0.43' 'sentencepiece' 'protobuf' triton \
|
|
Pillow safetensors tqdm packaging psutil
|
|
# Converter deps (nbformat for notebook_to_python.py).
|
|
pip install 'nbformat>=5.10' 'nbconvert>=7.16'
|
|
# Install unsloth from the LOCAL checkout (the PR head), not PyPI.
|
|
# The PR-time CI must validate the code in this PR; PyPI unsloth
|
|
# may lag the in-repo CPU-torch fallback in unsloth/kernels/utils.py
|
|
# (lines 162-170) that handles missing torch._C._cuda_getCurrentRawStream.
|
|
pip install --no-deps unsloth_zoo
|
|
pip install --no-deps -e ./unsloth
|
|
|
|
- name: Convert notebooks for AST scan
|
|
# Same upstream-conversion-error tolerance as the static job.
|
|
continue-on-error: true
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py convert \
|
|
--notebooks-dir notebooks --out _converted
|
|
|
|
- name: Dump unsloth + trl API surface (under CUDA spoof)
|
|
run: |
|
|
PYTHONPATH=unsloth/tests python -u - <<'PY'
|
|
import sys, json, inspect
|
|
import _zoo_aggressive_cuda_spoof as _spoof
|
|
_spoof.apply()
|
|
import unsloth
|
|
import trl
|
|
surface = {}
|
|
for cls_name in ("FastLanguageModel", "FastVisionModel", "FastModel"):
|
|
cls = getattr(unsloth, cls_name, None)
|
|
if cls is None:
|
|
continue
|
|
surface[cls_name] = sorted(n for n in dir(cls) if not n.startswith("_"))
|
|
surface["SFTConfig_kwargs"] = sorted(inspect.signature(trl.SFTConfig.__init__).parameters)
|
|
json.dump(surface, open("_api_surface.json", "w"), indent=2)
|
|
print("dumped surface for:", list(surface))
|
|
PY
|
|
|
|
- name: Run API rule against converted notebooks
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py api \
|
|
--converted-dir _converted \
|
|
--surface _api_surface.json
|
|
|
|
smoke-install:
|
|
name: smoke install (Colab-shaped venv, opt-in)
|
|
if: ${{ github.event.inputs.include_smoke == 'true' || github.event_name == 'schedule' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 25
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
# One representative notebook per installation_*_content template.
|
|
# Add rows when a new install template lands in update_all_notebooks.py.
|
|
notebook:
|
|
- 'nb/Llama3.1_(8B)-Alpaca.ipynb' # installation_content
|
|
- 'nb/Gemma3_(4B)-Vision.ipynb' # installation_content + vision
|
|
- 'nb/Llama3.1_(8B)-GRPO.ipynb' # installation_extra_grpo_content
|
|
- 'nb/gpt-oss-(20B)-Fine-tuning.ipynb' # installation_gpt_oss_content
|
|
- 'nb/Qwen3_5_(4B)_Vision.ipynb' # installation_qwen3_5_content
|
|
- 'nb/Nemotron-3-Nano-30B-A3B_A100.ipynb' # installation_nemotron_nano_content
|
|
- 'nb/Whisper.ipynb' # installation_whisper_content
|
|
- 'nb/Synthetic_Data_Hackathon.ipynb' # installation_synthetic_data_content
|
|
steps:
|
|
- name: Validate client_payload.ref shape
|
|
if: github.event_name == 'repository_dispatch'
|
|
env:
|
|
NOTEBOOKS_REF: ${{ github.event.client_payload.ref }}
|
|
run: |
|
|
if ! printf '%s' "$NOTEBOOKS_REF" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
echo "::error::client_payload.ref contains disallowed characters" >&2
|
|
exit 1
|
|
fi
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with: { path: unsloth }
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
repository: unslothai/notebooks
|
|
ref: ${{ env.NOTEBOOKS_REF }}
|
|
path: notebooks
|
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with: { python-version: '3.12' }
|
|
|
|
- name: Seed Colab-shaped venv from pip-freeze (CPU-mapped)
|
|
run: |
|
|
# Strip cu128 local versions, route torch/torchvision to the CPU
|
|
# wheel index, drop CUDA-specific deps the runner can't use.
|
|
python -u - <<'PY' > /tmp/seed_pins.txt
|
|
import json, re
|
|
mapping = json.load(open("unsloth/scripts/data/colab_to_cpu_pin.json"))
|
|
rewrite = mapping["rewrite"]
|
|
skip = set(mapping["skip"])
|
|
spoof = set(mapping["module_spoof"])
|
|
out = []
|
|
for line in open("unsloth/scripts/data/colab_pip_freeze.gpu.txt"):
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
m = re.match(r"^([A-Za-z0-9._-]+)\s*==\s*(.+)$", line)
|
|
if not m:
|
|
continue
|
|
name, ver = m.group(1).lower(), m.group(2)
|
|
if name in skip:
|
|
continue
|
|
if name in spoof:
|
|
continue
|
|
if name in rewrite:
|
|
ver = re.sub(r"[+\-].+$", "", ver)
|
|
out.append(f"{name}=={ver}")
|
|
else:
|
|
ver = re.sub(r"[+\-].+$", "", ver)
|
|
out.append(f"{name}=={ver}")
|
|
print("\n".join(out))
|
|
PY
|
|
head -5 /tmp/seed_pins.txt
|
|
wc -l /tmp/seed_pins.txt
|
|
|
|
- name: Install Colab-shaped venv
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
# Best-effort: any single line that fails to resolve on CPU is
|
|
# tolerated; the smoke contract is "the install cell + the unsloth
|
|
# import works", not "the entire Colab venv reproduces."
|
|
while IFS= read -r spec; do
|
|
pip install "$spec" --index-url https://download.pytorch.org/whl/cpu \
|
|
--extra-index-url https://pypi.org/simple || \
|
|
echo "::warning::pin failed: $spec"
|
|
done < /tmp/seed_pins.txt
|
|
|
|
- name: Run install cell
|
|
run: |
|
|
python unsloth/scripts/notebook_validator.py convert \
|
|
--notebooks-dir notebooks --out _converted
|
|
# Take the converted .py and run the install cell only.
|
|
BASE="$(basename '${{ matrix.notebook }}' .ipynb | tr -d '()' | tr -c '[:alnum:]_' _)"
|
|
PY="_converted/${BASE}.py"
|
|
[ -f "$PY" ] || { echo "::error::$PY not found"; ls _converted | head; exit 1; }
|
|
# Truncate at the first `from unsloth import` so we run install +
|
|
# core imports only.
|
|
awk '/^from unsloth import/ { print "import sys; sys.exit(0)"; exit } { print }' "$PY" > _smoke.py
|
|
PYTHONPATH=unsloth/tests python -u - <<'PY'
|
|
import _zoo_aggressive_cuda_spoof as _s; _s.apply()
|
|
# Stub torchcodec for cells that import it — no CPU wheel exists.
|
|
import sys, types
|
|
if "torchcodec" not in sys.modules:
|
|
sys.modules["torchcodec"] = types.ModuleType("torchcodec")
|
|
exec(open("_smoke.py").read(), {"__name__": "__main__"})
|
|
PY
|
|
|
|
- name: Verify imports under spoof
|
|
run: |
|
|
PYTHONPATH=unsloth/tests python -u - <<'PY'
|
|
import sys, types
|
|
if "torchcodec" not in sys.modules:
|
|
sys.modules["torchcodec"] = types.ModuleType("torchcodec")
|
|
import _zoo_aggressive_cuda_spoof as _s; _s.apply()
|
|
import unsloth, peft, torch, torchao, transformers, tokenizers
|
|
print("OK: imports pass under CUDA spoof")
|
|
PY
|