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'."""