mirror of
https://github.com/unslothai/unsloth.git
synced 2026-07-09 15:58:41 +00:00
Bumps the actions group with 11 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/checkout](https://github.com/actions/checkout) | `4.2.2` | `7.0.0` | | [actions/setup-python](https://github.com/actions/setup-python) | `6.2.0` | `6.3.0` | | [actions/cache/restore](https://github.com/actions/cache) | `5.0.5` | `6.1.0` | | [actions/cache/save](https://github.com/actions/cache) | `5.0.5` | `6.1.0` | | [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.1` | `7.0.1` | | [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.19.1` | `2.19.4` | | [ossf/scorecard-action](https://github.com/ossf/scorecard-action) | `2.4.1` | `2.4.3` | | [github/codeql-action](https://github.com/github/codeql-action) | `3` | `4.36.3` | | [tauri-apps/tauri-action](https://github.com/tauri-apps/tauri-action) | `0.6.2` | `1.0.0` | | [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog) | `3.95.3` | `3.95.8` | | [actions/stale](https://github.com/actions/stale) | `10.2.0` | `10.3.0` | Updates `actions/checkout` from 4.2.2 to 7.0.0 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.2.2...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) Updates `actions/setup-python` from 6.2.0 to 6.3.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v6.2.0...ece7cb06caefa5fff74198d8649806c4678c61a1) Updates `actions/cache/restore` from 5.0.5 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](27d5ce7f10...55cc834586) Updates `actions/cache/save` from 5.0.5 to 6.1.0 - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](27d5ce7f10...55cc834586) Updates `actions/upload-artifact` from 4.6.1 to 7.0.1 - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v4.6.1...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a) Updates `step-security/harden-runner` from 2.19.1 to 2.19.4 - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](a5ad31d6a1...9af89fc715) Updates `ossf/scorecard-action` from 2.4.1 to 2.4.3 - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](f49aabe0b5...4eaacf0543) Updates `github/codeql-action` from 3 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v3...v4.36.3) Updates `tauri-apps/tauri-action` from 0.6.2 to 1.0.0 - [Release notes](https://github.com/tauri-apps/tauri-action/releases) - [Changelog](https://github.com/tauri-apps/tauri-action/blob/dev/CHANGELOG.md) - [Commits](84b9d35b5f...1deb371b0c) Updates `trufflesecurity/trufflehog` from 3.95.3 to 3.95.8 - [Release notes](https://github.com/trufflesecurity/trufflehog/releases) - [Commits](37b77001d0...00155c9dc5) Updates `actions/stale` from 10.2.0 to 10.3.0 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](b5d41d4e1d...eb5cf3af3a) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/setup-python dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: actions/cache/restore dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/cache/save dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: actions/upload-artifact dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: step-security/harden-runner dependency-version: 2.19.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: ossf/scorecard-action dependency-version: 2.4.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: github/codeql-action dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: tauri-apps/tauri-action dependency-version: 1.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: trufflesecurity/trufflehog dependency-version: 3.95.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/stale dependency-version: 10.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com>
379 lines
17 KiB
YAML
379 lines
17 KiB
YAML
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
# Whole-repo, multi-language source-lint gate. Runs on every PR
|
|
# (no path filter) because each step is sub-second to a few seconds
|
|
# and together they catch a class of breakage the focused build
|
|
# workflows would miss:
|
|
#
|
|
# - Python syntax + ruff + leftover debugger calls (across 350+
|
|
# committed .py files, not just studio/backend).
|
|
# - Shell `bash -n` parse for every committed *.sh.
|
|
# - `yaml.safe_load` and `json.loads` round-trip for every
|
|
# committed YAML / JSON config.
|
|
#
|
|
# TypeScript and Rust are NOT duplicated here on purpose:
|
|
# - Studio Frontend CI runs `npm run typecheck` (= `tsc --noEmit`)
|
|
# and `npm run build` (vite/swc) on every studio/frontend/**
|
|
# change, which is a full TS AST + type check.
|
|
# - Studio Tauri CI runs `tauri build --debug --no-bundle` on
|
|
# every studio/src-tauri/** or studio/frontend/** change, which
|
|
# compiles the Rust crate (= cargo check + cargo build).
|
|
# Each is a stricter check than a parse-only step would be, so a
|
|
# fast-fail duplicate here would only burn cache; the dedicated
|
|
# workflows already block merges on Rust / TS regressions.
|
|
|
|
name: Lint CI
|
|
|
|
on:
|
|
pull_request:
|
|
push:
|
|
branches: [main, pip]
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
source-lint:
|
|
name: Source lint (Python + shell + YAML + JSON + safety nets)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
with:
|
|
python-version: '3.12'
|
|
cache: 'pip'
|
|
|
|
# Pin ruff to match .pre-commit-config.yaml so a CI-only ruff
|
|
# bump cannot disagree with what pre-commit accepted.
|
|
# codespell is pinned for the same reason: a reviewer should
|
|
# never see a typo report appear and disappear depending on
|
|
# which codespell version the runner happened to install.
|
|
- run: pip install 'ruff==0.15.12' 'pyyaml>=6' 'codespell>=2.3,<3'
|
|
|
|
- name: Linux deps for shellcheck
|
|
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends shellcheck
|
|
|
|
- name: Python AST/syntax check (every committed .py must compile)
|
|
# python -m compileall uses the same parser the interpreter
|
|
# uses, so anything broken here would also crash at
|
|
# `import X` on a user's machine. Sub-second across 350+
|
|
# files. Hard gate.
|
|
run: |
|
|
python -m compileall -q -j 0 \
|
|
unsloth unsloth_cli studio tests cli.py unsloth-cli.py
|
|
|
|
- name: Python ruff check (whole repo)
|
|
# The narrow rule set in pyproject.toml [tool.ruff.lint]
|
|
# selects E9 / F63 / F7 / F82 -- syntax errors, broken
|
|
# comparisons, undefined names. The whole repo passes today,
|
|
# so this is a hard gate.
|
|
run: |
|
|
ruff check unsloth unsloth_cli studio tests cli.py unsloth-cli.py
|
|
|
|
- name: Import-hoist verifier self-test
|
|
# scripts/verify_import_hoist.py is a scope-aware (LEGB) AST
|
|
# resolver that gates import-hoisting / alias-rename refactors
|
|
# against two bugs ruff and pyflakes both miss:
|
|
# 1. dangling alias -- `from a import b as _b` hoisted to
|
|
# `from a import b` but a leftover `_b` reference now
|
|
# resolves to nothing (or to some other module-level `_b`).
|
|
# 2. rename clash -- `_b -> b` silently re-points at a
|
|
# different object already named `b` in that scope.
|
|
# This step runs the tool's 8 negative-control cases so a
|
|
# regression in the verifier itself fails before we trust it on
|
|
# a diff. Hermetic, stdlib-only, sub-second. Hard gate.
|
|
run: |
|
|
python scripts/verify_import_hoist.py --self-test
|
|
|
|
- name: Import-hoist / alias-rename safety (changed Python files)
|
|
# Runs the verifier in compare mode on every in-place-modified
|
|
# .py in the PR: parses each file BEFORE (base branch) and AFTER
|
|
# (this diff), resolves every name load, and fails on a BLOCKER
|
|
# (dangling alias / rename clash / re-pointed import). INFO
|
|
# findings (a helper relocated to another file) do not fail.
|
|
#
|
|
# --diff-filter=M (in-place edits only) is deliberate: that is
|
|
# exactly where a hoist refactor lives, and it skips brand-new
|
|
# files whose re-export imports would otherwise look "unused".
|
|
#
|
|
# Diff against the true merge-base, not the base tip. A two-dot
|
|
# diff against the tip re-lints every file the base branch
|
|
# changed after the PR branched, comparing newer base code
|
|
# (BEFORE) against the PR's older snapshot (AFTER) - a
|
|
# time-reversed comparison that flags the base branch's own
|
|
# refactors as blockers on PRs that never touched those files.
|
|
# The compare API returns the merge-base without needing local
|
|
# history, and fetching that single commit by SHA keeps the
|
|
# shallow (fetch-depth: 1) clone.
|
|
if: github.event_name == 'pull_request'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
MERGE_BASE=$(gh api \
|
|
"repos/${{ github.repository }}/compare/${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" \
|
|
--jq .merge_base_commit.sha)
|
|
git fetch --no-tags --depth=1 origin "$MERGE_BASE"
|
|
mapfile -t CHANGED < <(
|
|
git diff --name-only --diff-filter=M \
|
|
"$MERGE_BASE" HEAD -- '*.py' \
|
|
| grep -vE '(^|/)(unsloth_compiled_cache|node_modules|build|dist)/' || true
|
|
)
|
|
if [ "${#CHANGED[@]}" -eq 0 ]; then
|
|
echo "no in-place-modified Python files to check"
|
|
exit 0
|
|
fi
|
|
printf 'merge base: %s\n' "$MERGE_BASE"
|
|
printf 'checking %d file(s):\n' "${#CHANGED[@]}"
|
|
printf ' %s\n' "${CHANGED[@]}"
|
|
python scripts/verify_import_hoist.py \
|
|
--before "$MERGE_BASE" --after HEAD "${CHANGED[@]}"
|
|
|
|
- name: No leftover debugger / pdb / breakpoint calls
|
|
# Catches the "I'll just stick a breakpoint() here" mistake
|
|
# before it ships. AST-based so commented-out debugger
|
|
# markers don't false-positive (a bare grep would; there
|
|
# are three commented `# breakpoint()` markers in
|
|
# unsloth/models/rl* today). Sub-second.
|
|
run: |
|
|
python <<'PY'
|
|
import ast, pathlib, sys
|
|
|
|
SKIP_PARTS = {".venv", "venv", "build", "dist", ".git",
|
|
"unsloth_compiled_cache", "node_modules",
|
|
"unsloth.egg-info"}
|
|
|
|
bad = []
|
|
scanned = 0
|
|
for path in sorted(pathlib.Path(".").rglob("*.py")):
|
|
if any(part in SKIP_PARTS for part in path.parts):
|
|
continue
|
|
scanned += 1
|
|
try:
|
|
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
|
|
except SyntaxError:
|
|
continue # compileall step above already failed this
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Call):
|
|
continue
|
|
fn = node.func
|
|
if isinstance(fn, ast.Name) and fn.id == "breakpoint":
|
|
bad.append((path, node.lineno, "breakpoint()"))
|
|
elif (isinstance(fn, ast.Attribute) and fn.attr == "set_trace"
|
|
and isinstance(fn.value, ast.Name)
|
|
and fn.value.id in {"pdb", "ipdb"}):
|
|
bad.append((path, node.lineno, f"{fn.value.id}.set_trace()"))
|
|
|
|
if bad:
|
|
for path, lineno, what in bad:
|
|
print(f"::error file={path},line={lineno}::leftover {what} -- remove before merging")
|
|
sys.exit(1)
|
|
print(f"no leftover debugger calls (scanned {scanned} files)")
|
|
PY
|
|
|
|
- name: License-header drift (informational; whole repo)
|
|
# Three header families are accepted across the repo:
|
|
# 1. SPDX one-liner: `# SPDX-License-Identifier: ...`
|
|
# Used across studio/ (AGPL-3.0-only) and a few new
|
|
# files elsewhere.
|
|
# 2. Apache-2.0 long form, marker phrase
|
|
# "Licensed under the Apache License". Used across
|
|
# unsloth/ and unsloth_cli/.
|
|
# 3. GNU long form, marker phrase "General Public License".
|
|
# That single substring covers GPL, LGPL ("GNU Lesser
|
|
# General Public License") and AGPL ("GNU Affero
|
|
# General Public License") preambles, all three of
|
|
# which appear in unsloth/kernels/* (LGPL/AGPL) without
|
|
# the SPDX line.
|
|
# Empty files (mainly empty __init__.py) are skipped.
|
|
# Surfaced as a warning; cleaning up the actual misses is a
|
|
# follow-up PR, not a CI fix.
|
|
continue-on-error: true
|
|
run: |
|
|
python <<'PY'
|
|
import pathlib
|
|
|
|
ACCEPTED = (
|
|
"SPDX-License-Identifier", # any SPDX line
|
|
"Licensed under the Apache License", # Apache-2.0 long form
|
|
"General Public License", # GPL / LGPL / AGPL long form
|
|
)
|
|
SKIP_PARTS = {".venv", "venv", "build", "dist", ".git",
|
|
"unsloth_compiled_cache", "node_modules",
|
|
"unsloth.egg-info"}
|
|
|
|
studio_missing = []
|
|
other_missing = []
|
|
for path in sorted(pathlib.Path(".").rglob("*.py")):
|
|
if any(part in SKIP_PARTS for part in path.parts):
|
|
continue
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
if not text.strip():
|
|
continue # empty __init__.py etc.
|
|
head = "\n".join(text.splitlines()[:25])
|
|
if any(marker in head for marker in ACCEPTED):
|
|
continue
|
|
if "studio" in path.parts:
|
|
studio_missing.append(path)
|
|
else:
|
|
other_missing.append(path)
|
|
|
|
total = len(studio_missing) + len(other_missing)
|
|
if total == 0:
|
|
print("every committed .py has a recognised license header")
|
|
else:
|
|
print(f"::warning::{total} Python files have no recognised license "
|
|
f"header (SPDX / Apache-2.0 / GNU long form): "
|
|
f"studio={len(studio_missing)}, other={len(other_missing)}")
|
|
for path in (studio_missing + other_missing)[:30]:
|
|
print(f" {path}")
|
|
if total > 30:
|
|
print(f" ... and {total - 30} more")
|
|
PY
|
|
|
|
- name: Shell scripts parse cleanly (`bash -n`)
|
|
# Same idea as Python's compileall: parse-only check that
|
|
# every committed *.sh would not blow up at `bash script.sh`
|
|
# invocation time on a release box. tests/sh/ is the largest
|
|
# cluster (the install.sh shape tests).
|
|
run: |
|
|
shopt -s globstar
|
|
fail=0
|
|
for f in $(git ls-files '*.sh'); do
|
|
if ! bash -n "$f"; then
|
|
echo "::error file=$f::shell parse error"
|
|
fail=1
|
|
fi
|
|
done
|
|
if [ "$fail" -ne 0 ]; then
|
|
exit 1
|
|
fi
|
|
n=$(git ls-files '*.sh' | wc -l)
|
|
echo "$n shell scripts parse cleanly"
|
|
|
|
- name: YAML files parse cleanly (yaml.safe_load)
|
|
# Catches truncated workflow files, broken indents in
|
|
# dependabot.yml / pre-commit configs, etc. Includes
|
|
# .github/workflows/*.yml so a typo in the file we just
|
|
# added shows up immediately.
|
|
run: |
|
|
python <<'PY'
|
|
import pathlib, sys, yaml
|
|
|
|
SKIP_PARTS = {".venv", "venv", "build", "dist", ".git",
|
|
"node_modules", "unsloth_compiled_cache",
|
|
"unsloth.egg-info"}
|
|
|
|
bad = []
|
|
scanned = 0
|
|
for path in sorted(list(pathlib.Path(".").rglob("*.yml"))
|
|
+ list(pathlib.Path(".").rglob("*.yaml"))):
|
|
if any(part in SKIP_PARTS for part in path.parts):
|
|
continue
|
|
scanned += 1
|
|
try:
|
|
with path.open("r", encoding="utf-8") as fh:
|
|
list(yaml.safe_load_all(fh))
|
|
except Exception as exc:
|
|
bad.append((path, exc))
|
|
|
|
if bad:
|
|
for path, exc in bad:
|
|
print(f"::error file={path}::YAML parse failed: {exc}")
|
|
sys.exit(1)
|
|
print(f"{scanned} YAML files parse cleanly")
|
|
PY
|
|
|
|
- name: JSON files parse cleanly (json.loads)
|
|
# Catches malformed package.json, biome.json, etc. Skips:
|
|
# - huge npm/bun lockfiles (machine-generated, slow to
|
|
# parse, no value).
|
|
# - tsconfig*.json: TypeScript convention is JSONC (JSON
|
|
# with `/* ... */` comments), which standard json.loads
|
|
# rejects. Strip-and-validate would need json5 or a
|
|
# hand-rolled comment scrubber for marginal value, since
|
|
# `tsc --noEmit` already validates these in Frontend CI.
|
|
run: |
|
|
python <<'PY'
|
|
import fnmatch, json, pathlib, sys
|
|
|
|
SKIP_PARTS = {".venv", "venv", "build", "dist", ".git",
|
|
"node_modules", "unsloth_compiled_cache",
|
|
"unsloth.egg-info"}
|
|
SKIP_NAMES = {"package-lock.json", "bun.lock"}
|
|
SKIP_PATTERNS = ("tsconfig*.json",)
|
|
|
|
bad = []
|
|
scanned = 0
|
|
for path in sorted(pathlib.Path(".").rglob("*.json")):
|
|
if any(part in SKIP_PARTS for part in path.parts):
|
|
continue
|
|
if path.name in SKIP_NAMES:
|
|
continue
|
|
if any(fnmatch.fnmatch(path.name, pat) for pat in SKIP_PATTERNS):
|
|
continue
|
|
scanned += 1
|
|
try:
|
|
json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception as exc:
|
|
bad.append((path, exc))
|
|
|
|
if bad:
|
|
for path, exc in bad:
|
|
print(f"::error file={path}::JSON parse failed: {exc}")
|
|
sys.exit(1)
|
|
print(f"{scanned} JSON files parse cleanly")
|
|
PY
|
|
|
|
- name: codespell typo check (informational)
|
|
# Catches typos in code, comments, and docs across the repo.
|
|
# Skips lockfiles, generated assets, binary artefacts, and
|
|
# the LICENSE files (US/UK spelling drift in legal text is
|
|
# not ours to second-guess). The ignore-words-list pulls
|
|
# out short identifiers + valid technical terms that
|
|
# codespell's default dictionary would otherwise flag
|
|
# (e.g. `ans` as a math-quiz variable name in
|
|
# tests/utils/aime_eval.py, `parm`/`parms` in PyTorch
|
|
# nn.Module idioms). Non-blocking until the surfaced typos
|
|
# are fixed; drop continue-on-error after the cleanup.
|
|
continue-on-error: true
|
|
run: |
|
|
codespell \
|
|
--skip='*.lock,*.lockb,*.json,*.svg,*.png,*.jpg,*.jpeg,*.gif,*.ico,*.woff*,*.ttf,*.eot,*.zip,*.gz,*.gguf,*.safetensors,*.bin,node_modules,.git,build,dist,unsloth_compiled_cache,unsloth.egg-info,target,studio/frontend/dist,*.pyc,*-licenses.txt,LICENSE*' \
|
|
--ignore-words-list='ans,bu,hel,fo,te,ot,hist,ned,sav,recurser,datas,nin,parm,parms,checkin,nd,fr,inout,donot,uint' \
|
|
--quiet-level=2
|
|
|
|
- name: shellcheck on committed *.sh (informational)
|
|
# Goes beyond `bash -n` (which only parses): catches subtle
|
|
# shell bugs like unquoted variable expansions, useless
|
|
# `cat`, command substitutions inside `[[`, etc. The
|
|
# install/setup scripts are critical-path so the signal is
|
|
# worth surfacing. Non-blocking until install.sh's
|
|
# hand-rolled patterns get cleaned up; drop continue-on-error
|
|
# afterwards.
|
|
continue-on-error: true
|
|
run: |
|
|
# Exclude SC1090 ("source not followable") -- legitimate
|
|
# for installer scripts that source files at runtime
|
|
# paths shellcheck cannot resolve statically.
|
|
# SC2034 ("variable assigned but never used") fires on
|
|
# the export-only assignment idiom we use in install.sh.
|
|
shellcheck -e SC1090,SC2034 $(git ls-files '*.sh')
|
|
|
|
- name: ruff format drift (informational)
|
|
# The canonical formatter is scripts/run_ruff_format.py
|
|
# = ruff format + scripts/enforce_kwargs_spacing.py, so plain
|
|
# `ruff format --check` reports the kwarg-spacing diff as
|
|
# drift. Surface the count for visibility but keep
|
|
# non-blocking until the custom pipeline is wired in here.
|
|
continue-on-error: true
|
|
run: |
|
|
ruff format --check unsloth unsloth_cli studio tests cli.py unsloth-cli.py
|