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 }} PR_NUMBER: ${{ github.event.pull_request.number }} EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | retry() { "$@" || { sleep 15; "$@"; } || { sleep 30; "$@"; }; } BASE_REF=$(gh api "repos/${GITHUB_REPOSITORY}/branches/${EVENT_BASE_REF}" --jq .name) || BASE_REF=$(retry gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .base.ref) MERGE_BASE=$(retry gh api "repos/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${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 pull-requests: 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 }} PR_NUMBER: ${{ github.event.pull_request.number }} EVENT_BASE_REF: ${{ github.event.pull_request.base.ref }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | base_ref=$(gh api "repos/${GITHUB_REPOSITORY}/branches/${EVENT_BASE_REF}" --jq .name) || base_ref=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" --jq .base.ref) merge_base=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${base_ref}...${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/|enterprise/|(pyproject\.toml|uv\.lock)$|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 litellm, enterprise, dependency 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_provider_endpoints_support_generated run: uv run --no-sync python scripts/generate_provider_endpoints_support.py --check - name: test_generate_provider_endpoints_support run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_generate_provider_endpoints_support.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: router_surface_check run: uv run --no-sync python ./tests/code_coverage_tests/router_surface_check.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_none_string_error_fields run: uv run --no-sync python ./tests/code_coverage_tests/check_no_none_string_error_fields.py - name: test_check_no_none_string_error_fields run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_check_no_none_string_error_fields.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