#!/usr/bin/env bash # # pre_commit_lint.sh — shift CI lint left. Run it (via `make pre-commit`) right # before `git commit`; it inspects your staged files and runs only the matching # gating CI checks, so a clean run means a green CI lint: # - litellm/ Python staged -> `make lint` (test-linting.yml's lint job) # - dashboard staged -> prettier + eslint + lint budgets (test-litellm-ui-build.yml's frontend-lint) # - proxy/types staged -> regenerate dashboard API types and fail on drift (check-ui-api-types.yml) # # Each block is skipped when no matching files are staged, so unrelated commits stay # fast. This is intentionally not auto-installed as a git hook (see scripts/install_git_hooks.sh): # the dashboard and basedpyright passes can take minutes, so it's run on demand rather # than firing on every human commit. It is hook-compatible if you want that anyway: # `ln -s ../../scripts/pre_commit_lint.sh .git/hooks/pre-commit`. set -eu repo_root=$(git rev-parse --show-toplevel) cd "$repo_root" staged=$(git diff --cached --name-only --diff-filter=ACMR) staged_match() { printf '%s\n' "$staged" | grep -E "$1" || true; } # CI's lint job (test-linting.yml) only inspects litellm/, so a tests-only or # scripts-only commit can't turn it red; scope the trigger there to skip the slow # make lint when it couldn't catch anything. litellm_py_files=$(staged_match '^litellm/.*\.py$') # ruff format (and CI's format step) skip enterprise; the rest of make lint covers it. fmt_files=$(printf '%s\n' "$litellm_py_files" | grep -v '^litellm/enterprise/' || true) # check-ui-api-types.yml triggers on any file under litellm/proxy or litellm/types # (Prisma schema and configs included, not just Python) plus the generator and its # lockfiles, so match that whole trigger set rather than a Python subset. spec_files=$(staged_match '^(litellm/(proxy|types)/.*|ui/litellm-dashboard/(scripts/gen-api-types\.mjs|package\.json|package-lock\.json|src/lib/http/schema\.d\.ts))$') # CI's frontend-lint runs prettier over a wider extension set than eslint; keep that # split so this flags exactly what the job would. ui_prettier_files=$(staged_match '^ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs|json|css|scss|md|mdx|yml|yaml|html)$') ui_eslint_files=$(staged_match '^ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs)$') lint_dashboard() { ( rc=0 prettier_rel=() eslint_rel=() while IFS= read -r f; do [ -n "$f" ] && prettier_rel+=("${f#ui/litellm-dashboard/}") done <&2; status=1; } # `make lint` format-checks files in origin/base...HEAD, which at pre-commit time # predates the staged change, so format-check the staged litellm files directly to # cover a brand-new commit before it lands. if [ -n "$fmt_files" ]; then echo "pre-commit: ruff format --check (staged litellm files)" printf '%s\n' "$fmt_files" | xargs uv run --no-sync ruff format --check --exclude '/enterprise/' \ || { echo "✗ Unformatted staged files. Fix with: make format, then re-stage." >&2; status=1; } fi fi if [ -n "$ui_prettier_files" ] || [ -n "$ui_eslint_files" ]; then echo "pre-commit: linting dashboard (prettier + eslint + lint budgets)" lint_dashboard || { echo "✗ Dashboard lint failed. See above; format with: (cd ui/litellm-dashboard && npm run format)." >&2; status=1; } fi if [ -n "$spec_files" ]; then echo "pre-commit: checking dashboard API types are in sync (npm run gen:api)" # gen-api-types.mjs imports litellm.proxy.proxy_server, which needs the proxy deps # and an up-to-date Prisma client; check-ui-api-types.yml installs those and runs # prisma generate before gen:api, so mirror that here or a stale client can mask # drift that CI will still flag. if ! uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma; then echo "✗ Could not regenerate Prisma client (prisma generate failed)." >&2 status=1 elif ( cd ui/litellm-dashboard && LITELLM_PYTHON="uv run --no-sync python" npm run gen:api ); then if ! git diff --quiet -- ui/litellm-dashboard/src/lib/http/schema.d.ts; then echo "✗ Dashboard API types are stale; regenerated src/lib/http/schema.d.ts. Stage it and re-run make pre-commit." >&2 status=1 fi else echo "✗ Could not regenerate API types (npm run gen:api failed)." >&2 status=1 fi fi exit $status