This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new a218626923b Remove large-PR heuristic from selective checks (#68109)
a218626923b is described below

commit a218626923bfe1fecb2f266a5cf8b41b9f7a4a60
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Jun 6 02:10:52 2026 +0200

    Remove large-PR heuristic from selective checks (#68109)
    
    Selective checks forced the full test matrix whenever a PR touched 25+
    files or changed 500+ lines of production code. This size-based heuristic
    made large but low-risk PRs run the entire CI suite, so drop it.
    
    Full tests are still triggered by the targeted rules (env/API/provider
    file changes, the "full tests needed" label, missing commit ref, etc.).
    The *_PRODUCTION_FILES groups are kept — they still feed the SAST/SCA
    scan target (run_python_scans / run_javascript_scans).
    
    Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
---
 .../src/airflow_breeze/utils/selective_checks.py   |  99 +---------
 dev/breeze/tests/test_selective_checks.py          | 217 ---------------------
 2 files changed, 9 insertions(+), 307 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py 
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 3bdbf9fc985..6bf7b589b1a 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -225,17 +225,16 @@ CI_FILE_GROUP_MATCHES: HashableDict[FileGroupForCi] = 
HashableDict(
         FileGroupForCi.PYTHON_PRODUCTION_FILES: [
             # Production Python source the runtime ships — excludes tests, 
docs,
             # dev tooling, and generated files within those trees. Used by
-            # `run_python_scans` (SAST/SCA target) and the line-threshold check
-            # in `_is_large_enough_pr` to decide whether a PR's diff is large
-            # enough to force the full test matrix.
+            # `run_python_scans` (SAST/SCA target) to decide whether the 
security
+            # scans need to run.
             #
-            # `example_dags/` are illustrative, not shipped runtime code, so a 
large
-            # example-DAG diff must not force the full matrix. They are still 
selected
-            # for their own tests via the broader `ALL_AIRFLOW_PYTHON_FILES` /
-            # `ALL_PROVIDERS_PYTHON_FILES` groups, so excluding them here only 
affects
-            # the line-count gate (and SAST target), not test selection. The
-            # `(?:.*/)?` covers both airflow-core's top-level 
`airflow/example_dags/`
-            # and the nested `providers/<name>/.../example_dags/` layout.
+            # `example_dags/` are illustrative, not shipped runtime code, so 
they
+            # are excluded from the SAST target. They are still selected for 
their
+            # own tests via the broader `ALL_AIRFLOW_PYTHON_FILES` /
+            # `ALL_PROVIDERS_PYTHON_FILES` groups, so excluding them here only
+            # affects the SAST target, not test selection. The `(?:.*/)?` 
covers
+            # both airflow-core's top-level `airflow/example_dags/` and the 
nested
+            # `providers/<name>/.../example_dags/` layout.
             
r"^airflow-core/src/airflow/(?!(?:.*/)?example_dags/)(?!.*/(?:openapi-gen|i18n/locales)/).*\.py$",
             r"^task-sdk/src/airflow/(?!.*_generated\.py$).*\.py$",
             r"^airflow-ctl/src/airflowctl/(?!.*generated\.py$).*\.py$",
@@ -739,8 +738,6 @@ class SelectiveChecks:
         ):
             console_print("[warning]Running full set of tests because 
tests/utils changed[/]")
             return True
-        if self._is_large_enough_pr():
-            return True
         if FULL_TESTS_NEEDED_LABEL in self._pr_labels:
             console_print(
                 "[warning]Full tests needed because "
@@ -749,84 +746,6 @@ class SelectiveChecks:
             return True
         return False
 
-    def _is_large_enough_pr(self) -> bool:
-        """
-        Check if PR is large enough to run full tests.
-
-        Both heuristics — the count of changed files (``FILE_THRESHOLD``) and 
the
-        total lines changed (``LINE_THRESHOLD``) — only consider 
production-code
-        files. Tests, docs, newsfragments, generated files, translations, 
example
-        DAGs, and dev tooling are low-risk: a PR that only touches them, 
however
-        many files or lines, must not force the full test matrix. A 1000-line 
(or
-        40-file) test or docs PR is not the same shape of risk as the same 
churn in
-        scheduler code, and only the latter should trigger the full test 
matrix.
-        """
-        FILE_THRESHOLD = 25
-        LINE_THRESHOLD = 500
-
-        if not self._files:
-            return False
-
-        # Both gates count churn in production code only. We compose the 
existing
-        # `*_PRODUCTION_FILES` and helm groups rather than rolling a bespoke 
pattern
-        # set, so the definition of "production code" stays in lockstep with 
the rest
-        # of CI (e.g. SAST scans targeted by `run_python_scans` /
-        # `run_javascript_scans`). These groups already exclude tests, docs,
-        # generated files, translations, and example DAGs.
-        production_files = list(
-            dict.fromkeys(
-                self._matching_files(FileGroupForCi.PYTHON_PRODUCTION_FILES, 
CI_FILE_GROUP_MATCHES)
-                + 
self._matching_files(FileGroupForCi.JAVASCRIPT_PRODUCTION_FILES, 
CI_FILE_GROUP_MATCHES)
-                + self._matching_files(FileGroupForCi.HELM_FILES, 
CI_FILE_GROUP_MATCHES)
-            )
-        )
-        if not production_files:
-            return False
-
-        files_changed = len(production_files)
-        if files_changed >= FILE_THRESHOLD:
-            console_print(
-                f"[warning]Running full set of tests because PR touches 
{files_changed} "
-                f"production files (≥{FILE_THRESHOLD} threshold)[/]"
-            )
-            return True
-
-        if not self._commit_ref:
-            console_print("[warning]Cannot determine if PR is big enough, 
skipping the check[/]")
-            return False
-
-        try:
-            result = run_command(
-                ["git", "diff", "--numstat", 
f"{self._commit_ref}^...{self._commit_ref}"] + production_files,
-                capture_output=True,
-                text=True,
-                cwd=AIRFLOW_ROOT_PATH,
-                check=False,
-            )
-
-            if result.returncode == 0:
-                total_lines = 0
-                for line in result.stdout.strip().split("\n"):
-                    if line:
-                        parts = line.split("\t")
-                        if len(parts) >= 2:
-                            try:
-                                additions = int(parts[0])
-                                deletions = int(parts[1])
-                                total_lines += additions + deletions
-                            except ValueError:
-                                pass
-                if total_lines >= LINE_THRESHOLD:
-                    console_print(
-                        f"[warning]Running full set of tests because PR 
changes {total_lines} lines "
-                        f"of production code in {len(production_files)} 
file(s)[/]"
-                    )
-                    return True
-        except Exception:
-            pass
-
-        return False
-
     @cached_property
     def python_versions(self) -> list[str]:
         if self.all_versions:
diff --git a/dev/breeze/tests/test_selective_checks.py 
b/dev/breeze/tests/test_selective_checks.py
index 2f6863b8bd4..1df9e4463cb 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -3601,223 +3601,6 @@ def 
test_provider_dependency_bump_check_in_optional_dependencies(mock_run_comman
         ).provider_dependency_bump
 
 
[email protected](
-    ("files", "expected_outputs"),
-    [
-        pytest.param(
-            (
-                "airflow-core/src/airflow/models/dag.py",
-                "airflow-core/src/airflow/models/taskinstance.py",
-                "airflow-core/tests/unit/models/test_dag.py",
-                "task-sdk/src/airflow/sdk/definitions/dag.py",
-                "task-sdk/tests/task_sdk/definitions/test_dag.py",
-            ),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Small PR with 5 files changed",
-        ),
-        pytest.param(
-            tuple(f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(30)),
-            {
-                "full-tests-needed": "true",
-            },
-            id="Large PR with 30 files changed",
-        ),
-        pytest.param(
-            (
-                "uv.lock",
-                "package-lock.json",
-            ),
-            {
-                "full-tests-needed": "false",
-            },
-            id="PR with only lock files changed",
-        ),
-        # The file-count gate, like the line-count gate, only counts production
-        # code. A PR that touches many test, docs, or example-DAG files — and 
no
-        # production code — must not force the full matrix on its file count 
alone.
-        pytest.param(
-            tuple(f"airflow-core/tests/unit/models/test_file{i}.py" for i in 
range(30)),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large test-only PR (30 files) does not trigger full tests",
-        ),
-        pytest.param(
-            tuple(f"airflow-core/docs/page_{i}.rst" for i in range(30)),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large docs-only PR (30 files) does not trigger full tests",
-        ),
-        pytest.param(
-            tuple(f"airflow-core/src/airflow/example_dags/example_{i}.py" for 
i in range(30)),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large example_dags-only PR (30 files) does not trigger full 
tests",
-        ),
-        # A mix below the production-file threshold (20 production + 20 test 
files)
-        # must not trip the file-count gate on the combined count of 40.
-        pytest.param(
-            tuple(
-                [f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(20)]
-                + [f"airflow-core/tests/unit/models/test_file{i}.py" for i in 
range(20)]
-            ),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Mixed PR with 20 production files (of 40) does not trigger on 
file count",
-        ),
-    ],
-)
-def test_large_pr_by_file_count(files, expected_outputs: dict[str, str]):
-    stderr = SelectiveChecks(
-        files=files,
-        commit_ref=NEUTRAL_COMMIT,
-        github_event=GithubEvents.PULL_REQUEST,
-        default_branch="main",
-    )
-    assert_outputs_are_printed(expected_outputs, str(stderr))
-
-
[email protected](
-    ("files", "git_diff_output", "expected_outputs"),
-    [
-        pytest.param(
-            tuple(f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(10)),
-            "\n".join([f"10\t10\tairflow-core/src/airflow/models/file{i}.py" 
for i in range(10)]),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Small PR with 200 lines changed",
-        ),
-        pytest.param(
-            tuple(f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(10)),
-            "\n".join([f"30\t30\tairflow-core/src/airflow/models/file{i}.py" 
for i in range(10)]),
-            {
-                "full-tests-needed": "true",
-            },
-            id="PR with 600 lines changed",
-        ),
-        pytest.param(
-            ("airflow-core/src/airflow/configuration.py",),
-            "500\t500\tairflow-core/src/airflow/configuration.py",
-            {
-                "full-tests-needed": "true",
-            },
-            id="Single large file with 1000 lines",
-        ),
-        pytest.param(
-            tuple(f"airflow-core/tests/unit/models/test_file{i}.py" for i in 
range(10)),
-            
"\n".join([f"100\t100\tairflow-core/tests/unit/models/test_file{i}.py" for i in 
range(10)]),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large test-only PR (2000 lines) does not trigger full tests",
-        ),
-        pytest.param(
-            ("docs/index.rst", 
"airflow-core/docs/security/security_model.rst"),
-            
"600\t600\tdocs/index.rst\n400\t400\tairflow-core/docs/security/security_model.rst",
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large docs-only PR does not trigger full tests",
-        ),
-        pytest.param(
-            (
-                "airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts",
-                "airflow-ctl/src/airflowctl/api/datamodels/generated.py",
-                "task-sdk/src/airflow/sdk/api/datamodels/_generated.py",
-            ),
-            "\n".join(
-                [
-                    
"400\t400\tairflow-core/src/airflow/ui/openapi-gen/queries/queries.ts",
-                    
"400\t400\tairflow-ctl/src/airflowctl/api/datamodels/generated.py",
-                    
"400\t400\ttask-sdk/src/airflow/sdk/api/datamodels/_generated.py",
-                ]
-            ),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Generated-only large PR does not trigger full tests",
-        ),
-        # In mixed PRs the production-file filter narrows the `git diff 
--numstat`
-        # call to the production paths, so the mocked stdout below only 
contains
-        # the production-file rows (mirroring what real git would return for
-        # that filtered argument list).
-        pytest.param(
-            tuple(
-                [f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(5)]
-                + [f"airflow-core/tests/unit/models/test_file{i}.py" for i in 
range(5)]
-            ),
-            "\n".join([f"60\t60\tairflow-core/src/airflow/models/file{i}.py" 
for i in range(5)]),
-            {
-                "full-tests-needed": "true",
-            },
-            id="Mixed PR with 600 production lines triggers (test lines 
excluded but prod >= 500)",
-        ),
-        pytest.param(
-            tuple(
-                [f"airflow-core/src/airflow/models/file{i}.py" for i in 
range(5)]
-                + [f"airflow-core/tests/unit/models/test_file{i}.py" for i in 
range(5)]
-            ),
-            "\n".join([f"20\t20\tairflow-core/src/airflow/models/file{i}.py" 
for i in range(5)]),
-            {
-                "full-tests-needed": "false",
-            },
-            id="Mixed PR with only 200 production lines does not trigger (test 
lines excluded)",
-        ),
-        # A large example-DAG diff in a "plain" provider (not standard/git, 
which
-        # have their own full-tests rule) must NOT force the full matrix. This 
is
-        # the exact shape of apache/airflow#68037.
-        pytest.param(
-            (
-                
"providers/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py",
-            ),
-            
"600\t600\tproviders/common/ai/src/airflow/providers/common/ai/example_dags/example_aip_progress_tracker.py",
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large provider example_dags-only PR does not trigger full 
tests",
-        ),
-        pytest.param(
-            ("airflow-core/src/airflow/example_dags/example_complex.py",),
-            
"600\t600\tairflow-core/src/airflow/example_dags/example_complex.py",
-            {
-                "full-tests-needed": "false",
-            },
-            id="Large airflow-core example_dags-only PR does not trigger full 
tests",
-        ),
-        # Regression guard: a large *non-example* file in the same plain 
provider
-        # must still count as production code and trigger the full matrix.
-        pytest.param(
-            
("providers/arangodb/src/airflow/providers/arangodb/operators/arangodb.py",),
-            
"600\t600\tproviders/arangodb/src/airflow/providers/arangodb/operators/arangodb.py",
-            {
-                "full-tests-needed": "true",
-            },
-            id="Large provider production (non-example) PR still triggers full 
tests",
-        ),
-    ],
-)
-def test_large_pr_by_line_count(files, git_diff_output, expected_outputs: 
dict[str, str]):
-    with patch("airflow_breeze.utils.selective_checks.run_command") as 
mock_run:
-        mock_result = Mock()
-        mock_result.returncode = 0
-        mock_result.stdout = git_diff_output
-        mock_run.return_value = mock_result
-
-        stderr = SelectiveChecks(
-            files=files,
-            commit_ref=NEUTRAL_COMMIT,
-            github_event=GithubEvents.PULL_REQUEST,
-            default_branch="main",
-        )
-        assert_outputs_are_printed(expected_outputs, str(stderr))
-
-
 @patch("airflow_breeze.utils.selective_checks.run_command")
 def test_common_compat_changed_with_next_version_passes(mock_run_command):
     """Test that check passes when common.compat changes and other provider 
has '# use next version'."""

Reply via email to