From 274f00f840617bb0921f0fd42695ae9128a4ee75 Mon Sep 17 00:00:00 2001 From: mateo Date: Wed, 8 Jul 2026 22:17:28 +0000 Subject: [PATCH] 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> --- .circleci/config.yml | 4 +++ .../actions/detect-backend-changes/action.yml | 25 +++++++++++----- .github/workflows/test-code-quality.yml | 30 +++++++++++++++++++ .github/workflows/test-linting.yml | 21 +++++++++++++ .github/workflows/test-mcp.yml | 6 ++++ .github/workflows/test_server_root_path.yml | 21 +++++++++++-- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b0a705966a2..c57b2e19718 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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: | diff --git a/.github/actions/detect-backend-changes/action.yml b/.github/actions/detect-backend-changes/action.yml index af01038f294..75607f550c5 100644 --- a/.github/actions/detect-backend-changes/action.yml +++ b/.github/actions/detect-backend-changes/action.yml @@ -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}" diff --git a/.github/workflows/test-code-quality.yml b/.github/workflows/test-code-quality.yml index 872a1799d98..e3509302df0 100644 --- a/.github/workflows/test-code-quality.yml +++ b/.github/workflows/test-code-quality.yml @@ -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 diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index c2fc3453261..821ade2d4fb 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -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: | diff --git a/.github/workflows/test-mcp.yml b/.github/workflows/test-mcp.yml index 5b5290880c1..293e96d5b44 100644 --- a/.github/workflows/test-mcp.yml +++ b/.github/workflows/test-mcp.yml @@ -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 diff --git a/.github/workflows/test_server_root_path.yml b/.github/workflows/test_server_root_path.yml index f59cee29893..cd661c6a1fd 100644 --- a/.github/workflows/test_server_root_path.yml +++ b/.github/workflows/test_server_root_path.yml @@ -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