ci: skip slow lint/code-quality/mcp/server-root-path jobs on docs/ui-only PRs

Extends the detect-relevant-changes gate from #32532 to the remaining slow PR checks so docs/ui-only PRs finish in seconds while still reporting green required checks. lint, code-quality and mcp use the backend category (skip on ui-only and docs-only); server-root-path uses the client category (skip only on pure-docs since it is a UI-serving e2e). Also guards the last unguarded CircleCI job, using_litellm_on_windows, which was running on every PR.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
mateo 2026-07-08 22:17:28 +00:00 committed by Devin AI
parent 45f9beed2a
commit 274f00f840
6 changed files with 96 additions and 11 deletions

View file

@ -204,6 +204,10 @@ jobs:
CARGO_NET_RETRY: "5"
steps:
- checkout
- run:
name: "Skip job when no backend-relevant files changed"
shell: bash.exe
command: bash .circleci/scripts/path_filter.sh backend
- run:
name: Install Python
command: |

View file

@ -1,15 +1,23 @@
name: "Detect backend-relevant changes"
name: "Detect relevant changes"
description: >-
Classify the pull request's changed files with .circleci/scripts/classify_changes.sh
and expose decision=run|skip. decision=skip means only ui/**, **.md or **.mdx files
changed, so callers can short-circuit expensive steps while the job still completes
successfully and satisfies its required status check. The decision defaults to run for
any non pull_request event or whenever the changed set cannot be resolved, so tests are
never skipped when the classification is uncertain.
and expose decision=run|skip. With category=backend, decision=skip means only ui/**,
**.md or **.mdx files changed; with category=client, decision=skip means only docs
(**.md, **.mdx, docs/**) changed so ui/** changes still run. Callers can short-circuit
expensive steps while the job still completes successfully and satisfies its required
status check. The decision defaults to run for any non pull_request event or whenever
the changed set cannot be resolved, so tests are never skipped when the classification
is uncertain.
inputs:
category:
description: "backend (skip on ui/docs-only) or client (skip on docs-only)"
required: false
default: "backend"
outputs:
decision:
description: "run when backend-relevant files changed, otherwise skip"
description: "run when category-relevant files changed, otherwise skip"
value: ${{ steps.classify.outputs.decision }}
runs:
@ -19,6 +27,7 @@ runs:
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
CATEGORY: ${{ inputs.category }}
run: |
set -uo pipefail
if [ -z "${BASE_SHA:-}" ]; then
@ -43,6 +52,6 @@ runs:
fi
echo "detect-backend-changes: changed files vs ${BASE_SHA}:"
printf '%s\n' "${changed}" | sed 's/^/ /'
decision="$(printf '%s\n' "${changed}" | bash .circleci/scripts/classify_changes.sh backend)" || decision="run"
decision="$(printf '%s\n' "${changed}" | bash .circleci/scripts/classify_changes.sh "${CATEGORY}")" || decision="run"
echo "detect-backend-changes: decision=${decision}"
echo "decision=${decision}" >> "${GITHUB_OUTPUT}"

View file

@ -25,7 +25,12 @@ jobs:
with:
persist-credentials: false
- name: Detect relevant changes
id: changes
uses: ./.github/actions/detect-backend-changes
- name: Checkout litellm-docs into docs/my-website (for documentation_tests)
if: steps.changes.outputs.decision != 'skip'
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
repository: BerriAI/litellm-docs
@ -53,76 +58,101 @@ jobs:
${{ runner.os }}-uv-
- name: Install dependencies
if: steps.changes.outputs.decision != 'skip'
run: uv sync --frozen --all-groups --all-extras
- name: check_licenses
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_licenses.py
- name: check_provider_folders_documented
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_provider_folders_documented.py
- name: router_code_coverage
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/router_code_coverage.py
- name: test_chat_completion_imports
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/test_chat_completion_imports.py
- name: info_log_check
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/info_log_check.py
- name: check_guardrail_apply_decorator
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_guardrail_apply_decorator.py
- name: test_ban_set_verbose
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/test_ban_set_verbose.py
- name: code_qa_check_tests
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/code_qa_check_tests.py
- name: check_get_model_cost_key_performance
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_get_model_cost_key_performance.py
- name: test_proxy_types_import
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/test_proxy_types_import.py
- name: callback_manager_test
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/callback_manager_test.py
- name: recursive_detector
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/recursive_detector.py
- name: test_router_strategy_async
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/test_router_strategy_async.py
- name: litellm_logging_code_coverage
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/litellm_logging_code_coverage.py
- name: ensure_async_clients_test
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/ensure_async_clients_test.py
- name: enforce_llms_folder_style
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/enforce_llms_folder_style.py
- name: prevent_key_leaks_in_exceptions
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/prevent_key_leaks_in_exceptions.py
- name: check_unsafe_enterprise_import
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_unsafe_enterprise_import.py
- name: ban_copy_deepcopy_kwargs
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/ban_copy_deepcopy_kwargs.py
- name: check_fastuuid_usage
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/check_fastuuid_usage.py
- name: memory_test
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/code_coverage_tests/memory_test.py
- name: documentation_test_env_keys
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/documentation_tests/test_env_keys.py
- name: documentation_test_router_settings
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/documentation_tests/test_router_settings.py
- name: documentation_test_api_docs
if: steps.changes.outputs.decision != 'skip'
run: uv run --no-sync python ./tests/documentation_tests/test_api_docs.py

View file

@ -27,6 +27,10 @@ jobs:
clean: true
persist-credentials: false
- name: Detect relevant changes
id: changes
uses: ./.github/actions/detect-backend-changes
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
@ -43,10 +47,12 @@ jobs:
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: Install dependencies
if: steps.changes.outputs.decision != 'skip'
run: |
uv sync --frozen --group proxy-dev
@ -54,12 +60,14 @@ jobs:
# only after `prisma generate` writes prisma/client.py et al. Without this the
# DB wrappers typed against the generated client would degrade to Unknown.
- name: Generate Prisma client
if: steps.changes.outputs.decision != 'skip'
env:
PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache
run: |
uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
- name: Check ruff format
if: steps.changes.outputs.decision != 'skip'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
@ -71,6 +79,7 @@ jobs:
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
@ -80,40 +89,47 @@ jobs:
head -50 litellm/litellm_core_utils/custom_logger_registry.py | tail -10
- name: Run Ruff linting
if: steps.changes.outputs.decision != 'skip'
run: |
cd litellm
uv run --no-sync ruff check .
cd ..
- name: Check strict-rule budget (delta vs base)
if: steps.changes.outputs.decision != 'skip'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
uv run --no-sync python scripts/ruff_strict_gate.py --base "$BASE_SHA"
- name: Check type-discipline budget (mutable collections / casts / type guards / kwargs / unexplained suppressions, delta vs base)
if: steps.changes.outputs.decision != 'skip'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
uv run --no-sync python scripts/type_discipline_gate.py --base "$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 budget (delta vs base)
if: steps.changes.outputs.decision != 'skip'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
(uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py --base "$BASE_SHA"
- 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)
@ -133,12 +149,17 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Detect relevant changes
id: changes
uses: ./.github/actions/detect-backend-changes
- name: Set up Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.12"
- name: Ratchet check (budgets may only decrease; non-gating)
if: steps.changes.outputs.decision != 'skip'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |

View file

@ -21,6 +21,10 @@ jobs:
with:
persist-credentials: false
- name: Detect relevant changes
id: changes
uses: ./.github/actions/detect-backend-changes
- name: Thank You Message
run: |
echo "### 🙏 Thank you for contributing to LiteLLM!" >> $GITHUB_STEP_SUMMARY
@ -37,10 +41,12 @@ jobs:
version: "0.10.9"
- name: Install dependencies
if: steps.changes.outputs.decision != 'skip'
run: |
uv lock --check
.github/scripts/uv_sync_with_retries.sh --frozen --group proxy-dev --extra proxy --extra semantic-router
- name: Run MCP tests
if: steps.changes.outputs.decision != 'skip'
run: |
uv run --no-sync pytest tests/mcp_tests -x -vv -n 4 --cov=./litellm --cov-report=xml --durations=5

View file

@ -26,16 +26,25 @@ jobs:
with:
persist-credentials: false
- name: Detect relevant changes
id: changes
uses: ./.github/actions/detect-backend-changes
with:
category: client
- name: Free up disk space
if: steps.changes.outputs.decision != 'skip'
run: |
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/share/boost
sudo apt-get clean
df -h /
- name: Set up Docker Buildx
if: steps.changes.outputs.decision != 'skip'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build Docker image
if: steps.changes.outputs.decision != 'skip'
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
with:
context: .
@ -45,6 +54,7 @@ jobs:
push: false
- name: Start LiteLLM container with SERVER_ROOT_PATH
if: steps.changes.outputs.decision != 'skip'
run: |
docker run -d \
--name litellm-test \
@ -55,6 +65,7 @@ jobs:
--detailed_debug
- name: Wait for container to be healthy
if: steps.changes.outputs.decision != 'skip'
run: |
echo "Waiting for LiteLLM to start..."
max_attempts=30
@ -79,10 +90,11 @@ jobs:
sleep 5
- name: Show container logs
if: always()
if: always() && steps.changes.outputs.decision != 'skip'
run: docker logs litellm-test
- name: Test UI endpoint with root path
if: steps.changes.outputs.decision != 'skip'
run: |
ROOT_PATH="${{ matrix.root_path }}"
echo "Testing UI at: http://localhost:4000${ROOT_PATH}/ui/"
@ -102,11 +114,13 @@ jobs:
exit 1
- name: Setup Node for Playwright
if: steps.changes.outputs.decision != 'skip'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "20"
- name: Install UI deps and Chromium
if: steps.changes.outputs.decision != 'skip'
working-directory: ui/litellm-dashboard
run: |
retry() {
@ -131,13 +145,14 @@ jobs:
retry npx playwright install --with-deps chromium
- name: Run SERVER_ROOT_PATH redirect e2e
if: steps.changes.outputs.decision != 'skip'
working-directory: ui/litellm-dashboard
env:
SERVER_ROOT_PATH: ${{ matrix.root_path }}
run: npx playwright test --config=e2e_tests/serverRootPath.config.ts
- name: Upload Playwright artifacts on failure
if: failure()
if: failure() && steps.changes.outputs.decision != 'skip'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: playwright-trace-${{ strategy.job-index }}
@ -145,7 +160,7 @@ jobs:
retention-days: 7
- name: Cleanup
if: always()
if: always() && steps.changes.outputs.decision != 'skip'
run: |
docker stop litellm-test || true
docker rm litellm-test || true