mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-02 07:13:02 +00:00
* fix(ci): measure PR gates from the target branch's current tip The lint gate base, the ui lint and ui unit test scopes, and the spend logs guard compared the event's pull_request.base.sha with the head to find the merge base. GitHub can leave base.sha weeks behind the base branch, and when that old commit is an ancestor of a head that merged newer main, compare returns the old commit itself, so the strict gates blamed main's own code on the PR. Compare base.ref with the head instead, passed through env, so GitHub resolves the branch tip when the job runs. This is the same base make check uses locally. The ui unit test push guard now keys on BASE_REF, which is empty on push and workflow_dispatch. The regression test runs every workflow step that reads merge_base_commit against a fake gh backed by a temp repo with a stale base.sha, for main and a litellm_/ base with a slash. * fix(ci): check out the base branch's current guard code in the PR guards * fix(ci): read the PR's current base branch so re-runs survive a deleted base Every merge-base step now asks the pulls API for the PR's base branch instead of reading the event's base.ref, which a re-run keeps after a stacked base is merged and deleted. The guard checkouts go back to base.sha, which stays reachable. * test(ci): mark the step script path Final * fix(ci): keep the spend logs guard on main and run both guard scripts from the base branch tip * fix(ci): measure from the event's base branch unless it was deleted, and test what each step hands on A re-run after a retarget off main kept following the PR's current base, so lint, ui-unit and the cost map guard could pass without measuring against main. They now read the current base only when the event's base branch no longer exists. The tests run each merge base step whole and check the value its consumer gets * fix(ci): follow a renamed base branch to its new name before the compare --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
720 lines
30 KiB
YAML
720 lines
30 KiB
YAML
name: lint
|
|
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- "litellm_**"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
|
|
jobs:
|
|
python:
|
|
name: python
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
actions: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
services:
|
|
redis:
|
|
image: redis@sha256:e2debfb7956fa12c7ddc79d7e645c8cf26b30c99a6e9161ea9bf4171e1668a5f
|
|
ports:
|
|
- 6379:6379
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
fetch-depth: 1
|
|
clean: true
|
|
persist-credentials: false
|
|
|
|
- name: Detect relevant changes
|
|
id: changes
|
|
uses: ./.github/actions/detect-changes
|
|
|
|
- name: Fetch gate base (merge-base with target branch)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
retry() { "$@" || { sleep 15; "$@"; } || { sleep 30; "$@"; }; }
|
|
BASE_REF=$(gh api "repos/${GITHUB_REPOSITORY}/branches/${EVENT_BASE_REF}" --jq .name) || BASE_REF=$(retry gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .base.ref)
|
|
MERGE_BASE=$(retry gh api "repos/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${HEAD_SHA}?per_page=1" --jq '.merge_base_commit.sha')
|
|
test -n "$MERGE_BASE"
|
|
retry git fetch --no-tags --depth=1 origin "$MERGE_BASE"
|
|
echo "GATE_BASE_SHA=$MERGE_BASE" >> "$GITHUB_ENV"
|
|
|
|
- name: Set up Python
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Cache uv dependencies
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
with:
|
|
path: |
|
|
~/.cache/uv
|
|
.venv
|
|
key: ${{ runner.os }}-uv-lint-${{ hashFiles('uv.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-uv-lint-
|
|
|
|
- name: Clean Python cache
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
find . -type d -name "__pycache__" -exec rm -rf {} + || true
|
|
find . -name "*.pyc" -delete || true
|
|
|
|
- name: Check uv.lock is up to date
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv lock --check || (echo "❌ uv.lock is out of sync with pyproject.toml. Run 'uv lock' locally and commit the result." && exit 1)
|
|
|
|
- name: Cache the Rust build
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
uses: ./.github/actions/cache-cargo-build
|
|
|
|
- name: Install dependencies
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv sync --frozen --group proxy-dev --group e2e-dev
|
|
|
|
- name: Cache Prisma binaries
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
uses: ./.github/actions/cache-prisma-binaries
|
|
|
|
- name: Generate Prisma client
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
|
|
|
|
- name: Check ruff format
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
git diff --name-only --diff-filter=ACMR "$GATE_BASE_SHA" HEAD -- ':(glob)litellm/**/*.py' | grep -v '^litellm/enterprise/' > "$RUNNER_TEMP/ruff_format_files.txt" || true
|
|
if [ ! -s "$RUNNER_TEMP/ruff_format_files.txt" ]; then
|
|
echo "No changed litellm Python files to check with ruff format."
|
|
exit 0
|
|
fi
|
|
xargs uv run --no-sync ruff format --check --exclude '/enterprise/' < "$RUNNER_TEMP/ruff_format_files.txt"
|
|
|
|
- name: Debug - Check file state
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
echo "Current branch:"
|
|
git branch --show-current
|
|
echo "Last 3 commits:"
|
|
git log --oneline -3
|
|
echo "File content around line 43:"
|
|
head -50 litellm/litellm_core_utils/custom_logger_registry.py | tail -10
|
|
|
|
- name: Check MCP operation boundary
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: uv run --no-sync python scripts/check_mcp_operation_boundary.py
|
|
|
|
- name: Run Ruff linting
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
cd litellm
|
|
uv run --no-sync ruff check .
|
|
cd ..
|
|
|
|
- name: Run Ruff linting (test tree)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv run --no-sync ruff check --config ruff-tests.toml tests
|
|
|
|
- name: Check strict ruff rules (delta vs merge-base counts)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
uv run --no-sync python scripts/ruff_strict_gate.py --base "$GATE_BASE_SHA"
|
|
|
|
- name: Check type discipline (mutable collections / casts / type guards / kwargs / unexplained suppressions, delta vs merge-base counts)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
uv run --no-sync python scripts/type_discipline_gate.py --base "$GATE_BASE_SHA"
|
|
|
|
- name: Check test quality (zero-assert / mock-echo tests, sys.path.insert, raw env writes, litellm global mutation, credential-gated skips, conftest snapshot inventory, delta vs merge-base counts)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
uv run --no-sync python scripts/test_quality_gate.py --base "$GATE_BASE_SHA"
|
|
|
|
- name: Check OpenAPI docs (POST routes without a request schema, description or 200 schema, delta vs merge-base counts)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy
|
|
uv run --no-sync python scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA"
|
|
|
|
- name: Print OpenAI version
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv run --no-sync python -c "import openai; print(f'OpenAI version: {openai.__version__}')"
|
|
|
|
- name: Check basedpyright (delta vs merge-base counts)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
uv run --no-sync python scripts/type_check_gate.py --base "$GATE_BASE_SHA"
|
|
|
|
- name: Check tests/e2e basedpyright (zero errors)
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
if git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- ':(glob)tests/e2e/**/*.py' ':(glob)tests/e2e_harness/**/*.py' pyrightconfig.json | grep -q .; then
|
|
uv run --no-sync basedpyright tests/e2e tests/e2e_harness
|
|
else
|
|
echo "No changed tests/e2e Python files; skipping."
|
|
fi
|
|
|
|
- name: Run the e2e harness tests
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
env:
|
|
LITELLM_MASTER_KEY: sk-e2e-harness-tests-reach-no-proxy
|
|
E2E_CACHE_TEST_REDIS_URL: redis://127.0.0.1:6379/0
|
|
run: |
|
|
if ! git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- tests/e2e tests/e2e_harness ':(exclude)tests/e2e/ui' tests/code_coverage_tests/test_provider_replay_harness.py tests/code_coverage_tests/test_provider_cache.py pyproject.toml uv.lock .github/workflows/test-linting.yml | grep -q .; then
|
|
echo "No changed e2e harness files; skipping."
|
|
exit 0
|
|
fi
|
|
retry() { "$@" || { sleep 15; "$@"; } || { sleep 30; "$@"; }; }
|
|
CLAUDE_VERSION="$(retry uv run --no-sync python tests/e2e/claude_code/pr_gate_version_resolver.py)"
|
|
tests/e2e/claude_code/cron_vm/install_claude_code.sh "$CLAUDE_VERSION" "$RUNNER_TEMP/claude-cli"
|
|
PATH="$RUNNER_TEMP/claude-cli:$PATH" uv run --no-sync pytest -q tests/e2e_harness
|
|
uv run --no-sync pytest --tb=short -q --noconftest -o addopts= -o "pythonpath=tests/e2e tests/e2e_harness" -p no:rerunfailures \
|
|
tests/code_coverage_tests/test_provider_replay_harness.py \
|
|
tests/code_coverage_tests/test_provider_cache.py
|
|
|
|
- name: Check for circular imports
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
cd litellm
|
|
uv run --no-sync python ../tests/documentation_tests/test_circular_imports.py
|
|
cd ..
|
|
|
|
- name: Check import safety
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
run: |
|
|
uv run --no-sync python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1)
|
|
secret-scan:
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Run secret scan test
|
|
run: |
|
|
uv run --no-project --with 'pytest==9.0.2' pytest tests/code_coverage_tests/test_no_hardcoded_secrets.py -v
|
|
|
|
- name: Run ggshield secret scan
|
|
env:
|
|
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
|
|
run: |
|
|
if [ -n "$GITGUARDIAN_API_KEY" ]; then
|
|
retry() { "$@" || { sleep 15; "$@"; } || { sleep 30; "$@"; }; }
|
|
retry git fetch --no-tags --unshallow origin
|
|
uv tool run --from 'ggshield==1.48.0' ggshield secret scan repo .
|
|
else
|
|
echo "GITGUARDIAN_API_KEY not set, skipping ggshield scan"
|
|
fi
|
|
ui:
|
|
name: ui
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 8
|
|
defaults:
|
|
run:
|
|
working-directory: ui/litellm-dashboard
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
fetch-depth: 1
|
|
persist-credentials: false
|
|
|
|
- name: Collect changed files
|
|
id: changed
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
base_ref=$(gh api "repos/${GITHUB_REPOSITORY}/branches/${EVENT_BASE_REF}" --jq .name) || base_ref=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .base.ref)
|
|
merge_base=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${base_ref}...${HEAD_SHA}?per_page=1" --jq '.merge_base_commit.sha')
|
|
test -n "$merge_base"
|
|
git fetch --no-tags --depth=1 origin "$merge_base" "$HEAD_SHA"
|
|
: > "$RUNNER_TEMP/prettier_files.txt"
|
|
: > "$RUNNER_TEMP/eslint_files.txt"
|
|
while IFS= read -r f; do
|
|
[ -f "$f" ] || continue
|
|
case "$f" in
|
|
*.js | *.jsx | *.ts | *.tsx | *.mjs | *.cjs)
|
|
printf '%s\n' "$f" >> "$RUNNER_TEMP/prettier_files.txt"
|
|
printf '%s\n' "$f" >> "$RUNNER_TEMP/eslint_files.txt" ;;
|
|
*.json | *.css | *.scss | *.md | *.mdx | *.yml | *.yaml | *.html)
|
|
printf '%s\n' "$f" >> "$RUNNER_TEMP/prettier_files.txt" ;;
|
|
esac
|
|
done < <(git diff --name-only --diff-filter=ACMR --relative "$merge_base" "$HEAD_SHA" -- .)
|
|
if [ -s "$RUNNER_TEMP/prettier_files.txt" ] || [ -s "$RUNNER_TEMP/eslint_files.txt" ]; then
|
|
echo "has_files=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "has_files=false" >> "$GITHUB_OUTPUT"
|
|
echo "No lintable UI files changed in this PR; nothing to check."
|
|
fi
|
|
|
|
- name: Setup Node.js
|
|
if: steps.changed.outputs.has_files == 'true'
|
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
|
with:
|
|
node-version-file: ui/litellm-dashboard/.nvmrc
|
|
cache: "npm"
|
|
cache-dependency-path: ui/litellm-dashboard/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
if: steps.changed.outputs.has_files == 'true'
|
|
run: npm ci
|
|
|
|
- name: Lint changed files (prettier + eslint)
|
|
if: steps.changed.outputs.has_files == 'true'
|
|
run: |
|
|
prettier_files=()
|
|
eslint_files=()
|
|
while IFS= read -r f; do prettier_files+=("$f"); done < "$RUNNER_TEMP/prettier_files.txt"
|
|
while IFS= read -r f; do eslint_files+=("$f"); done < "$RUNNER_TEMP/eslint_files.txt"
|
|
status=0
|
|
if [ ${#prettier_files[@]} -gt 0 ]; then
|
|
echo "::group::Prettier (${#prettier_files[@]} files)"
|
|
npx prettier --check "${prettier_files[@]}" || { status=1; echo "::error::Unformatted files. Fix with: npm run format"; }
|
|
echo "::endgroup::"
|
|
fi
|
|
if [ ${#eslint_files[@]} -gt 0 ]; then
|
|
echo "::group::ESLint (${#eslint_files[@]} files)"
|
|
npx eslint --no-warn-ignored --pass-on-unpruned-suppressions "${eslint_files[@]}" || status=1
|
|
echo "::endgroup::"
|
|
fi
|
|
exit $status
|
|
|
|
- name: Check lint budgets
|
|
if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }}
|
|
run: |
|
|
npx eslint . -f json -o "$RUNNER_TEMP/lint-report.json" || true
|
|
node scripts/check-lint-budgets.mjs "$RUNNER_TEMP/lint-report.json" eslint-budgets.json
|
|
|
|
- name: Check for dead code (knip)
|
|
if: ${{ !cancelled() && steps.changed.outputs.has_files == 'true' }}
|
|
run: npm run knip:ci
|
|
ui-api-types:
|
|
name: ui-api-types
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
persist-credentials: false
|
|
fetch-depth: 2
|
|
|
|
- name: Detect changes that can affect the generated types
|
|
id: changes
|
|
run: |
|
|
set -euo pipefail
|
|
if ! base="$(git rev-parse --verify --quiet HEAD^2 >/dev/null && git rev-parse HEAD^1)"; then
|
|
echo "Not a pull request merge commit, running the full check."
|
|
echo "relevant=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
files="$(git diff --name-only "$base" HEAD)"
|
|
if grep -Eq '^(litellm/|enterprise/|(pyproject\.toml|uv\.lock)$|ui/litellm-dashboard/(src/lib/http/schema\.d\.ts|scripts/gen-api-types\.mjs|package(-lock)?\.json)$|\.github/workflows/test-linting\.yml$)' <<< "$files"; then
|
|
echo "relevant=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "No litellm, enterprise, dependency or generator changes in this pull request, nothing to verify."
|
|
echo "relevant=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Set up Python
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Cache uv dependencies
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
with:
|
|
path: |
|
|
~/.cache/uv
|
|
.venv
|
|
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-uv-
|
|
|
|
- name: Cache the Rust build
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: ./.github/actions/cache-cargo-build
|
|
|
|
- name: Install backend dependencies
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
run: .github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router
|
|
|
|
- name: Cache Prisma binaries
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: ./.github/actions/cache-prisma-binaries
|
|
|
|
- name: Generate Prisma client
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
run: uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
|
|
|
|
- name: Regenerate the lazy OpenAPI snapshot
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
run: uv run --no-sync python -m litellm.proxy._lazy_openapi_snapshot
|
|
|
|
- name: Fail if the lazy OpenAPI snapshot is stale
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
run: |
|
|
if ! git diff --exit-code -- litellm/proxy/_lazy_openapi_snapshot.json; then
|
|
echo "::error file=litellm/proxy/_lazy_openapi_snapshot.json::The lazy OpenAPI snapshot is out of sync with the lazily loaded routes."
|
|
echo ""
|
|
echo "A lazily loaded route or model changed without regenerating the snapshot that /openapi.json serves for unloaded features."
|
|
echo "To fix, run from the repo root:"
|
|
echo " uv run python -m litellm.proxy._lazy_openapi_snapshot"
|
|
echo "then run npm run gen:api from ui/litellm-dashboard and commit both files."
|
|
exit 1
|
|
fi
|
|
echo "_lazy_openapi_snapshot.json is in sync with the lazily loaded routes."
|
|
|
|
- name: Set up Node.js
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
|
with:
|
|
node-version-file: ui/litellm-dashboard/.nvmrc
|
|
cache: "npm"
|
|
cache-dependency-path: ui/litellm-dashboard/package-lock.json
|
|
|
|
- name: Install dashboard dependencies
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
working-directory: ui/litellm-dashboard
|
|
run: npm ci
|
|
|
|
- name: Regenerate types from the live spec
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
working-directory: ui/litellm-dashboard
|
|
env:
|
|
LITELLM_PYTHON: "uv run --no-sync python"
|
|
run: npm run gen:api
|
|
|
|
- name: Fail if types are stale
|
|
if: steps.changes.outputs.relevant == 'true'
|
|
run: |
|
|
if ! git diff --exit-code -- ui/litellm-dashboard/src/lib/http/schema.d.ts; then
|
|
echo "::error file=ui/litellm-dashboard/src/lib/http/schema.d.ts::Generated API types are out of sync with the proxy OpenAPI spec."
|
|
echo ""
|
|
echo "A backend route or model changed without regenerating the dashboard types."
|
|
echo "To fix, run from ui/litellm-dashboard:"
|
|
echo " npm run gen:api"
|
|
echo "then commit the updated src/lib/http/schema.d.ts."
|
|
exit 1
|
|
fi
|
|
echo "schema.d.ts is in sync with the proxy OpenAPI spec."
|
|
code-quality:
|
|
name: code-quality
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Checkout litellm-docs into docs/my-website (for documentation_tests)
|
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
repository: BerriAI/litellm-docs
|
|
path: docs/my-website
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Cache uv dependencies
|
|
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
|
with:
|
|
path: |
|
|
~/.cache/uv
|
|
.venv
|
|
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-uv-
|
|
|
|
- name: Cache the Rust build
|
|
uses: ./.github/actions/cache-cargo-build
|
|
|
|
- name: Install dependencies
|
|
run: uv sync --frozen --all-groups --all-extras
|
|
|
|
- name: check_licenses
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_licenses.py
|
|
|
|
- name: check_provider_folders_documented
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_provider_folders_documented.py
|
|
|
|
- name: check_provider_endpoints_support_generated
|
|
run: uv run --no-sync python scripts/generate_provider_endpoints_support.py --check
|
|
|
|
- name: test_generate_provider_endpoints_support
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_generate_provider_endpoints_support.py
|
|
|
|
- name: check_prisma_binary_cache
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_prisma_binary_cache.py
|
|
|
|
- name: check_workflow_startup_safety
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_workflow_startup_safety.py
|
|
|
|
- name: check_workflow_job_name_collisions
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_workflow_job_name_collisions.py
|
|
|
|
- name: test_workflow_job_name_collisions
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_workflow_job_name_collisions.py
|
|
|
|
- name: test_unit_passed_gate
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_unit_passed_gate.py
|
|
|
|
- name: test_e2e_idp_stack
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_e2e_idp_stack.py
|
|
|
|
- name: test_e2e_metadata
|
|
env:
|
|
PYTHONPATH: tests/e2e
|
|
run: |
|
|
export LITELLM_MASTER_KEY="sk-$(openssl rand -hex 16)"
|
|
uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_e2e_metadata.py tests/code_coverage_tests/test_e2e_junit_report.py
|
|
|
|
- name: Check merge smoke harness
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_merge_smoke.py
|
|
|
|
- name: router_code_coverage
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/router_code_coverage.py
|
|
|
|
- name: router_surface_check
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/router_surface_check.py
|
|
|
|
- name: test_chat_completion_imports
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/test_chat_completion_imports.py
|
|
|
|
- name: info_log_check
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/info_log_check.py
|
|
|
|
- name: check_guardrail_apply_decorator
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_guardrail_apply_decorator.py
|
|
|
|
- name: test_ban_set_verbose
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/test_ban_set_verbose.py
|
|
|
|
- name: code_qa_check_tests
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/code_qa_check_tests.py
|
|
|
|
- name: check_get_model_cost_key_performance
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_get_model_cost_key_performance.py
|
|
|
|
- name: test_proxy_types_import
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/test_proxy_types_import.py
|
|
|
|
- name: callback_manager_test
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/callback_manager_test.py
|
|
|
|
- name: recursive_detector
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/recursive_detector.py
|
|
|
|
- name: test_router_strategy_async
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/test_router_strategy_async.py
|
|
|
|
- name: litellm_logging_code_coverage
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/litellm_logging_code_coverage.py
|
|
|
|
- name: ensure_async_clients_test
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/ensure_async_clients_test.py
|
|
|
|
- name: enforce_llms_folder_style
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/enforce_llms_folder_style.py
|
|
|
|
- name: prevent_key_leaks_in_exceptions
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/prevent_key_leaks_in_exceptions.py
|
|
|
|
- name: check_unsafe_enterprise_import
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_unsafe_enterprise_import.py
|
|
|
|
- name: check_telemetry_imports
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_telemetry_imports.py
|
|
|
|
- name: ban_copy_deepcopy_kwargs
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/ban_copy_deepcopy_kwargs.py
|
|
|
|
- name: check_fastuuid_usage
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_fastuuid_usage.py
|
|
|
|
- name: check_py310_typing_imports
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_py310_typing_imports.py
|
|
|
|
- name: check_e2e_no_raw_requests
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_e2e_no_raw_requests.py
|
|
|
|
- name: check_migrations_no_data_rewrites
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_migrations_no_data_rewrites.py
|
|
|
|
- name: check_migrations_lock_timeout
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_migrations_lock_timeout.py
|
|
|
|
- name: check_no_none_string_error_fields
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_no_none_string_error_fields.py
|
|
|
|
- name: test_check_no_none_string_error_fields
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_check_no_none_string_error_fields.py
|
|
|
|
- name: check_no_publicly_known_master_key
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_no_publicly_known_master_key.py
|
|
|
|
- name: test_check_no_publicly_known_master_key
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_check_no_publicly_known_master_key.py
|
|
|
|
- name: check_unbounded_in_lists (fails on findings not in the baseline)
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_unbounded_in_lists.py
|
|
|
|
- name: memory_test
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/memory_test.py
|
|
|
|
- name: documentation_test_env_keys
|
|
run: uv run --no-sync python ./tests/documentation_tests/test_env_keys.py
|
|
|
|
- name: documentation_test_router_settings
|
|
run: uv run --no-sync python ./tests/documentation_tests/test_router_settings.py
|
|
|
|
- name: documentation_test_api_docs
|
|
run: uv run --no-sync python ./tests/documentation_tests/test_api_docs.py
|
|
ci-coverage:
|
|
name: ci-coverage
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Assert every test file and Dockerfile is invoked by a job
|
|
run: |
|
|
python -m pip install "pyyaml==6.0.3"
|
|
python .github/scripts/assert_ci_coverage.py
|
|
|
|
- name: Assert no -k expression deselects a file from every job that globs it
|
|
run: python .github/scripts/assert_ci_coverage.py --slices
|
|
|
|
- name: Assert .github/workflows/ holds only workflows, correctly named
|
|
run: python .github/scripts/assert_workflow_dir_hygiene.py
|
|
semgrep:
|
|
name: semgrep
|
|
permissions:
|
|
contents: read
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Run Semgrep (custom rules)
|
|
run: uv tool run --from 'semgrep==1.157.0' semgrep scan --config .semgrep/rules . --error
|
|
lint-passed:
|
|
name: lint passed
|
|
needs: [python, secret-scan, ui, ui-api-types, code-quality, ci-coverage, semgrep]
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 2
|
|
permissions: {}
|
|
steps:
|
|
- name: Require every lint job to succeed
|
|
env:
|
|
NEEDS: ${{ toJSON(needs) }}
|
|
run: |
|
|
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<< "$NEEDS"
|
|
jq -e 'all(.[]; .result == "success")' <<< "$NEEDS" > /dev/null
|