Skip to content

ci: gate CircleCI jobs on changed paths - #32080

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_ci_path_based_gating
Jul 5, 2026
Merged

ci: gate CircleCI jobs on changed paths#32080
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_ci_path_based_gating

Conversation

@ryan-crabbe-berri

@ryan-crabbe-berri ryan-crabbe-berri commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

The gating decision is a pure function that we unit test, plus a local end to end run of the real script against a throwaway git repo (a stub stands in for circleci-agent so the halt path is observable)

$ python -m pytest tests/test_litellm/test_circleci_path_filter.py -q
17 passed

--- not a PR (fail-open) ---
path-filter[backend]: running job (not a pull request)
--- docs-only PR, backend (expect halt) ---
  README.md
path-filter[backend]: only unrelated (docs/client) changes detected; halting job as successful
[stub] circleci-agent step halt
--- docs-only PR, client (expect halt) ---
  README.md
path-filter[client]: only unrelated (docs/client) changes detected; halting job as successful
[stub] circleci-agent step halt
--- backend PR, backend (expect run) ---
  README.md
  litellm_core.py
path-filter[backend]: running job (backend-relevant changes detected)
--- backend PR, client (expect run, backend runs both) ---
path-filter[client]: running job (client-relevant changes detected)

This PR itself touches .circleci/ and tests/, which count as backend, so every job runs here as expected; a docs-only or ui/-only PR is where the skips kick in

Type

🚄 Infrastructure

Changes

Previously every job in the CircleCI build_and_test workflow ran on every PR regardless of what changed. Each job now begins, right after checkout, with a skip_if_unrelated_changes step that resolves the PR's merge base, diffs it against HEAD, and halts the job as successful when the change is not relevant to that job's category. Docs-only PRs (*.md, *.mdx, docs/) run nothing, ui/-only PRs run just the four frontend jobs (ui_build, ui_unit_tests, e2e_ui_testing, e2e_ui_testing_server_root_path), and any backend change runs both the backend and frontend jobs

The logic is split so the risky part is testable. .circleci/scripts/classify_changes.sh is a pure mapping from a changed-file list on stdin plus a category to run/skip, and .circleci/scripts/path_filter.sh wraps it with the git plumbing. The wrapper fails open (runs the job) on anything uncertain: a non-PR pipeline, an unresolvable merge base, or a git error, so we never skip real tests by accident. It halts with circleci-agent step halt, which reports the job as green, so this plays nicely with branch-protection required checks

The Windows install smoke job is deliberately left ungated so we don't have to run the bash gate under PowerShell; it is a single cheap job and keeping it always-on avoids cross-platform fragility. It can be gated in a follow-up if desired


Note

Medium Risk
Incorrect path gating could skip required CI on real code changes; mitigated by fail-open behavior and dedicated regression tests, but misclassification would still be a process risk until caught.

Overview
CircleCI jobs no longer always run the full test matrix on every PR. After checkout, most jobs invoke a new skip_if_unrelated_changes step that diffs the PR against a resolved merge base and halts the job as successful when changes are not relevant.

Docs-only PRs (docs/, *.md, *.mdx) skip both backend and client jobs. ui/-only PRs run the four client jobs (ui_build, ui_unit_tests, e2e_ui_testing, e2e_ui_testing_server_root_path with category: client) but skip backend jobs. Any non-doc backend path runs backend jobs and also client jobs (backend changes are treated as needing UI coverage).

The decision logic lives in .circleci/scripts/classify_changes.sh (stdin file list → run/skip) and .circleci/scripts/path_filter.sh (git merge-base, PR detection, circleci-agent step halt). The wrapper fails open (runs the job) when not a PR, merge base cannot be resolved, git errors occur, or the classifier fails.

using_litellm_on_windows is intentionally left ungated. tests/test_litellm/test_circleci_path_filter.py locks the classification contract and end-to-end halt vs run behavior.

Reviewed by Cursor Bugbot for commit 50945cc. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Chores

    • Improved CI job handling so unrelated changes no longer trigger every build.
    • Added smarter change detection for client and backend paths.
  • Tests

    • Added coverage for path-based CI behavior, including client-only, backend-only, and docs-only changes.
    • Verified jobs continue safely when change detection cannot complete.

Every CircleCI job used to run on every PR. Now each job starts with a
lightweight `skip_if_unrelated_changes` step that inspects the PR diff and
halts the job as successful when nothing relevant changed. Docs-only PRs
(*.md, *.mdx, docs/) run nothing, UI-only PRs (ui/) run just the frontend
jobs, and any backend change still runs both the backend and frontend jobs.

The decision logic lives in .circleci/scripts/classify_changes.sh (pure,
reads the changed-file list on stdin) so it can be unit tested, while
path_filter.sh handles the git plumbing and fails open (runs the job) on
any uncertainty such as a missing merge base or a non-PR pipeline. Halting
via `circleci-agent step halt` keeps the job green, so required status
checks are never left pending. The Windows smoke job is intentionally left
ungated to avoid cross-platform shell fragility
@ryan-crabbe-berri
ryan-crabbe-berri requested a review from a team July 3, 2026 21:29
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds change-based job gating to the CircleCI build_and_test workflow so that docs-only and UI-only PRs no longer trigger backend test jobs. The implementation is split into a pure classifier (classify_changes.sh) and a git-plumbing wrapper (path_filter.sh) that resolves the merge base, diffs against HEAD, and halts the job as successful when no relevant files changed.

  • path_filter.sh wraps all uncertainty with explicit fail-open paths: non-PR pipelines, unresolvable merge bases, git errors, and classifier failures all call run_full (exits 0 with the job continuing) rather than halting. The two previously raised issues — an unguarded classifier call that could silently halt and a pipefail-unsafe logging pipeline — are both fixed in this revision (|| run_full "classify_changes.sh failed" and || true respectively).
  • classify_changes.sh implements the routing rules as a pure bash case statement: ui/* → client, docs/*/*.md/*.mdx → ignored, everything else → backend. Case ordering ensures markdown files under ui/ are counted as client rather than docs.
  • Tests lock in 17 unit-level classification cases plus 4 end-to-end integration tests (including a regression test for the fail-open-on-classifier-error path), all using local git repos with no real network calls.

Confidence Score: 5/5

Safe to merge. The fail-open contract is sound, both previously flagged issues are correctly fixed, and the test suite covers the critical edge cases.

The two previously flagged defects — the unguarded classifier call that could silently halt jobs when the script errored, and the pipefail-unsafe logging pipe — are both properly addressed. Every uncertain path (non-PR, unresolvable merge base, git failure, classifier failure) now calls run_full and exits 0. A dedicated regression test drives a deliberately broken classifier and asserts the job runs rather than halts.

No files require special attention.

Important Files Changed

Filename Overview
.circleci/scripts/path_filter.sh New wrapper script: resolves merge base, diffs against HEAD, and calls classify_changes.sh. Both previously flagged issues (non-fail-open classifier call, pipefail-unsafe logging) are correctly fixed with `
.circleci/scripts/classify_changes.sh Pure classification function mapping changed file list (stdin) + category to run/skip. Bash case pattern ordering is correct: ui/* is checked before *.md/*.mdx so markdown under ui/ counts as client, not docs. Unknown category falls through to echo run (fail-open).
tests/test_litellm/test_circleci_path_filter.py 17 tests covering the classification matrix (14 parametrized + 3 edge cases) plus 4 end-to-end path_filter.sh tests including the new regression test for classifier failures. Tests use only local git repos and subprocess calls — no real network calls, complying with the repo's mock-only test rule.
.circleci/config.yml Adds the skip_if_unrelated_changes reusable command and inserts it as the first post-checkout step in ~40 jobs. Backend jobs use the default category: backend; the four UI jobs (ui_build, ui_unit_tests, e2e_ui_testing, e2e_ui_testing_server_root_path) correctly use category: client. The Windows smoke job is intentionally left ungated per the PR description.

Reviews (2): Last reviewed commit: "fix(ci): keep path filter fail-open when..." | Re-trigger Greptile

Comment thread .circleci/scripts/path_filter.sh Outdated
Comment thread .circleci/scripts/path_filter.sh Outdated
Guard the classify_changes.sh invocation with `|| run_full` so a broken or
non-zero classifier runs the job instead of falling through to a silent
halt, and mark the advisory logging pipe best-effort with `|| true`. Add
path_filter.sh regression tests covering the docs-only halt, backend run,
non-PR fail-open, and classifier-failure fail-open paths
@ryan-crabbe-berri

Copy link
Copy Markdown
Contributor Author

@greptileai I've addressed both findings in 50945cc. The classifier invocation is now guarded with || run_full so it fails open, the advisory logging pipe is marked best-effort, and there are new regression tests covering the fail-open paths (including a deliberately-failing classifier). Please take another look

@mateo-berri mateo-berri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM; thanks!

@mateo-berri

Copy link
Copy Markdown
Contributor

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mateo-berri

Copy link
Copy Markdown
Contributor

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 50945cc. Configure here.

if [ -z "$merge_base" ] || git merge-base --is-ancestor "$merge_base" "$candidate" 2>/dev/null; then
merge_base="$candidate"
fi
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge base picks newest candidate

Medium Severity

When resolving the PR diff, the script keeps the newest merge base among main, litellm_internal_staging, and litellm_oss_staging instead of the base branch the PR targets. That can shrink git diff so backend edits already on the branch disappear from the changed-file list while later docs-only commits remain, and backend jobs halt as successful even though the PR still carries backend changes relative to the target branch.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 50945cc. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds CircleCI scripts classify_changes.sh and path_filter.sh that determine whether a job's changes are relevant to backend or client categories based on Git diff against a computed merge base, and wires a new skip_if_unrelated_changes command into numerous backend and UI jobs in config.yml, with accompanying regression tests.

Changes

CircleCI path-based job gating

Layer / File(s) Summary
Change classification logic
.circleci/scripts/classify_changes.sh
New script reads changed file paths from stdin, flags client (ui/*) vs backend paths, and outputs run/skip decisions per category.
Merge-base diff and halt mechanism
.circleci/scripts/path_filter.sh
New script resolves a merge base across candidate branches, diffs changed files against it, invokes classify_changes.sh, and halts the CircleCI step or lets the job continue.
Command definition and job wiring
.circleci/config.yml
Adds skip_if_unrelated_changes reusable command (category param, default backend) and inserts it after checkout in many backend jobs and, with category: client, in UI-related jobs (ui_build, ui_unit_tests, e2e_ui_testing, e2e_ui_testing_server_root_path).
Regression test suite
tests/test_litellm/test_circleci_path_filter.py
New tests cover classify_changes.sh decisions for docs/client/backend scenarios and edge cases, plus path_filter.sh integration tests using temporary git repos for PR/non-PR and classifier-failure scenarios.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CircleCIJob
  participant PathFilterSh
  participant Git
  participant ClassifyChangesSh
  CircleCIJob->>PathFilterSh: run with category argument
  PathFilterSh->>Git: fetch candidate base branches
  Git-->>PathFilterSh: merge base commit
  PathFilterSh->>Git: diff HEAD vs merge base
  Git-->>PathFilterSh: changed file list
  PathFilterSh->>ClassifyChangesSh: pipe changed files, category
  ClassifyChangesSh-->>PathFilterSh: run or skip decision
  PathFilterSh->>CircleCIJob: halt step or continue
Loading

Related PRs: None identified

Suggested labels: ci, testing

Suggested reviewers: None identified

🐰 A rabbit sniffs at paths both new and old,
"Backend or client?" the scripts have told,
Merge base found, the diff computed clean,
Skip or run — no wasted CI machine.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: CircleCI jobs are gated by changed paths.
Description check ✅ Passed The description follows the template with checklist, proof, type, and changes, and only optional issue/ticket sections are blank.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch litellm_ci_path_based_gating

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_litellm/test_circleci_path_filter.py (1)

112-129: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the circleci-agent stub setup into a fixture.

The stub-binary creation and env-var wiring in _run_path_filter is straightforward but could become a pytest.fixture to reduce duplication if more integration tests are added later. Not urgent given only four call sites currently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_litellm/test_circleci_path_filter.py` around lines 112 - 129,
Extract the `circleci-agent` stub setup and PATH/env wiring from
`_run_path_filter` into a reusable pytest fixture or helper so future tests can
share it; keep `_run_path_filter` focused on invoking `subprocess.run` for
`path_filter.sh`, and preserve the current `CIRCLE_PULL_REQUEST` behavior when
`is_pr` is toggled.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_litellm/test_circleci_path_filter.py`:
- Around line 112-129: Extract the `circleci-agent` stub setup and PATH/env
wiring from `_run_path_filter` into a reusable pytest fixture or helper so
future tests can share it; keep `_run_path_filter` focused on invoking
`subprocess.run` for `path_filter.sh`, and preserve the current
`CIRCLE_PULL_REQUEST` behavior when `is_pr` is toggled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1886f074-33c9-4d17-b912-b8187afffe78

📥 Commits

Reviewing files that changed from the base of the PR and between 5f4b9ad and 50945cc.

📒 Files selected for processing (4)
  • .circleci/config.yml
  • .circleci/scripts/classify_changes.sh
  • .circleci/scripts/path_filter.sh
  • tests/test_litellm/test_circleci_path_filter.py

yuneng-berri
yuneng-berri previously approved these changes Jul 5, 2026
@yuneng-berri
yuneng-berri dismissed their stale review July 5, 2026 01:53

dismissing

@yuneng-berri

Copy link
Copy Markdown
Collaborator

Can you confirm if skipped tests count as successful runs?

@mateo-berri

Copy link
Copy Markdown
Contributor

Can you confirm if skipped tests count as successful runs?

yep. on skip the job runs circleci-agent step halt (.circleci/scripts/path_filter.sh:40). see: https://support.circleci.com/hc/en-us/articles/360015562253-Conditionally-End-a-Running-Job-Gracefully <- CircleCI documents as ending the job successfully, so GitHub checks show green

@mateo-berri
mateo-berri merged commit f5438d1 into litellm_internal_staging Jul 5, 2026
127 checks passed
@mateo-berri
mateo-berri deleted the litellm_ci_path_based_gating branch July 5, 2026 02:15
EkkoG pushed a commit to EkkoG/litellm that referenced this pull request Jul 7, 2026
* ci: gate CircleCI jobs on changed paths

Every CircleCI job used to run on every PR. Now each job starts with a
lightweight `skip_if_unrelated_changes` step that inspects the PR diff and
halts the job as successful when nothing relevant changed. Docs-only PRs
(*.md, *.mdx, docs/) run nothing, UI-only PRs (ui/) run just the frontend
jobs, and any backend change still runs both the backend and frontend jobs.

The decision logic lives in .circleci/scripts/classify_changes.sh (pure,
reads the changed-file list on stdin) so it can be unit tested, while
path_filter.sh handles the git plumbing and fails open (runs the job) on
any uncertainty such as a missing merge base or a non-PR pipeline. Halting
via `circleci-agent step halt` keeps the job green, so required status
checks are never left pending. The Windows smoke job is intentionally left
ungated to avoid cross-platform shell fragility

* fix(ci): keep path filter fail-open when classifier errors

Guard the classify_changes.sh invocation with `|| run_full` so a broken or
non-zero classifier runs the job instead of falling through to a silent
halt, and mark the advisory logging pipe best-effort with `|| true`. Add
path_filter.sh regression tests covering the docs-only halt, backend run,
non-PR fail-open, and classifier-failure fail-open paths
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants