From c5ddee6ff8925b3f2f5c7a6e44f38b12ed1eb7d9 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:47:38 -0700 Subject: [PATCH 1/2] ci: use shallow checkout for schema compatibility checks --- .github/workflows/test-linting.yml | 2 +- .github/workflows/test-unit.yml | 29 ++++++++- scripts/check_schema_compatibility.py | 34 +++++++---- .../test_schema_compatibility_cli.py | 60 +++++++++++++++++++ 4 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 tests/code_coverage_tests/test_schema_compatibility_cli.py diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index b9f8d7e7dfb..456e55c134f 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -543,7 +543,7 @@ jobs: 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 + run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_unit_passed_gate.py tests/code_coverage_tests/test_schema_compatibility_cli.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 diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 4d47a882891..afb28d5e264 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -955,13 +955,37 @@ jobs: timeout-minutes: ${{ matrix.shard == 'db-and-spend' && 10 || 3 }} with: persist-credentials: false - fetch-depth: ${{ matrix.shard == 'db-and-spend' && '0' || '1' }} + fetch-depth: 1 - name: Detect relevant changes id: changes timeout-minutes: 2 uses: ./.github/actions/detect-changes + - name: Fetch schema comparison baseline + if: matrix.shard == 'db-and-spend' && steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + CHECKED_OUT_SHA: ${{ github.sha }} + run: | + for attempt in 1 2 3; do + if baseline="$(gh api "repos/${REPO}/compare/${DEFAULT_BRANCH}...${CHECKED_OUT_SHA}?per_page=1" --jq '.merge_base_commit.sha')" \ + && [[ "$baseline" =~ ^[0-9a-f]{40}$ ]] \ + && git fetch --no-tags --depth=1 origin "$baseline"; then + printf 'SCHEMA_BASE_COMMIT=%s\n' "$baseline" >> "$GITHUB_ENV" + exit 0 + fi + if [ "$attempt" -lt 3 ]; then + echo "Schema baseline fetch failed, retrying attempt $((attempt + 1))/3" + sleep "$((attempt * 5))" + fi + done + echo "::error::Could not resolve and fetch the schema baseline after 3 attempts" + exit 1 + - name: Set up Python if: steps.changes.outputs.decision != 'skip' timeout-minutes: 3 @@ -994,7 +1018,6 @@ jobs: env: RUST_BRIDGE_ARTIFACT: ${{ needs.rust-bridge.outputs.artifact }} UNIT_SHARD: ${{ matrix.shard }} - SCHEMA_BASE_REF: origin/${{ github.event.repository.default_branch }} run: | diff -u model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json if [ -z "$RUST_BRIDGE_ARTIFACT" ]; then @@ -1007,7 +1030,7 @@ jobs: fi uv run --no-sync python -c 'import os, sys; print(sys.version); assert f"{sys.version_info.major}.{sys.version_info.minor}" == os.environ["UV_PYTHON"]' if [ "$UNIT_SHARD" = "db-and-spend" ]; then - uv run --no-sync python scripts/check_schema_compatibility.py --base-ref "$SCHEMA_BASE_REF" + uv run --no-sync python scripts/check_schema_compatibility.py --base-commit "$SCHEMA_BASE_COMMIT" fi - name: Cache Prisma binaries diff --git a/scripts/check_schema_compatibility.py b/scripts/check_schema_compatibility.py index 898c65dff2c..24e45dbd915 100644 --- a/scripts/check_schema_compatibility.py +++ b/scripts/check_schema_compatibility.py @@ -7,14 +7,20 @@ import sys from collections.abc import Iterator, Mapping from pathlib import Path from types import MappingProxyType -from typing import Final +from typing import Final, TypeAlias -from pydantic import TypeAdapter +from pydantic import BaseModel, ConfigDict, TypeAdapter -Schema = Mapping[str, Mapping[str, str]] +Schema: TypeAlias = Mapping[str, Mapping[str, str]] MODELS: Final = re.compile(r"^[ \t]*model\s+(\w+)\s*\{(.*?)^[ \t]*\}", re.MULTILINE | re.DOTALL) +class _Options(BaseModel): + model_config = ConfigDict(frozen=True) + base_ref: str | None + base_commit: str | None + + def parse_model_fields(schema: str) -> Schema: def fields(body: str) -> Mapping[str, str]: parts: Final = tuple(line.split() for line in body.splitlines()) @@ -54,12 +60,20 @@ def check_breaking_changes(old_schema: Schema, new_schema: Schema) -> tuple[str, def main() -> int: parser: Final = argparse.ArgumentParser() - parser.add_argument("--base-ref", required=True) - options: Final = parser.parse_args() - base_ref: Final = TypeAdapter(str).validate_python(options.base_ref) - base: Final = subprocess.run( - ("git", "merge-base", "HEAD", base_ref), capture_output=True, text=True, check=True - ).stdout.strip() + baseline: Final = parser.add_mutually_exclusive_group(required=True) + baseline.add_argument("--base-ref") + baseline.add_argument("--base-commit") + options: Final = _Options.model_validate(vars(parser.parse_args())) + base: Final = ( + options.base_commit + if options.base_commit is not None + else subprocess.run( + ("git", "merge-base", "HEAD", TypeAdapter(str).validate_python(options.base_ref)), + capture_output=True, + text=True, + check=True, + ).stdout.strip() + ) previous: Final = subprocess.run( ("git", "show", f"{base}:schema.prisma"), capture_output=True, text=True, check=True ).stdout @@ -69,7 +83,7 @@ def main() -> int: if changes: sys.stderr.write("\n".join(changes) + "\n") return 1 - sys.stdout.write(f"Schema compatibility passed against merge base {base}\n") + sys.stdout.write(f"Schema compatibility passed against baseline {base}\n") return 0 diff --git a/tests/code_coverage_tests/test_schema_compatibility_cli.py b/tests/code_coverage_tests/test_schema_compatibility_cli.py new file mode 100644 index 00000000000..cf3ffc76431 --- /dev/null +++ b/tests/code_coverage_tests/test_schema_compatibility_cli.py @@ -0,0 +1,60 @@ +import subprocess +import sys +from pathlib import Path +from typing import Final + +import pytest + +CHECKER: Final = Path(__file__).resolve().parents[2] / "scripts/check_schema_compatibility.py" +SCHEMA: Final = "model Item {\n id String @id\n label String?\n}\n" + + +def _git(root: Path, *args: str) -> str: + return subprocess.run( + ("git", "-C", str(root), *args), capture_output=True, text=True, check=True, timeout=30 + ).stdout.strip() + + +@pytest.mark.parametrize("baseline_option", ("--base-ref", "--base-commit")) +@pytest.mark.parametrize(("schema", "expected"), ((SCHEMA, 0), ("model Item {\n id String @id\n}\n", 1))) +def test_schema_check_preserves_break_detection_with_or_without_history( + tmp_path: Path, schema: str, expected: int, baseline_option: str +) -> None: + source: Final = tmp_path / "source" + source.mkdir() + _git(source, "init", "--initial-branch=main") + _git(source, "config", "user.name", "Schema test") + _git(source, "config", "user.email", "schema-test@example.invalid") + _git(source, "config", "commit.gpgsign", "false") + (source / "schema.prisma").write_text(SCHEMA) + _git(source, "add", "schema.prisma") + _git(source, "commit", "-m", "baseline") + baseline: Final = _git(source, "rev-parse", "HEAD") + (source / "schema.prisma").write_text(schema) + _git(source, "add", "schema.prisma") + _git(source, "commit", "--allow-empty", "-m", "candidate") + checkout: Final = tmp_path / "checkout" + _git(tmp_path, "clone", "--depth=1", source.as_uri(), str(checkout)) + _git(checkout, "fetch", "--no-tags", "--depth=1", "origin", baseline) + if baseline_option == "--base-ref": + _git(checkout, "fetch", "--unshallow", "origin", "main") + + result: Final = subprocess.run( + (sys.executable, "-I", str(CHECKER), baseline_option, baseline), + cwd=checkout, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + + assert result.returncode == expected, result.stdout + result.stderr + assert _git(checkout, "rev-parse", "--is-shallow-repository") == ( + "true" if baseline_option == "--base-commit" else "false" + ) + expected_output: Final = ( + f"Schema compatibility passed against baseline {baseline}" + if expected == 0 + else "Breaking: Field Item.label was removed" + ) + assert expected_output in result.stdout + result.stderr From 471a5726e6da1e28a5b8b2c0b87b83fcb1b6ffa8 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:51:41 -0700 Subject: [PATCH 2/2] docs(ci): require shallow checkouts unless history is needed --- .github/AGENTS.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/AGENTS.md diff --git a/.github/AGENTS.md b/.github/AGENTS.md new file mode 100644 index 00000000000..9c8ca6d0db8 --- /dev/null +++ b/.github/AGENTS.md @@ -0,0 +1,9 @@ +# Checkout history + +Use shallow checkouts by default. `actions/checkout` defaults to one commit, so omitting `fetch-depth` is fine. For comparisons, fetch only the required commits with `--no-tags --depth=1` and preserve the existing baseline semantics. Fetching two shallow endpoints does not make `git merge-base` work across missing ancestry + +Use deeper checkout, `fetch-depth: 0`, or `--unshallow` only when the consuming command needs history. For new or changed exceptions, state the consumer and why a targeted fetch cannot preserve its behavior beside that checkout or fetch. Keep bounded retries and timeouts on network operations, and never skip a required check when its baseline is unavailable + +Current exceptions are the depth-two parent diff in `test-linting.yml`, Linear's commit-history scan in `linear-release.yml`, and the historical GitGuardian repository scan in `test-linting.yml`. Do not remove release history or reduce secret-scanning coverage just to make checkout shallow + +Mateo requested this default on 2026-10-27 after a full-history unit checkout timed out before tests started