mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
PR #37550 taught the backend unit-test shards to read the pull request's own file list, but four required jobs were never wired to that gate and ran in full on every pull request regardless of what it touched. A UI-only pull request still paid roughly 17 runner-minutes of Python work it could not have affected, and a backend-only one still installed and built the dashboard. Lint and the MCP suite now take the existing backend decision. The dashboard build and unit tests take a new ui decision, which tracks ui/ rather than reusing client: client deliberately runs whenever the backend changes, because it gates CircleCI's end-to-end jobs that drive a real proxy, while the build and the unit tests cannot see the backend at all. CI config counts as ui-relevant too, so a pull request that rewrites the dashboard workflows still exercises them instead of shipping unvalidated. The gate stays inside the job rather than moving to on.paths or to a job-level condition on the shard callers. A workflow filtered out by on.paths never starts and never reports, so a required check waits forever, and a skipped caller job publishes its own name instead of the nested "<shard> / Run tests" the ruleset requires. Both were measured before settling on this shape. Three setup steps in the shard base and in the documentation job also leaked past the gate, so a skipped shard still spent about twelve seconds installing uv and restoring its cache. They now carry the same condition, and the documentation job stops cloning litellm-docs when it has nothing to validate.
208 lines
6.9 KiB
YAML
208 lines
6.9 KiB
YAML
name: _Unit Test Base (Reusable)
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
test-path:
|
|
description: "Pytest path(s) to run"
|
|
required: true
|
|
type: string
|
|
workers:
|
|
description: "Number of pytest-xdist workers"
|
|
required: false
|
|
type: number
|
|
default: 2
|
|
reruns:
|
|
description: "Number of reruns for flaky tests"
|
|
required: false
|
|
type: number
|
|
default: 2
|
|
timeout-minutes:
|
|
description: >-
|
|
Timeout for the test step alone. Setup (checkout, dependency install,
|
|
Prisma client generation) gets its own allowance on top, so a slow
|
|
runner or a cold binary download can never cancel passing tests.
|
|
required: false
|
|
type: number
|
|
default: 20
|
|
job-timeout-minutes:
|
|
description: >-
|
|
Backstop for the whole job. Keep it >= `timeout-minutes` plus 35: 30 for
|
|
the per-step ceilings on the setup steps below, and 5 for the runner
|
|
overhead the job clock charges but no step owns (job init, step
|
|
transitions, post-job cleanup). That headroom is what makes the test
|
|
budget a floor rather than a hope, since setup cannot overrun into it
|
|
without failing its own step first. GitHub expressions have no
|
|
arithmetic, so the sum is passed in rather than computed.
|
|
required: false
|
|
type: number
|
|
default: 55
|
|
max-failures:
|
|
description: "Stop after this many failures"
|
|
required: false
|
|
type: number
|
|
default: 10
|
|
dist:
|
|
description: "pytest-xdist distribution mode (loadscope|load|worksteal|loadfile|no)"
|
|
required: false
|
|
type: string
|
|
default: "loadscope"
|
|
artifact-name:
|
|
description: "Unique name for the coverage artifact (must be unique per run)"
|
|
required: true
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
run:
|
|
name: Run tests
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: ${{ inputs.job-timeout-minutes }}
|
|
permissions:
|
|
contents: read
|
|
pull-requests: read
|
|
outputs:
|
|
decision: ${{ steps.changes.outputs.decision }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
timeout-minutes: 3
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Detect relevant changes
|
|
id: changes
|
|
timeout-minutes: 2
|
|
uses: ./.github/actions/detect-changes
|
|
|
|
- name: Set up Python
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 3
|
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Set up uv
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 3
|
|
uses: ./.github/actions/setup-uv-with-retries
|
|
with:
|
|
version: "0.10.9"
|
|
|
|
- name: Cache uv dependencies
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 5
|
|
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: Install dependencies
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 8
|
|
run: |
|
|
.github/scripts/uv_sync_with_retries.sh --frozen --group ci --group proxy-dev --extra google --extra proxy --extra semantic-router --extra saml
|
|
|
|
- name: Cache Prisma binaries
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 3
|
|
uses: ./.github/actions/cache-prisma-binaries
|
|
|
|
- name: Generate Prisma client
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: 3
|
|
run: |
|
|
uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
|
|
|
|
- name: Run tests
|
|
if: steps.changes.outputs.decision != 'skip'
|
|
timeout-minutes: ${{ inputs.timeout-minutes }}
|
|
env:
|
|
TEST_PATH: ${{ inputs.test-path }}
|
|
MAX_FAILURES: ${{ inputs.max-failures }}
|
|
WORKERS: ${{ inputs.workers }}
|
|
RERUNS: ${{ inputs.reruns }}
|
|
DIST: ${{ inputs.dist }}
|
|
run: |
|
|
if [ "${WORKERS}" = "0" ]; then
|
|
uv run --no-sync pytest ${TEST_PATH:?} \
|
|
--tb=short -vv \
|
|
--maxfail="${MAX_FAILURES}" \
|
|
--reruns "${RERUNS}" \
|
|
--reruns-delay 1 \
|
|
--durations=20 \
|
|
--cov=./litellm \
|
|
--cov-report=xml:coverage.xml \
|
|
--cov-config=pyproject.toml
|
|
else
|
|
uv run --no-sync pytest ${TEST_PATH:?} \
|
|
--tb=short -vv \
|
|
--maxfail="${MAX_FAILURES}" \
|
|
-n "${WORKERS}" \
|
|
--reruns "${RERUNS}" \
|
|
--reruns-delay 1 \
|
|
--dist="${DIST}" \
|
|
--durations=20 \
|
|
--cov=./litellm \
|
|
--cov-report=xml:coverage.xml \
|
|
--cov-config=pyproject.toml
|
|
fi
|
|
|
|
- name: Save coverage report
|
|
if: always() && steps.changes.outputs.decision != 'skip'
|
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
|
with:
|
|
name: coverage-${{ inputs.artifact-name }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: coverage.xml
|
|
retention-days: 1
|
|
|
|
upload-coverage:
|
|
name: Upload coverage to Codecov
|
|
needs: run
|
|
if: always() && needs.run.outputs.decision != 'skip'
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
pull-requests: write
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Download coverage report
|
|
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
|
with:
|
|
pattern: coverage-${{ inputs.artifact-name }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
path: coverage-reports
|
|
merge-multiple: true
|
|
|
|
- name: Upload to Codecov
|
|
id: codecov-upload
|
|
continue-on-error: true
|
|
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4
|
|
with:
|
|
use_oidc: true
|
|
directory: coverage-reports
|
|
root_dir: ${{ github.workspace }}
|
|
flags: ${{ inputs.artifact-name }}
|
|
fail_ci_if_error: false
|
|
|
|
- name: Upload to Codecov (retry)
|
|
if: steps.codecov-upload.outcome == 'failure'
|
|
continue-on-error: true
|
|
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4
|
|
with:
|
|
use_oidc: true
|
|
directory: coverage-reports
|
|
root_dir: ${{ github.workspace }}
|
|
flags: ${{ inputs.artifact-name }}
|
|
fail_ci_if_error: false
|