ci: follow the default branch in development tooling

This commit is contained in:
Yuneng Jiang 2026-09-07 14:34:45 -07:00
parent f896df1b06
commit e238d20fbd
No known key found for this signature in database
15 changed files with 437 additions and 94 deletions

View file

@ -29,7 +29,7 @@ Never test structure of code only function of it
End-to-end tests belong in `tests/e2e/` and must follow the harness conventions documented in that directory's `CLAUDE.md`
When creating PRs, don't set base to `main`. `litellm_internal_staging` is the default base branch and serves that purpose for both internal and external / OSS contributions
When creating PRs, target the repository's current default branch for both internal and external / OSS contributions. Check it with `python3 scripts/default_branch.py --branch` instead of assuming a branch name or relying on cached `origin/HEAD`
When writing a PR body, treat the comments and imperative instructions inside .github/pull_request_template.md as rules to follow, not just layout. Agent harnesses may strip HTML comments from copies of that file injected into context, so read .github/pull_request_template.md from disk before writing a PR body to make sure you see every comment rule
@ -52,7 +52,7 @@ Don't hesitate to use values in .env to get needed API keys and other secrets, a
Python max line length is 120, not 88
Never edit or commit `ruff-strict-budget.json`, `type-discipline-budget.json`, `basedpyright-code-budget.json`, or `test-quality-budget.json` on a PR branch, and don't run `make lint-budget-update` there. A scheduled Devin automation lowers the limits on `litellm_internal_staging` in its own PR by exactly what landed since the last ratchet, so concurrent PRs don't fight over the same `"limit"` lines. If your branch already carries a budget edit, drop it before opening the PR
Never edit or commit `ruff-strict-budget.json`, `type-discipline-budget.json`, `basedpyright-code-budget.json`, or `test-quality-budget.json` on a PR branch, and don't run `make lint-budget-update` there. A scheduled Devin automation lowers the limits on the default branch in its own PR by exactly what landed since the last ratchet, so concurrent PRs don't fight over the same `"limit"` lines. Keep the hosted automation's target in sync when the repository default changes. If your branch already carries a budget edit, drop it before opening the PR
`make check` (f.k.a. `make pre-commit`, which still works identically as an alias) saves its complete output to a log file in .git (overwriting previous logs) and prints that path as its first and last output lines. To inspect a run, read or grep that log instead of re-running the multi-minute checks just to see a different slice
@ -70,7 +70,7 @@ When referencing or running models (coding, QA'ing, writing docs, writing tests,
Always pull before starting any work. The checkout or worktree may be sitting on a stale branch
If you're an internal contributor, when creating a new PR, the typical flow is to branch off litellm_internal_staging and create a branch prefixed with litellm_. Do not create a branch prefixed with claude/ and generally do not have / in your branch names
If you're an internal contributor, when creating a new PR, the typical flow is to branch off the repository's current default branch and create a branch prefixed with litellm_. Do not create a branch prefixed with claude/ and generally do not have / in your branch names
Do not add `Co-Authored-By: Claude` or any Claude attribution to commit messages. Never use a `claude/` prefix or put a `/` in a branch name. Do not add "Generated with Claude Code" (or any similar attribution) to PR descriptions or comments. Do not create a new PR/branch off the existing PR to fix/add something that is related and could've just been committed directly to the existing PR's branch

View file

@ -315,10 +315,12 @@ Ensure the UI builds successfully before submitting your PR:
npm run build
```
Local lint and budget checks follow origin's current default branch. They refresh it from the remote instead of trusting cached `origin/HEAD`. For an intentional comparison against another branch or commit, use `make check BASE_REF=<ref>` or the standalone gate's `--base <ref>` option. An explicit ref can also be used offline once it has been fetched locally. Without an override, unavailable remote metadata stops the check
## Submitting Your PR
1. **Push your branch**: `git push origin your-feature-branch`
2. **Create a PR**: Go to GitHub and open a pull request against [`litellm_internal_staging`](https://github.com/BerriAI/litellm/tree/litellm_internal_staging), which is the default base branch. Do not target `main`.
2. **Create a PR**: Go to GitHub and open a pull request against the repository's current default branch. Run `python3 scripts/default_branch.py --branch` to check its name
3. **Fill out the PR template**: Provide clear description of changes
4. **Wait for review**: Maintainers will review and provide feedback
5. **Address feedback**: Make requested changes and push updates

View file

@ -34,7 +34,7 @@ help:
@echo " make lint-basedpyright-budget-update - Ratchet basedpyright limits down by what this branch fixed"
@echo " make lint-format - Check ruff format formatting (matches CI)"
@echo " make lint-ruff-budget - Gate the codebase total of each strict ruff rule against its limit"
@echo " make lint-gate - Strict ruff gate in CI-parity mode (fetches staging, simulates the merge)"
@echo " make lint-gate - Strict ruff gate in CI-parity mode (fetches the default branch, simulates the merge)"
@echo " make lint-ruff-budget-update - Ratchet ruff-strict-budget.json limits down by what this branch fixed"
@echo " make lint-test-quality - Gate the test suite against test-quality-budget.json"
@echo " make lint-budget-update - Ratchet all budgets down (ruff + type-discipline + test quality + basedpyright)"
@ -60,6 +60,9 @@ help:
UV := uv
UV_RUN := $(UV) run --no-sync
BASE_REF ?=
export BASE_REF
RESOLVE_BASE = python3 scripts/default_branch.py --base "$(BASE_REF)"
# Machine-wide slot queue for the heavy targets below; python3 + stdlib only, so
# it runs before any venv exists. See scripts/gate_slot_lock.py.
@ -133,7 +136,7 @@ format-check: install-dev
# Single fetch of the PR base so the delta-based gates below share one network round
# trip instead of each re-fetching when chained from `lint`.
lint-fetch-base:
git fetch origin litellm_internal_staging
@$(RESOLVE_BASE)
# Mirror test-linting.yml's lint job environment: the proxy-dev group plus a generated
# Prisma client, so `basedpyright tests/e2e` resolves the same modules CI does. The
@ -150,7 +153,9 @@ lint-install:
# recursively, so 'litellm/*.py' covers nested modules and the top-level files that
# CI's 'litellm/**/*.py' skips, which makes this target a superset of the CI step.
lint-format-check-changed: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
@files=$$(git diff --name-only --diff-filter=ACMR origin/litellm_internal_staging...HEAD -- 'litellm/*.py' | grep -v '^litellm/enterprise/' || true); \
@base_ref=$$($(RESOLVE_BASE)) && \
changed=$$(git diff --name-only --diff-filter=ACMR "$$base_ref...HEAD" -- 'litellm/*.py') && \
files=$$(printf '%s\n' "$$changed" | grep -v '^litellm/enterprise/' || true) || exit $$?; \
if [ -z "$$files" ]; then \
echo "No changed litellm Python files to format-check."; \
else \
@ -167,7 +172,9 @@ lint-ruff: $(LINT_DEP_INSTALL)
# https://github.com/astral-sh/ruff/discussions/10977
# https://github.com/astral-sh/ruff/discussions/4049
lint-format-changed: install-dev
@git diff origin/main --unified=0 --no-color -- '*.py' | \
@base_ref=$$($(RESOLVE_BASE)) && \
diff=$$(git diff "$$base_ref" --unified=0 --no-color -- '*.py') && \
printf '%s\n' "$$diff" | \
perl -ne '\
if (/^diff --git a\/(.*) b\//) { $$file = $$1; } \
if (/^@@ .* \+(\d+)(?:,(\d+))? @@/) { \
@ -182,20 +189,22 @@ lint-format-changed: install-dev
done
lint-ruff-dev: install-dev
@tmpfile=$$(mktemp /tmp/ruff-dev.XXXXXX) && \
@base_ref=$$($(RESOLVE_BASE)) || exit $$?; \
tmpfile=$$(mktemp /tmp/ruff-dev.XXXXXX) && \
cd litellm && \
($(UV_RUN) ruff check . --output-format=pylint || true) > "$$tmpfile" && \
$(UV_RUN) diff-quality --violations=pylint "$$tmpfile" --compare-branch=origin/main && \
$(UV_RUN) diff-quality --violations=pylint "$$tmpfile" --compare-branch="$$base_ref" && \
cd .. ; \
rm -f "$$tmpfile"
lint-ruff-FULL-dev: install-dev
@files=$$(git diff --name-only origin/main -- '*.py'); \
@base_ref=$$($(RESOLVE_BASE)) && \
files=$$(git diff --name-only "$$base_ref" -- '*.py') || exit $$?; \
if [ -n "$$files" ]; then echo "$$files" | xargs $(UV_RUN) ruff check; \
else echo "No changed .py files to check."; fi
lint-basedpyright: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
$(UV_RUN) python scripts/type_check_gate.py --base origin/litellm_internal_staging
$(UV_RUN) python scripts/type_check_gate.py --base "$(BASE_REF)"
lint-e2e-basedpyright: $(LINT_E2E_DEP_INSTALL)
$(UV_RUN) basedpyright tests/e2e
@ -203,37 +212,37 @@ lint-e2e-basedpyright: $(LINT_E2E_DEP_INSTALL)
# Type-discipline budget (mutable collections / casts / type guards / kwargs /
# unexplained suppressions), the test-linting.yml step `make lint` used to omit.
lint-type-discipline: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
$(UV_RUN) python scripts/type_discipline_gate.py --base origin/litellm_internal_staging
$(UV_RUN) python scripts/type_discipline_gate.py --base "$(BASE_REF)"
# Test-quality budget (zero-assert / mock-echo tests, sys.path.insert, raw env writes,
# litellm module-global mutation, credential-gated skips, conftest snapshot
# inventory), counted across tests/ the same delta-vs-base way.
lint-test-quality: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
$(UV_RUN) python scripts/test_quality_gate.py --base origin/litellm_internal_staging
$(UV_RUN) python scripts/test_quality_gate.py --base "$(BASE_REF)"
# --update lowers each limit by what this branch fixed since its branch point, so
# it needs the base ref fetched to resolve the merge-base.
lint-basedpyright-budget-update: install-dev lint-fetch-base
$(UV_RUN) python scripts/type_check_gate.py --update
$(UV_RUN) python scripts/type_check_gate.py --update --base "$(BASE_REF)"
lint-format: format-check
lint-ruff-budget: install-dev
$(UV_RUN) python scripts/ruff_strict_gate.py
$(UV_RUN) python scripts/ruff_strict_gate.py --base "$(BASE_REF)"
# Strict gate, invoked the same way CI does in test-linting.yml so a local pass
# means the CI check will pass too.
lint-gate: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE)
$(UV_RUN) python scripts/ruff_strict_gate.py --base origin/litellm_internal_staging
$(UV_RUN) python scripts/ruff_strict_gate.py --base "$(BASE_REF)"
lint-ruff-budget-update: install-dev lint-fetch-base
$(UV_RUN) python scripts/ruff_strict_gate.py --update
$(UV_RUN) python scripts/ruff_strict_gate.py --update --base "$(BASE_REF)"
lint-type-discipline-budget-update: install-dev lint-fetch-base
$(UV_RUN) python scripts/type_discipline_gate.py --update
$(UV_RUN) python scripts/type_discipline_gate.py --update --base "$(BASE_REF)"
lint-test-quality-budget-update: install-dev lint-fetch-base
$(UV_RUN) python scripts/test_quality_gate.py --update
$(UV_RUN) python scripts/test_quality_gate.py --update --base "$(BASE_REF)"
# Ratchet all budgets in one shot (ruff strict + type-discipline + test quality + basedpyright)
lint-budget-update: lint-ruff-budget-update lint-type-discipline-budget-update lint-test-quality-budget-update lint-basedpyright-budget-update
@ -249,14 +258,15 @@ check-import-safety: $(LINT_DEP_INSTALL)
# runs the diff-scoped ruff format check, whole-tree ruff check, the strict-rule /
# type-discipline / basedpyright budgets as a delta vs the base, then the circular-import
# and import-safety checks. Steps that compare against the base resolve it the same way CI
# does (merge-base with origin/litellm_internal_staging). Setup (env sync, Prisma client,
# does (merge-base with origin's current default branch). Setup (env sync, Prisma client,
# base fetch) runs once up front; the checks themselves are independent, so a sub-make
# fans them out with -j and the fast ones finish under basedpyright's shadow.
lint:
@$(GATE_SLOT_LOCK) $(MAKE) lint-inner
lint-inner: lint-install lint-fetch-base
$(MAKE) -j $(LINT_JOBS) $(LINT_OUTPUT_SYNC) LINT_DEP_INSTALL= LINT_E2E_DEP_INSTALL= LINT_DEP_BASE= lint-checks
lint-inner: lint-install
@base_ref=$$($(RESOLVE_BASE)) && \
$(MAKE) BASE_REF="$$base_ref" -j $(LINT_JOBS) $(LINT_OUTPUT_SYNC) LINT_DEP_INSTALL= LINT_E2E_DEP_INSTALL= LINT_DEP_BASE= lint-checks
lint-checks: lint-format-check-changed lint-ruff lint-gate lint-type-discipline lint-test-quality lint-basedpyright lint-e2e-basedpyright check-circular-imports check-import-safety

View file

@ -6,12 +6,9 @@ import subprocess
import sys
from datetime import datetime
from pathlib import Path
import testing.postgresql
from typing import Final
DESTRUCTIVE_PATTERN = re.compile(r"\bDROP\s+(COLUMN|TABLE|INDEX)\b", re.IGNORECASE)
DEFAULT_BASE_BRANCH = "litellm_internal_staging"
def _find_destructive_statements(sql: str) -> list:
@ -94,31 +91,57 @@ def _print_stale_branch_refusal(base_branch: str, behind: int) -> None:
print(banner, file=out)
def _check_branch_freshness(root_dir: Path, base_branch: str) -> None:
def _default_base_branch(root_dir: Path) -> str:
try:
result: Final = subprocess.run(
[
sys.executable,
str(Path(__file__).resolve().parents[1] / "scripts" / "default_branch.py"),
"--repo-root",
str(root_dir),
"--branch",
],
check=True,
capture_output=True,
text=True,
timeout=90,
)
except (OSError, subprocess.SubprocessError) as exc:
_print_freshness_failure(
"default branch",
"Could not discover origin's default branch. Pass --base-branch <name> to choose one.",
exc.stderr if isinstance(exc, subprocess.CalledProcessError) else str(exc),
)
sys.exit(3)
return result.stdout.strip()
def _check_branch_freshness(root_dir: Path, base_branch: str | None = None) -> None:
"""Fetch origin/<base_branch> and exit 3 if HEAD is behind it."""
resolved_branch: Final = base_branch or _default_base_branch(root_dir)
cwd = str(root_dir)
try:
subprocess.run(
["git", "fetch", "origin", base_branch],
["git", "fetch", "origin", f"+refs/heads/{resolved_branch}:refs/remotes/origin/{resolved_branch}"],
check=True,
capture_output=True,
text=True,
cwd=cwd,
)
except FileNotFoundError:
_print_freshness_failure(base_branch, "git executable not found on PATH")
_print_freshness_failure(resolved_branch, "git executable not found on PATH")
sys.exit(3)
except subprocess.CalledProcessError as e:
_print_freshness_failure(
base_branch,
f"`git fetch origin {base_branch}` failed",
resolved_branch,
f"`git fetch origin {resolved_branch}` failed",
e.stderr or "",
)
sys.exit(3)
try:
result = subprocess.run(
["git", "rev-list", "--count", f"HEAD..origin/{base_branch}"],
["git", "rev-list", "--count", f"HEAD..origin/{resolved_branch}"],
check=True,
capture_output=True,
text=True,
@ -127,23 +150,23 @@ def _check_branch_freshness(root_dir: Path, base_branch: str) -> None:
behind = int(result.stdout.strip())
except subprocess.CalledProcessError as e:
_print_freshness_failure(
base_branch,
f"`git rev-list HEAD..origin/{base_branch}` failed",
resolved_branch,
f"`git rev-list HEAD..origin/{resolved_branch}` failed",
e.stderr or "",
)
sys.exit(3)
except ValueError:
_print_freshness_failure(
base_branch,
resolved_branch,
"could not parse commit count from `git rev-list`",
)
sys.exit(3)
if behind > 0:
_print_stale_branch_refusal(base_branch, behind)
_print_stale_branch_refusal(resolved_branch, behind)
sys.exit(3)
print(f"Branch freshness OK: up to date with origin/{base_branch}.")
print(f"Branch freshness OK: up to date with origin/{resolved_branch}.")
def _print_destructive_refusal(destructive_lines: list) -> None:
@ -198,7 +221,7 @@ def _print_destructive_refusal(destructive_lines: list) -> None:
def create_migration(
migration_name: str = None,
allow_destructive: bool = False,
base_branch: str = DEFAULT_BASE_BRANCH,
base_branch: str | None = None,
skip_freshness_check: bool = False,
):
"""
@ -211,7 +234,7 @@ def create_migration(
DROP COLUMN, DROP TABLE, or DROP INDEX statements. Without this
flag, the script exits non-zero and prints guidance.
base_branch (str): Branch to check freshness against
(default: "litellm_internal_staging").
(default: origin's current default branch).
skip_freshness_check (bool): Skip the "branch is up to date" check.
Only for intentional migrations against an older base.
"""
@ -225,6 +248,8 @@ def create_migration(
else:
_check_branch_freshness(root_dir, base_branch)
import testing.postgresql
try:
migrations_dir = (
root_dir / "litellm-proxy-extras" / "litellm_proxy_extras" / "migrations"
@ -342,9 +367,8 @@ if __name__ == "__main__":
)
parser.add_argument(
"--base-branch",
default=DEFAULT_BASE_BRANCH,
help=(
f"Branch to check freshness against (default: {DEFAULT_BASE_BRANCH}). "
"Branch to check freshness against (default: origin's current default branch). "
"The script fetches origin/<base-branch> and refuses to run if HEAD "
"is behind it."
),

View file

@ -48,7 +48,7 @@ uv run --with testing.postgresql python ci_cd/run_migration.py "your_migration_n
## What It Does
1. **Verifies the current branch is up to date with `origin/litellm_internal_staging`** (see [Branch freshness](#branch-freshness-check))
1. **Verifies the current branch is up to date with origin's current default branch** (see [Branch freshness](#branch-freshness-check))
2. Creates temp PostgreSQL DB
3. Applies existing migrations
4. Compares with `schema.prisma`
@ -57,11 +57,11 @@ uv run --with testing.postgresql python ci_cd/run_migration.py "your_migration_n
## Branch Freshness Check
Before generating anything, `run_migration.py` runs `git fetch origin <base>` and refuses to proceed if `HEAD` is behind `origin/<base>`. Default base is `litellm_internal_staging` (the branch PRs target). A previous incident saw a stale branch silently drop production columns; freshness is the first-line defense.
Before generating anything, `run_migration.py` runs `git fetch origin <base>` and refuses to proceed if `HEAD` is behind `origin/<base>`. The default base is discovered from origin's advertised HEAD on each run, so an existing clone follows a default-branch change without trusting cached `origin/HEAD`. If discovery or fetching fails, migration generation stops. A previous incident saw a stale branch silently drop production columns; freshness is the first-line defense.
Flags:
- `--base-branch <name>` — check against a different base (e.g. `main`). Default is `litellm_internal_staging`.
- `--base-branch <name>` — check against a different base (e.g. a release branch). Defaults to origin's current default branch
- `--skip-freshness-check` — bypass entirely. Only for intentional migrations against an older base.
When the guard fires:
@ -69,8 +69,9 @@ When the guard fires:
1. Update your branch:
```bash
git fetch origin && git rebase origin/litellm_internal_staging
# or git merge origin/litellm_internal_staging — whichever matches your workflow
base_branch=$(python3 scripts/default_branch.py --branch) &&
git fetch origin "+refs/heads/$base_branch:refs/remotes/origin/$base_branch" &&
git rebase "origin/$base_branch"
```
2. Re-run `run_migration.py`.

View file

@ -34,7 +34,7 @@ import subprocess
import sys
from pathlib import Path
from types import MappingProxyType
from typing import NamedTuple
from typing import Final, NamedTuple
if sys.version_info >= (3, 11):
import tomllib
@ -42,7 +42,6 @@ else:
import tomli as tomllib
REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_BASE = "origin/litellm_internal_staging"
DEFAULT_BUDGETS: tuple[str, ...] = (
"ruff-strict-budget.json",
"type-discipline-budget.json",
@ -182,12 +181,15 @@ def regressions_for(
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=DEFAULT_BASE)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument("budgets", nargs="*", help="budget files to check")
args = parser.parse_args()
from default_branch import resolve_base_ref
base_ref: Final = resolve_base_ref(args.base, REPO_ROOT)
budgets = args.budgets or list(DEFAULT_BUDGETS)
ref = _merge_base(args.base)
ref = _merge_base(base_ref)
if not _ref_is_commit(ref):
print(
f"FAIL: base ref {ref!r} does not resolve to a commit, so the ratchet has nothing "
@ -204,14 +206,14 @@ def main() -> int:
if base is None and head is None:
continue
if base is None:
print(f"skip {rel}: new file (no base at {args.base} to ratchet against)")
print(f"skip {rel}: new file (no base at {base_ref} to ratchet against)")
continue
checked.append(rel)
regressions.extend(regressions_for(rel, base, head, graduated_selectors(rel)))
if regressions:
print(
f"FAIL: budget limit(s) loosened vs base {args.base} (merge-base {ref[:12]}):"
f"FAIL: budget limit(s) loosened vs base {base_ref} (merge-base {ref[:12]}):"
)
for reg in regressions:
print(f" {reg.budget} {reg.rule}: {reg.detail}")
@ -223,7 +225,7 @@ def main() -> int:
return 1
suffix = f" ({', '.join(checked)})" if checked else ""
print(f"OK: no budget limit increased vs base {args.base}{suffix}")
print(f"OK: no budget limit increased vs base {base_ref}{suffix}")
return 0

61
scripts/default_branch.py Normal file
View file

@ -0,0 +1,61 @@
from __future__ import annotations
import argparse
import os
import subprocess
from pathlib import Path
from typing import Final
def _git(repo_root: Path, *args: str) -> str:
try:
result: Final = subprocess.run(
["git", *args],
cwd=repo_root,
env={**os.environ, "GIT_TERMINAL_PROMPT": "0"},
check=True,
capture_output=True,
text=True,
timeout=60,
)
except (OSError, subprocess.SubprocessError) as exc:
raise SystemExit(
"Cannot verify the base branch against origin. Check remote access, "
"or supply an explicit base ref (--base / BASE_REF). "
f"Git operation failed: {exc}"
) from exc
return result.stdout.strip()
def default_branch(repo_root: Path) -> str:
output: Final = _git(repo_root, "ls-remote", "--symref", "origin", "HEAD")
branches: Final = tuple(
line.removeprefix("ref: refs/heads/").removesuffix("\tHEAD")
for line in output.splitlines()
if line.startswith("ref: refs/heads/") and line.endswith("\tHEAD")
)
if len(branches) != 1:
raise SystemExit("Origin did not advertise a default branch. Supply an explicit base ref (--base / BASE_REF).")
_git(repo_root, "check-ref-format", f"refs/heads/{branches[0]}")
return branches[0]
def resolve_base_ref(base_ref: str | None, repo_root: Path) -> str:
if base_ref:
return base_ref
branch: Final = default_branch(repo_root)
_git(repo_root, "fetch", "--quiet", "origin", f"+refs/heads/{branch}:refs/remotes/origin/{branch}")
return f"origin/{branch}"
def main() -> None:
parser: Final = argparse.ArgumentParser(description="Resolve the live default branch of origin.")
parser.add_argument("--base", help="Explicit comparison ref; skips default-branch discovery")
parser.add_argument("--repo-root", type=Path, default=Path.cwd())
parser.add_argument("--branch", action="store_true", help="Print only the default branch name, without fetching")
args: Final = parser.parse_args()
print(default_branch(args.repo_root) if args.branch else resolve_base_ref(args.base, args.repo_root))
if __name__ == "__main__":
main()

View file

@ -7,7 +7,7 @@
# - anything staged -> scope is the staged files; changed-but-unstaged files
# whose checks were skipped are called out
# - nothing staged -> scope is the working tree's diff against the merge base
# with origin/litellm_internal_staging, untracked files included
# with origin's current default branch, untracked files included
# The per-area checks:
# - litellm/ Python -> `make lint` (test-linting.yml's lint job)
# - tests/e2e Python -> `make lint-e2e-basedpyright` (test-linting.yml's e2e type-check step)
@ -33,8 +33,8 @@ set -eu
# at a time instead of thrashing the machine. The wrapper exports
# LITELLM_GATE_SLOT_HELD, so this re-exec happens exactly once and everything this
# script spawns (make lint, the budget gates) skips its own acquisition.
script_dir=$(python3 -c 'import os, sys; print(os.path.dirname(os.path.realpath(sys.argv[1])))' "$0")
if [ -z "${LITELLM_GATE_SLOT_HELD:-}" ]; then
script_dir=$(python3 -c 'import os, sys; print(os.path.dirname(os.path.realpath(sys.argv[1])))' "$0")
exec python3 "$script_dir/gate_slot_lock.py" "$0" "$@"
fi
@ -65,20 +65,24 @@ untracked=$(git ls-files --others --exclude-standard)
if [ -n "$staged" ]; then
scope=$staged
else
git fetch --quiet origin litellm_internal_staging 2>/dev/null || true
merge_base=$(git merge-base origin/litellm_internal_staging HEAD 2>/dev/null) || {
echo "check: cannot resolve the merge base with origin/litellm_internal_staging." >&2
echo " Fix: git fetch origin litellm_internal_staging" >&2
base_ref=$(python3 "$script_dir/default_branch.py" --base "${BASE_REF:-}") || {
echo "check: FAIL"
exit 1
}
export BASE_REF="$base_ref"
merge_base=$(git merge-base "$base_ref" HEAD 2>/dev/null) || {
echo "check: cannot resolve the merge base with $base_ref." >&2
echo " Fix: fetch the base ref and provide BASE_REF=<ref>" >&2
echo "check: FAIL"
exit 1
}
scope=$(printf '%s\n' "$(git diff --name-only --diff-filter=ACMRD "$merge_base")" "$untracked" | sed '/^$/d' | sort -u)
if [ -z "$scope" ]; then
echo "check: nothing to check (no staged files, no working-tree changes, no branch changes vs origin/litellm_internal_staging)"
echo "check: nothing to check (no staged files, no working-tree changes, no branch changes vs $base_ref)"
echo "check: PASS"
exit 0
fi
echo "check: nothing staged; scoping to the working tree's diff against the merge base with origin/litellm_internal_staging:"
echo "check: nothing staged; scoping to the working tree's diff against the merge base with $base_ref:"
printf '%s\n' "$scope" | sed 's/^/ /'
fi

View file

@ -24,7 +24,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
STRICT_CONFIG = REPO_ROOT / "ruff-strict.toml"
BUDGET_PATH = REPO_ROOT / "ruff-strict-budget.json"
TARGET = "litellm"
DEFAULT_BASE = "origin/litellm_internal_staging"
_HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
@ -193,7 +192,7 @@ def ratcheted_budget(budget: dict, current: dict, base: dict) -> dict:
}
def cmd_update(base_ref: str = DEFAULT_BASE) -> None:
def cmd_update(base_ref: str) -> None:
"""Ratchet each rule's limit down by the violations this branch fixed.
The working-tree count is compared against a ruff pass over a detached
@ -212,13 +211,15 @@ def cmd_update(base_ref: str = DEFAULT_BASE) -> None:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=DEFAULT_BASE)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
from default_branch import resolve_base_ref
from gate_slot_lock import held_slot
base_ref: Final = resolve_base_ref(args.base, REPO_ROOT)
with held_slot():
cmd_update(args.base) if args.update else cmd_check(args.base)
cmd_update(base_ref) if args.update else cmd_check(base_ref)
if __name__ == "__main__":

View file

@ -14,7 +14,7 @@ immediately. ``--update`` ratchets a limit down by the violations fixed relative
to ``--base``, so the ceilings only ever fall. Base counts are measured with the
*current* checker, so a rule introduced on this branch is counted at the base too
and ratchets like every other one. The ratchet runs as a scheduled automation
against litellm_internal_staging, not on PR branches, so concurrent PRs never
against the repository's default branch, not on PR branches, so concurrent PRs never
race to edit the same limit.
The deliberate difference from its sibling: this gate has no headroom anywhere.
@ -43,7 +43,6 @@ REPO_ROOT: Final = Path(__file__).resolve().parent.parent
CHECKER: Final = REPO_ROOT / "scripts" / "check_test_quality.py"
BUDGET_PATH: Final = REPO_ROOT / "test-quality-budget.json"
TARGET: Final = "tests"
DEFAULT_BASE: Final = "origin/litellm_internal_staging"
TERMINATION_SIGNALS: Final = (signal.SIGTERM, signal.SIGHUP)
_HUNK: Final = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@", re.MULTILINE)
@ -240,7 +239,7 @@ def ratcheted_budget(
})
def cmd_update(base_ref: str = DEFAULT_BASE) -> None:
def cmd_update(base_ref: str) -> None:
"""Ratchet each rule's limit down by the violations this branch fixed."""
budget: Final = json.loads(BUDGET_PATH.read_text())
base_point: Final = resolve_base_point(base_ref)
@ -264,19 +263,20 @@ def cmd_seed() -> None:
def main() -> None:
parser: Final = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=DEFAULT_BASE)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument("--update", action="store_true")
parser.add_argument("--seed", action="store_true")
args: Final = parser.parse_args()
from default_branch import resolve_base_ref
from gate_slot_lock import held_slot
with held_slot():
if args.seed:
cmd_seed()
elif args.update:
cmd_update(args.base)
cmd_update(resolve_base_ref(args.base, REPO_ROOT))
else:
cmd_check(args.base)
cmd_check(resolve_base_ref(args.base, REPO_ROOT))
if __name__ == "__main__":

View file

@ -71,7 +71,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
BUDGET_PATH = REPO_ROOT / "basedpyright-code-budget.json"
PYRIGHT_CONFIG = REPO_ROOT / "pyrightconfig.json"
UV_LOCK = REPO_ROOT / "uv.lock"
DEFAULT_BASE = "origin/litellm_internal_staging"
CACHE_FILE_PREFIX = "basedpyright-base-"
CACHE_KEEP_ENTRIES = 8
ARTIFACT_NAME_PREFIX = "basedpyright-counts-"
@ -578,7 +577,7 @@ def ratcheted_budget(
}
def cmd_update(current: Mapping[str, int], base_ref: str = DEFAULT_BASE) -> None:
def cmd_update(current: Mapping[str, int], base_ref: str) -> None:
"""Ratchet each rule's limit down by the errors this branch fixed.
`current` is the working-tree count; the reference count comes
@ -666,12 +665,14 @@ def cmd_check(head: Mapping[str, int], base_ref: str) -> None:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=DEFAULT_BASE)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument("--update", action="store_true")
parser.add_argument("--emit-counts-dir", type=Path)
args = parser.parse_args()
from default_branch import resolve_base_ref
from gate_slot_lock import held_slot
base_ref: Final = None if args.emit_counts_dir is not None else resolve_base_ref(args.base, REPO_ROOT)
with held_slot():
ensure_typecheck_env()
head = count_basedpyright(run_basedpyright())
@ -679,10 +680,8 @@ def main() -> None:
cmd_emit_counts(
head, args.emit_counts_dir, _run(["git", "rev-parse", "HEAD"]).strip()
)
elif args.update:
cmd_update(head, args.base)
else:
cmd_check(head, args.base)
elif base_ref is not None:
cmd_update(head, base_ref) if args.update else cmd_check(head, base_ref)
if __name__ == "__main__":

View file

@ -44,7 +44,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent
CHECKER = REPO_ROOT / "scripts" / "check_type_discipline.py"
BUDGET_PATH = REPO_ROOT / "type-discipline-budget.json"
TARGET = "litellm"
DEFAULT_BASE = "origin/litellm_internal_staging"
_HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
_LINE = re.compile(r"^(?P<file>.+?):(?P<line>\d+): (?P<code>LIT\d+) ")
@ -239,7 +238,7 @@ def _base_budget_rules(base_point: str) -> frozenset:
return frozenset(json.loads(proc.stdout))
def cmd_update(base_ref: str = DEFAULT_BASE) -> None:
def cmd_update(base_ref: str) -> None:
"""Ratchet each rule's limit down by the violations this branch fixed.
The working-tree count is compared against a checker pass over a detached
@ -264,13 +263,15 @@ def cmd_update(base_ref: str = DEFAULT_BASE) -> None:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base", default=DEFAULT_BASE)
parser.add_argument("--base", help="Comparison ref (default: origin's current default branch)")
parser.add_argument("--update", action="store_true")
args = parser.parse_args()
from default_branch import resolve_base_ref
from gate_slot_lock import held_slot
base_ref: Final = resolve_base_ref(args.base, REPO_ROOT)
with held_slot():
cmd_update(args.base) if args.update else cmd_check(args.base)
cmd_update(base_ref) if args.update else cmd_check(base_ref)
if __name__ == "__main__":

View file

@ -79,7 +79,7 @@ Before publishing to the Terraform Registry:
## What a change needs
1. **Land it in `BerriAI/litellm`.** Open a PR against `litellm_internal_staging` with the source change and a `CHANGELOG.md` entry under `[Unreleased]`. CI runs `gofmt`, `go vet`, build, tests and the endpoint-drift audit. A change that breaks existing configurations or state must say so in the changelog: the version number cannot signal it any more
1. **Land it in `BerriAI/litellm`.** Open a PR against the repository's current default branch with the source change and a `CHANGELOG.md` entry under `[Unreleased]`. CI runs `gofmt`, `go vet`, build, tests and the endpoint-drift audit. A change that breaks existing configurations or state must say so in the changelog: the version number cannot signal it any more
2. **Wait for the next LiteLLM release.** The nightly dev release carries it within a day; it reaches a stable version on the next stable cut
3. **Verify** (optional): the version appears at https://registry.terraform.io/providers/BerriAI/litellm and https://github.com/BerriAI/terraform-provider-litellm/releases. If the tag is on the mirror but there is no release, the goreleaser run failed: https://github.com/BerriAI/terraform-provider-litellm/actions

View file

@ -0,0 +1,212 @@
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Final
import pytest
ROOT: Final = Path(__file__).resolve().parents[2]
def _git(repo: Path, *args: str) -> str:
return subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True, text=True).stdout.strip()
def _commit(repo: Path, message: str) -> None:
_git(repo, "add", ".")
_git(repo, "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-qm", message)
@pytest.fixture
def remote_and_clone(tmp_path: Path) -> tuple[Path, Path]:
seed: Final = tmp_path / "seed"
seed.mkdir()
_git(seed, "init", "-q", "-b", "litellm_internal_staging")
(seed / "scripts").mkdir()
for name in (
"default_branch.py",
"budget_ratchet_check.py",
"ruff_strict_gate.py",
"type_discipline_gate.py",
"test_quality_gate.py",
"type_check_gate.py",
"gate_slot_lock.py",
):
shutil.copyfile(ROOT / "scripts" / name, seed / "scripts" / name)
shutil.copyfile(ROOT / "Makefile", seed / "Makefile")
(seed / "litellm").mkdir()
(seed / "litellm" / "example.py").write_text("value = 0\n")
(seed / "ruff-strict-budget.json").write_text('{"C901": {"limit": 1}}\n')
_commit(seed, "staging base")
_git(seed, "checkout", "-qb", "main")
(seed / "litellm" / "example.py").write_text("value = 1\n")
(seed / "ruff-strict-budget.json").write_text('{"C901": {"limit": 0}}\n')
_commit(seed, "main base")
remote: Final = tmp_path / "remote.git"
_git(tmp_path, "clone", "-q", "--bare", str(seed), str(remote))
_git(remote, "symbolic-ref", "HEAD", "refs/heads/litellm_internal_staging")
repo: Final = tmp_path / "clone"
_git(tmp_path, "clone", "-q", "--single-branch", str(remote), str(repo))
return remote, repo
def _resolve(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(ROOT / "scripts" / "default_branch.py"), *args],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
def _make(repo: Path, target: str, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["make", target, "LINT_DEP_INSTALL=", "LINT_DEP_BASE=", *args],
cwd=repo,
capture_output=True,
text=True,
check=False,
env={key: value for key, value in os.environ.items() if key != "BASE_REF"},
)
def test_existing_single_branch_clone_follows_remote_switch(remote_and_clone: tuple[Path, Path]) -> None:
remote, repo = remote_and_clone
before: Final = _resolve(repo)
assert before.returncode == 0, before.stderr
assert before.stdout.strip() == "origin/litellm_internal_staging"
_git(remote, "symbolic-ref", "HEAD", "refs/heads/main")
after: Final = _resolve(repo)
assert after.returncode == 0, after.stderr
assert after.stdout.strip() == "origin/main"
assert _git(repo, "rev-parse", "origin/main") == _git(remote, "rev-parse", "main")
assert _git(repo, "symbolic-ref", "refs/remotes/origin/HEAD").endswith("/litellm_internal_staging")
@pytest.mark.parametrize("missing_head", [False, True])
def test_unverifiable_default_never_uses_cached_head(
remote_and_clone: tuple[Path, Path],
missing_head: bool,
) -> None:
remote, repo = remote_and_clone
if missing_head:
_git(remote, "symbolic-ref", "HEAD", "refs/heads/missing")
else:
_git(repo, "remote", "set-url", "origin", str(remote / "missing"))
result: Final = _resolve(repo)
assert result.returncode != 0
assert not result.stdout
assert "explicit base ref" in result.stderr
checked: Final = _make(repo, "lint-format-check-changed")
assert checked.returncode != 0
assert "No changed" not in checked.stdout
@pytest.mark.parametrize("base_ref", ["HEAD", "origin/litellm_internal_staging"])
def test_explicit_base_works_without_remote_access(
remote_and_clone: tuple[Path, Path],
base_ref: str,
) -> None:
remote, repo = remote_and_clone
_git(repo, "remote", "set-url", "origin", str(remote / "missing"))
result: Final = _resolve(repo, "--base", base_ref)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == base_ref
checked: Final = _make(repo, "lint-format-check-changed", f"BASE_REF={base_ref}")
assert checked.returncode == 0, checked.stderr
assert "No changed litellm Python files" in checked.stdout
def test_budget_ratchet_compares_against_new_default(remote_and_clone: tuple[Path, Path]) -> None:
remote, repo = remote_and_clone
_git(remote, "symbolic-ref", "HEAD", "refs/heads/main")
resolved: Final = _resolve(repo)
assert resolved.returncode == 0, resolved.stderr
_git(repo, "checkout", "-qb", "litellm_feature", "origin/main")
(repo / "ruff-strict-budget.json").write_text('{"C901": {"limit": 1}}\n')
command: Final = [sys.executable, "scripts/budget_ratchet_check.py"]
checked: Final = subprocess.run(command, cwd=repo, capture_output=True, text=True, check=False)
assert checked.returncode == 1
assert "limit raised 0 -> 1" in checked.stdout
assert "base origin/main" in checked.stdout
overridden: Final = subprocess.run(
[*command, "--base", "origin/litellm_internal_staging"],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert overridden.returncode == 0, overridden.stdout + overridden.stderr
def _freshness(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
sys.executable,
"-c",
"import sys; from pathlib import Path; "
"from ci_cd.run_migration import _check_branch_freshness; "
"_check_branch_freshness(Path(sys.argv[1]), sys.argv[2] if len(sys.argv) > 2 else None)",
str(repo),
*args,
],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
def test_migration_freshness_refuses_stale_branch_after_switch(remote_and_clone: tuple[Path, Path]) -> None:
remote, repo = remote_and_clone
before: Final = _freshness(repo)
assert before.returncode == 0, before.stderr
assert "Branch freshness OK" in before.stdout
_git(remote, "symbolic-ref", "HEAD", "refs/heads/main")
after: Final = _freshness(repo)
assert after.returncode == 3
assert "1 commit(s) behind origin/main" in after.stderr
overridden: Final = _freshness(repo, "litellm_internal_staging")
assert overridden.returncode == 0, overridden.stderr
_git(repo, "merge", "--ff-only", "origin/main")
updated: Final = _freshness(repo)
assert updated.returncode == 0, updated.stderr
assert "up to date with origin/main" in updated.stdout
def test_migration_freshness_refuses_unavailable_remote(remote_and_clone: tuple[Path, Path]) -> None:
remote, repo = remote_and_clone
_git(repo, "remote", "set-url", "origin", str(remote / "missing"))
result: Final = _freshness(repo)
assert result.returncode == 3
assert "Could not discover origin's default branch" in result.stderr
explicit: Final = _freshness(repo, "litellm_internal_staging")
assert explicit.returncode == 3
assert "git fetch origin litellm_internal_staging" in explicit.stderr
@pytest.mark.parametrize(
"gate",
[
"budget_ratchet_check",
"ruff_strict_gate",
"type_discipline_gate",
"test_quality_gate",
"type_check_gate",
],
)
def test_each_gate_refuses_an_unverifiable_default(remote_and_clone: tuple[Path, Path], gate: str) -> None:
remote, repo = remote_and_clone
_git(repo, "remote", "set-url", "origin", str(remote / "missing"))
result: Final = subprocess.run(
[sys.executable, f"scripts/{gate}.py"],
cwd=repo,
capture_output=True,
text=True,
check=False,
)
assert result.returncode != 0
assert "Cannot verify the base branch against origin" in result.stderr

View file

@ -159,12 +159,12 @@ def _commit_all(repo: Path, message: str) -> None:
)
def _set_base_ref(repo: Path) -> None:
subprocess.run(
["git", "update-ref", "refs/remotes/origin/litellm_internal_staging", "HEAD"],
cwd=repo,
check=True,
)
def _set_base_ref(repo: Path, branch: str = "litellm_internal_staging") -> None:
remote = repo.parent / "remote.git"
subprocess.run(["git", "clone", "-q", "--bare", str(repo), str(remote)], check=True)
subprocess.run(["git", "update-ref", f"refs/heads/{branch}", "HEAD"], cwd=remote, check=True)
subprocess.run(["git", "symbolic-ref", "HEAD", f"refs/heads/{branch}"], cwd=remote, check=True)
subprocess.run(["git", "remote", "add", "origin", str(remote)], cwd=repo, check=True)
def _stage_file(repo: Path, relative: str, body: str) -> None:
@ -174,10 +174,11 @@ def _stage_file(repo: Path, relative: str, body: str) -> None:
subprocess.run(["git", "add", relative], cwd=repo, check=True)
def test_nothing_staged_scopes_to_working_tree_diff_and_runs_checks(tmp_path: Path) -> None:
@pytest.mark.parametrize("branch", ["litellm_internal_staging", "main"])
def test_nothing_staged_scopes_to_working_tree_diff_and_runs_checks(tmp_path: Path, branch: str) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo)
_set_base_ref(repo, branch)
(repo / "litellm" / "foo.py").write_text("x = 2\n")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 0, proc.stdout + proc.stderr
@ -261,8 +262,8 @@ def test_nothing_staged_without_a_base_ref_fails_with_a_fetch_hint(tmp_path: Pat
_commit_all(repo, "base")
proc = _run(repo, bin_dir, {})
assert proc.returncode == 1
assert "cannot resolve the merge base" in proc.stdout
assert "git fetch origin litellm_internal_staging" in proc.stdout
assert "Cannot verify the base branch against origin" in proc.stdout
assert "explicit base ref" in proc.stdout
assert "check: FAIL" in proc.stdout
@ -622,3 +623,28 @@ def test_failing_run_ends_with_a_fail_verdict(tmp_path: Path) -> None:
assert proc.returncode == 1
assert "check: FAIL" in proc.stdout
assert "check: PASS" not in proc.stdout
def test_explicit_base_scopes_offline_without_a_remote(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
(repo / "litellm" / "foo.py").write_text("x = 2\n")
proc = _run(repo, bin_dir, {"BASE_REF": "HEAD"})
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "merge base with HEAD" in proc.stdout
assert "linting Python" in proc.stdout
def test_symlinked_hook_can_resolve_default_branch(tmp_path: Path) -> None:
repo, bin_dir = _sandbox(tmp_path)
_commit_all(repo, "base")
_set_base_ref(repo, "main")
hook = repo / ".git" / "hooks" / "pre-commit"
hook.symlink_to(SCRIPT)
proc = subprocess.run(
[str(hook)], cwd=repo, capture_output=True, text=True,
env=_env(repo, bin_dir, {}), timeout=120,
)
assert proc.returncode == 0, proc.stdout + proc.stderr
assert "no branch changes vs origin/main" in proc.stdout