mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-14 04:09:09 +00:00
* ci: delete legacy required-checks shim workflow * ci: fold semgrep into lint and helm into unit, drop the duplicate UI build semgrep runs the repo's own rules over the source, so it becomes lint / semgrep behind lint passed. The helm chart tests become unit / helm behind unit passed. UI Build Check ran the same docker build --target ui-builder as smoke / dashboard-build, so it is deleted * ci: merge the alphabetical llms shards into one llms-providers shard The two shards were split by letter range, which says nothing about what they run. Every provider without its own shard now runs in llms-providers, with the same 137 paths as before --------- Co-authored-by: yuneng <yuneng@berri.ai>
684 lines
28 KiB
YAML
684 lines
28 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: 15
|
|
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: 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
|
|
run: |
|
|
if ! git diff --name-only --diff-filter=ACMRD "$GATE_BASE_SHA" HEAD -- tests/e2e tests/e2e_harness ':(exclude)tests/e2e/ui' 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
|
|
|
|
- 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_changed_gate
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_e2e_changed_gate.py 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: 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_greptile_files_json
|
|
run: uv run --no-sync python ./tests/code_coverage_tests/check_greptile_files_json.py
|
|
|
|
- name: test_check_greptile_files_json
|
|
run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_check_greptile_files_json.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
|