diff --git a/.github/workflows/check-root-file-count.yml b/.github/workflows/check-root-file-count.yml new file mode 100644 index 00000000000..a862b1bfc75 --- /dev/null +++ b/.github/workflows/check-root-file-count.yml @@ -0,0 +1,36 @@ +name: Check Root File Count + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_staging + - "litellm_**" + merge_group: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + check-root-file-count: + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + MAX_ROOT_FILES: "44" + 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: Check root file count + run: python scripts/check_root_file_count.py "$MAX_ROOT_FILES" diff --git a/scripts/check_root_file_count.py b/scripts/check_root_file_count.py new file mode 100644 index 00000000000..630b388fd34 --- /dev/null +++ b/scripts/check_root_file_count.py @@ -0,0 +1,60 @@ +import subprocess +import sys +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class RootFileCheck: + root_files: tuple[str, ...] + max_root_files: int + + @property + def count(self) -> int: + return len(self.root_files) + + @property + def within_limit(self) -> bool: + return self.count <= self.max_root_files + + +def root_files_of(tracked_files: tuple[str, ...]) -> tuple[str, ...]: + return tuple(path for path in tracked_files if "/" not in path) + + +def list_tracked_files() -> tuple[str, ...]: + completed = subprocess.run( + ["git", "ls-files", "-z"], + capture_output=True, + check=True, + text=True, + ) + return tuple(path for path in completed.stdout.split("\0") if path) + + +def report(check: RootFileCheck) -> str: + if check.within_limit: + return f"Root file count OK: {check.count} tracked file(s) <= limit {check.max_root_files}" + listing = "\n".join(f" {path}" for path in sorted(check.root_files)) + return ( + f"::error::Too many files in the repository root: {check.count} tracked " + f"file(s), limit is {check.max_root_files}\n" + f"{listing}\n" + "Move new files into an appropriate subdirectory instead of the repo root. " + "If a new root file is genuinely required, raise MAX_ROOT_FILES in " + ".github/workflows/check-root-file-count.yml in the same PR so the bump is " + "reviewed" + ) + + +def main(argv: tuple[str, ...]) -> int: + max_root_files = int(argv[1]) + check = RootFileCheck( + root_files=root_files_of(list_tracked_files()), + max_root_files=max_root_files, + ) + print(report(check)) + return 0 if check.within_limit else 1 + + +if __name__ == "__main__": + sys.exit(main(tuple(sys.argv))) diff --git a/tests/test_litellm/test_check_root_file_count.py b/tests/test_litellm/test_check_root_file_count.py new file mode 100644 index 00000000000..05808ced3f1 --- /dev/null +++ b/tests/test_litellm/test_check_root_file_count.py @@ -0,0 +1,40 @@ +import importlib.util +from pathlib import Path + +_MODULE_PATH = Path(__file__).resolve().parents[2] / "scripts" / "check_root_file_count.py" +_spec = importlib.util.spec_from_file_location("check_root_file_count", _MODULE_PATH) +checker = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(checker) + + +def test_root_files_of_keeps_only_top_level_paths(): + tracked = ( + "README.md", + ".gitignore", + "litellm/main.py", + "scripts/check_root_file_count.py", + "docs/my-website/sidebars.js", + ) + assert checker.root_files_of(tracked) == ("README.md", ".gitignore") + + +def test_count_equal_to_limit_is_within_limit(): + check = checker.RootFileCheck(root_files=("a", "b", "c"), max_root_files=3) + assert check.count == 3 + assert check.within_limit is True + + +def test_count_over_limit_fails_and_lists_offenders(): + check = checker.RootFileCheck(root_files=("zzz.sh", "aaa.py", "bbb.json"), max_root_files=2) + assert check.within_limit is False + message = checker.report(check) + assert "::error::" in message + assert message.index("aaa.py") < message.index("bbb.json") < message.index("zzz.sh") + assert "MAX_ROOT_FILES" in message + + +def test_report_ok_message_when_within_limit(): + check = checker.RootFileCheck(root_files=("a", "b"), max_root_files=44) + message = checker.report(check) + assert "OK" in message + assert "::error::" not in message