mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-26 06:03:19 +00:00
* test(mcp): move the MCP OAuth happy path from a live Linear e2e to an integration test GitHub Actions now runs unit tests only, and the MCP OAuth happy path needed a captured Linear browser login to click through a real consent page. The OAuth flows it exercised are already integration tests against an owned authorization-server double, except one behavior: the stored per-user token must be served by a proxy process that never saw the consent, with no shared Redis to fall back on. That case is now in test_mcp_oauth_flows.py The e2e test, its owned-gateway helper, its workflow, the mcp_oauth_live marker and the JUnit gate script only that workflow used are removed * test(mcp): chat completions run an OAuth server tool with the caller's own stored token * test(e2e): drop the Linear chat-completion MCP OAuth e2e and its helpers The behavior it checked, a chat completion running an OAuth server's tool with the caller's stored per-user token, is now an integration test in test_mcp_llm_endpoints.py that needs no saved Linear browser login * test(mcp): decode the stored per-user credential as urlsafe base64 The plaintext check used b64decode, which drops the - and _ of a urlsafe value and raised on roughly one ciphertext in five * test(mcp): drop the chat OAuth integration test that duplicates test_mcp_slots F2-chat security/test_mcp_slots.py already sends a per-user OAuth token through /v1/chat/completions and asserts the peer receives it. Every mutant the new test killed was also killed by the existing suite, so it added no signal
706 lines
29 KiB
YAML
706 lines
29 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 }}
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
retry() { "$@" || { sleep 15; "$@"; } || { sleep 30; "$@"; }; }
|
|
MERGE_BASE=$(retry gh api "repos/${{ github.repository }}/compare/${BASE_SHA}...${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
|
|
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 }}
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
# base.sha is the base branch tip from when the PR was opened, while
|
|
# actions/checkout leaves HEAD on a merge of the PR into the *current*
|
|
# base tip. "$BASE_SHA"...HEAD therefore spans every base-branch commit
|
|
# landed since, so a PR that touches no UI file still gets linted
|
|
# against hundreds of other people's files. Diff the PR head against its
|
|
# own merge base instead, which is exactly what this PR changed.
|
|
merge_base=$(gh api "repos/${{ github.repository }}/compare/${BASE_SHA}...${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/(proxy|types)/|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 proxy, types 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_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: 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_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
|