From de7fd062abbca25aa0bc1ab13bd1a33759ef1b34 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 6 May 2026 21:21:59 +0000 Subject: [PATCH] CI: add codespell + shellcheck to Lint CI; add Security audit workflow Three Priority-1 follow-ups from the lint review. Lint CI gains two non-blocking gates that surface drift without blocking merges (the same shape as the existing format-drift step): - codespell: typo catcher across source / comments / docs. Skips lockfiles, generated assets, binary artefacts, LICENSE files. ignore-words-list pulls out short identifiers and PyTorch idioms (parm/parms, ans, hist, etc.) the default dictionary would flag. Local run finds 16 real typos to fix in a follow-up. - shellcheck: catches subtle shell bugs `bash -n` doesn't see -- unquoted expansions, useless cat, `[[ ]]` command substitution, etc. SC1090 + SC2034 muted because install/setup scripts legitimately source runtime paths and use export-only assignments. Critical-path coverage: install.sh, setup.sh, tests/sh/. Both pinned for reproducibility (codespell>=2.3,<3 in pip, shellcheck via apt-get). Both surface findings in PR annotations without failing the run; drop continue-on-error after the cleanup PRs land. New workflow: Security audit. Runs `pip-audit` against the same dep set Studio's backend pytest matrix installs, so we audit what the runtime actually loads (not what pyproject.toml's transitive resolution might pull in differently). Triggers: - PRs touching requirements / pyproject.toml, - push to main / pip, - nightly @ 04:13 UTC (off-the-hour to dodge cron rush), - workflow_dispatch. The default branch already carries 17 known vulnerabilities per the dependabot banner, so a hard gate today would block every PR on a baseline we have not triaged. Non-blocking; full table goes to GITHUB_STEP_SUMMARY for grep-ability and a 30-day artefact for historical comparison. The custom AST anti-pattern scan I prototyped was dropped: every class of CPU-import-time bug we hit in this PR (bitsandbytes, torchvision, _cuda_getCurrentRawStream, DEVICE_COUNT==0 stream init) is already caught by the Repo tests (CPU) job exercising the actual import on a CPU torch wheel. Restating the rule in AST form would only add noise. --- .github/workflows/lint-ci.yml | 47 +++++++++++++- .github/workflows/security-audit.yml | 96 ++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/security-audit.yml diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 1e8fab2149..e1a4f1591f 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -50,9 +50,15 @@ jobs: python-version: '3.12' cache: 'pip' - # Pin to match .pre-commit-config.yaml so a CI-only ruff bump - # cannot disagree with what pre-commit accepted. - - run: pip install 'ruff==0.15.12' 'pyyaml>=6' + # 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 @@ -267,6 +273,41 @@ jobs: 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 diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000000..b0640396b0 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. + +# pip-audit on the Studio backend's resolved dep tree. Runs: +# - on PRs touching any requirements file or pyproject.toml, +# - nightly on a schedule against main, so newly-published CVEs +# surface even when no PR opens, +# - on workflow_dispatch for ad-hoc invocations. +# +# pip-audit is non-blocking initially. The default branch already +# carries 17 known vulnerabilities (per the dependabot banner on +# every push), so a hard gate today would block every PR on a +# baseline we haven't triaged. As the baseline closes, drop +# continue-on-error and the next PR that introduces a NEW vuln +# will be blocked instead. + +name: Security audit + +on: + pull_request: + paths: + - 'studio/backend/requirements/**' + - 'pyproject.toml' + - '.github/workflows/security-audit.yml' + push: + branches: [main, pip] + schedule: + # 04:13 UTC every day. Off-the-hour so we don't pile onto the + # GitHub-Actions cron rush at :00. + - cron: '13 4 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + pip-audit: + name: pip-audit (Studio backend deps) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: 'pip' + + - name: Install pip-audit + the dep set under test + # We resolve the same dep set Studio's backend pytest matrix + # installs, then pip-audit it. This audits what Studio + # actually loads at runtime, not what pyproject.toml's + # transitive resolution might pull in differently on a + # different host. + run: | + python -m pip install --upgrade pip 'pip-audit>=2.7' + pip install -r studio/backend/requirements/studio.txt + pip install \ + python-multipart aiofiles sqlalchemy cryptography \ + pyyaml jinja2 mammoth unpdf requests typer \ + 'numpy<3' pytest pytest-asyncio httpx + pip install --index-url https://download.pytorch.org/whl/cpu \ + 'torch>=2.4,<2.11' 'torchvision<0.26' + pip install 'transformers>=4.51,<5.5' + pip install 'bitsandbytes>=0.45' + pip install 'unsloth_zoo>=2026.5.1' + pip install -e . --no-deps + + - name: pip-audit (informational, drives baseline triage) + # `--strict`: fail on any finding (we'll use it once the + # baseline is clean). + # For now, run without --strict and let the runner surface + # the count + which packages are affected. Output goes to + # the workflow summary so it's grep-able. + continue-on-error: true + run: | + pip-audit --format=columns | tee logs-audit.txt + { + echo "## pip-audit results" + echo + echo "\`\`\`" + cat logs-audit.txt + echo "\`\`\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload audit log + if: always() + uses: actions/upload-artifact@v4 + with: + name: pip-audit-log + path: logs-audit.txt + retention-days: 30