This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new d09396754c9 [v3-4-test] Run only the component's tests on chart and
airflow-ctl release branches (#75621) (#75624)
d09396754c9 is described below
commit d09396754c99f801ba7fd4592589c20a91f141ea
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sun Oct 25 21:12:44 2026 +0100
[v3-4-test] Run only the component's tests on chart and airflow-ctl release
branches (#75621) (#75624)
The chart/ and airflow-ctl/ release branches only ever get backports of the
Helm chart or of airflow-ctl, yet their CI selected tests from the changed
files like main does: a backport that touched shared files ran the whole
core and provider matrix, and helm tests, gated on main, did not run on a
chart branch whose breeze defaults say otherwise.
New test jobs default to not running there, so that a job added on main
does not start running on those branches unnoticed.
(cherry picked from commit 47296ef589226ec436543b0f8b56c2e5079ec54a)
Generated-by: Claude Opus 5
Co-authored-by: Jarek Potiuk <[email protected]>
---
.github/workflows/additional-prod-image-tests.yml | 50 ++++++--
.github/workflows/basic-tests.yml | 8 ++
.github/workflows/ci-amd.yml | 4 +
.github/workflows/ci-arm.yml | 4 +
.../scheduled-legacy-prod-image-canary.yml | 1 +
dev/breeze/doc/ci/04_selective_checks.md | 24 +++-
.../src/airflow_breeze/utils/selective_checks.py | 123 +++++++++++++++++-
.../src/airflow_breeze/utils/verification_plan.py | 2 +
dev/breeze/tests/test_selective_checks.py | 142 +++++++++++++++++++++
9 files changed, 345 insertions(+), 13 deletions(-)
diff --git a/.github/workflows/additional-prod-image-tests.yml
b/.github/workflows/additional-prod-image-tests.yml
index c850b783deb..c21fe129f89 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -116,6 +116,12 @@ on: # yamllint disable-line rule:truthy
description: "Whether to run Airflow CTL integration tests
(true/false)"
required: true
type: string
+ run-generic-prod-image-tests:
+ description: >-
+ Whether to run the tests of the PROD image itself (true/false): not
on the release branch of a
+ single component, which builds the image only for that component's
tests
+ required: true
+ type: string
permissions:
contents: read
jobs:
@@ -132,7 +138,9 @@ jobs:
constraints-branch: ${{ inputs.constraints-branch }}
docker-cache: ${{ inputs.docker-cache }}
disable-airflow-repo-cache: ${{ inputs.disable-airflow-repo-cache }}
- if: inputs.default-branch == 'main' && inputs.canary-run == 'true'
+ if: >-
+ inputs.default-branch == 'main' && inputs.canary-run == 'true' &&
+ inputs.run-generic-prod-image-tests == 'true'
prod-image-extra-checks-release-branch:
name: PROD image extra checks (release)
@@ -147,11 +155,14 @@ jobs:
constraints-branch: ${{ inputs.constraints-branch }}
docker-cache: ${{ inputs.docker-cache }}
disable-airflow-repo-cache: ${{ inputs.disable-airflow-repo-cache }}
- if: inputs.default-branch != 'main' && inputs.canary-run == 'true'
+ if: >-
+ inputs.default-branch != 'main' && inputs.canary-run == 'true' &&
+ inputs.run-generic-prod-image-tests == 'true'
test-examples-of-prod-image-building:
timeout-minutes: 60
name: "Test examples of PROD image building"
+ if: inputs.run-generic-prod-image-tests == 'true'
runs-on: ${{ format(fromJSON(inputs.runners)[0], 4, 16) }}
env:
GITHUB_REPOSITORY: ${{ github.repository }}
@@ -189,6 +200,7 @@ jobs:
test-docker-compose-quick-start:
timeout-minutes: 60
name: "Docker Compose quick start with PROD image verifying"
+ if: inputs.run-generic-prod-image-tests == 'true'
runs-on: ${{ format(fromJSON(inputs.runners)[0], 32, 128) }}
env:
PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}"
@@ -251,6 +263,7 @@ jobs:
test-e2e-integration-tests-basic:
name: "Test e2e integration tests with PROD image"
+ if: inputs.run-generic-prod-image-tests == 'true'
uses: ./.github/workflows/airflow-e2e-tests.yml
with:
workflow-name: "Regular e2e test"
@@ -269,7 +282,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "remote_log"
- if: inputs.canary-run == 'true' || inputs.run-remote-logging-s3-e2e-tests
== 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-remote-logging-s3-e2e-tests == 'true'
test-e2e-integration-tests-remote-log-elasticsearch:
name: "Elasticsearch remote logging tests with PROD image"
@@ -281,7 +296,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "remote_log_elasticsearch"
- if: inputs.canary-run == 'true' ||
inputs.run-remote-logging-elasticsearch-e2e-tests == 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-remote-logging-elasticsearch-e2e-tests == 'true'
test-e2e-integration-tests-remote-log-opensearch:
name: "OpenSearch remote logging tests with PROD image"
@@ -293,10 +310,13 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "remote_log_opensearch"
- if: inputs.canary-run == 'true' ||
inputs.run-remote-logging-opensearch-e2e-tests == 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-remote-logging-opensearch-e2e-tests == 'true'
test-e2e-integration-tests-xcom-object-storage:
name: "XCom object storage backend tests with PROD image"
+ if: inputs.run-generic-prod-image-tests == 'true'
uses: ./.github/workflows/airflow-e2e-tests.yml
with:
workflow-name: "XCom object storage backend e2e test"
@@ -316,7 +336,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "event_driven"
- if: inputs.canary-run == 'true' || inputs.run-event-driven-e2e-tests ==
'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-event-driven-e2e-tests == 'true'
test-e2e-integration-tests-java-sdk:
name: "Java SDK e2e tests with PROD image"
@@ -329,7 +351,9 @@ jobs:
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "java_sdk"
java-sdk-version: "${{ inputs.java-sdk-version }}"
- if: inputs.canary-run == 'true' || inputs.run-java-sdk-e2e-tests == 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-java-sdk-e2e-tests == 'true'
test-e2e-integration-tests-go-sdk:
name: "Go SDK e2e tests with PROD image"
uses: ./.github/workflows/airflow-e2e-tests.yml
@@ -340,7 +364,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "go_sdk"
- if: inputs.canary-run == 'true' || inputs.run-go-sdk-e2e-tests == 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-go-sdk-e2e-tests == 'true'
test-e2e-integration-tests-ts-sdk:
name: "TypeScript SDK e2e tests with PROD image"
@@ -352,7 +378,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "ts_sdk"
- if: inputs.canary-run == 'true' || inputs.run-ts-sdk-e2e-tests == 'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-ts-sdk-e2e-tests == 'true'
test-e2e-integration-tests-openlineage:
name: "OpenLineage e2e tests with PROD image"
@@ -364,7 +392,9 @@ jobs:
default-python-version: "${{ inputs.default-python-version }}"
use-uv: ${{ inputs.use-uv }}
e2e_test_mode: "openlineage"
- if: inputs.canary-run == 'true' || inputs.run-openlineage-e2e-tests ==
'true'
+ if: >-
+ (inputs.canary-run == 'true' && inputs.run-generic-prod-image-tests ==
'true') ||
+ inputs.run-openlineage-e2e-tests == 'true'
# Older-Airflow compatibility: costly, so it does not run on every
OpenLineage PR — only on canary
# (scheduled / main) or when a maintainer sets the "full tests needed" label
(both folded into the
diff --git a/.github/workflows/basic-tests.yml
b/.github/workflows/basic-tests.yml
index 1ecf344ef6c..944315d8e83 100644
--- a/.github/workflows/basic-tests.yml
+++ b/.github/workflows/basic-tests.yml
@@ -49,6 +49,12 @@ on: # yamllint disable-line rule:truthy
description: "Whether to run scripts tests (true/false)"
required: true
type: string
+ run-generic-basic-tests:
+ description: >-
+ Whether to run the standalone Airflow and shared distributions tests
(true/false): not on the
+ release branch of a single component
+ required: true
+ type: string
basic-checks-only:
description: "Whether to run only basic checks (true/false)"
required: true
@@ -137,6 +143,7 @@ jobs:
tests-shared-distributions:
timeout-minutes: 10
name: Shared distributions tests
+ if: inputs.run-generic-basic-tests == 'true'
env:
# Without a pin uv picks the runner's system Python, which can be older
than Airflow supports.
UV_PYTHON: "${{ inputs.default-python-version }}"
@@ -394,6 +401,7 @@ jobs:
test-airflow-standalone:
timeout-minutes: 30
name: "Test Airflow standalone commands"
+ if: inputs.run-generic-basic-tests == 'true'
runs-on: ${{ format(fromJSON(inputs.runners)[0], 4, 16) }}
env:
AIRFLOW_HOME: ~/airflow
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index 49f08dbd367..a80606f666b 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -324,6 +324,8 @@ jobs:
run-agent-framework-tests: ${{
steps.selective-checks.outputs.run-agent-framework-tests }}
run-airflow-ctl-tests: ${{
steps.selective-checks.outputs.run-airflow-ctl-tests }}
run-airflow-ctl-integration-tests: ${{
steps.selective-checks.outputs.run-airflow-ctl-integration-tests }}
+ run-generic-basic-tests: ${{
steps.selective-checks.outputs.run-generic-basic-tests }}
+ run-generic-prod-image-tests: ${{
steps.selective-checks.outputs.run-generic-prod-image-tests }}
run-amazon-tests: ${{ steps.selective-checks.outputs.run-amazon-tests }}
run-api-codegen: ${{ steps.selective-checks.outputs.run-api-codegen }}
run-api-tests: ${{ steps.selective-checks.outputs.run-api-tests }}
@@ -492,6 +494,7 @@ jobs:
canary-run: ${{needs.build-info.outputs.canary-run}}
run-breeze-integration-tests:
${{needs.build-info.outputs.run-breeze-integration-tests}}
run-scripts-tests: ${{needs.build-info.outputs.run-scripts-tests}}
+ run-generic-basic-tests:
${{needs.build-info.outputs.run-generic-basic-tests}}
latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}}
use-uv: ${{needs.build-info.outputs.use-uv}}
platform: ${{ needs.build-info.outputs.platform }}
@@ -1164,6 +1167,7 @@ jobs:
use-uv: ${{ needs.build-info.outputs.use-uv }}
run-ui-e2e-tests: ${{ needs.build-info.outputs.run-ui-e2e-tests }}
run-airflow-ctl-integration-tests: ${{
needs.build-info.outputs.run-airflow-ctl-integration-tests }}
+ run-generic-prod-image-tests: ${{
needs.build-info.outputs.run-generic-prod-image-tests }}
if: needs.build-info.outputs.prod-image-build == 'true'
tests-kubernetes:
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index e6299a05ece..dd6c85c035e 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -206,6 +206,8 @@ jobs:
run-agent-framework-tests: ${{
steps.selective-checks.outputs.run-agent-framework-tests }}
run-airflow-ctl-tests: ${{
steps.selective-checks.outputs.run-airflow-ctl-tests }}
run-airflow-ctl-integration-tests: ${{
steps.selective-checks.outputs.run-airflow-ctl-integration-tests }}
+ run-generic-basic-tests: ${{
steps.selective-checks.outputs.run-generic-basic-tests }}
+ run-generic-prod-image-tests: ${{
steps.selective-checks.outputs.run-generic-prod-image-tests }}
run-amazon-tests: ${{ steps.selective-checks.outputs.run-amazon-tests }}
run-api-codegen: ${{ steps.selective-checks.outputs.run-api-codegen }}
run-api-tests: ${{ steps.selective-checks.outputs.run-api-tests }}
@@ -374,6 +376,7 @@ jobs:
canary-run: ${{needs.build-info.outputs.canary-run}}
run-breeze-integration-tests:
${{needs.build-info.outputs.run-breeze-integration-tests}}
run-scripts-tests: ${{needs.build-info.outputs.run-scripts-tests}}
+ run-generic-basic-tests:
${{needs.build-info.outputs.run-generic-basic-tests}}
latest-versions-only: ${{needs.build-info.outputs.latest-versions-only}}
use-uv: ${{needs.build-info.outputs.use-uv}}
platform: ${{ needs.build-info.outputs.platform }}
@@ -1046,6 +1049,7 @@ jobs:
use-uv: ${{ needs.build-info.outputs.use-uv }}
run-ui-e2e-tests: ${{ needs.build-info.outputs.run-ui-e2e-tests }}
run-airflow-ctl-integration-tests: ${{
needs.build-info.outputs.run-airflow-ctl-integration-tests }}
+ run-generic-prod-image-tests: ${{
needs.build-info.outputs.run-generic-prod-image-tests }}
if: needs.build-info.outputs.prod-image-build == 'true'
tests-kubernetes:
diff --git a/.github/workflows/scheduled-legacy-prod-image-canary.yml
b/.github/workflows/scheduled-legacy-prod-image-canary.yml
index 706baf19430..ad395431d0f 100644
--- a/.github/workflows/scheduled-legacy-prod-image-canary.yml
+++ b/.github/workflows/scheduled-legacy-prod-image-canary.yml
@@ -239,6 +239,7 @@ jobs:
use-uv: ${{ needs.build-info.outputs.use-uv }}
run-ui-e2e-tests: ${{ needs.build-info.outputs.run-ui-e2e-tests }}
run-airflow-ctl-integration-tests: ${{
needs.build-info.outputs.run-airflow-ctl-integration-tests }}
+ run-generic-prod-image-tests: "true"
tests-kubernetes:
name: "Kubernetes tests"
diff --git a/dev/breeze/doc/ci/04_selective_checks.md
b/dev/breeze/doc/ci/04_selective_checks.md
index d15ff81dd5e..15509596127 100644
--- a/dev/breeze/doc/ci/04_selective_checks.md
+++ b/dev/breeze/doc/ci/04_selective_checks.md
@@ -204,6 +204,8 @@ turned on):
```mermaid
flowchart TD
+ CB{target branch chart/... or airflow-ctl/...?} -->|yes| COMP[only that
component's tests ON,<br/>whatever changed; see below]
+ CB -->|no| FTN
FTN{full_tests_needed?} -->|yes| ALL[every run_* area = ON<br/>all
versions, all test types]
FTN -->|no| SEL[selective: gate each area on its file group]
SEL --> RUT{run_unit_tests?}
@@ -217,7 +219,22 @@ flowchart TD
PROD --> IMG
```
-`*` Helm tests only run on the `main` branch.
+`*` Helm tests only run on the `main` branch and on `chart/` branches.
+
+The release branches of a single component (`chart/vX-Yx-test|stable`,
`airflow-ctl/vX-Y-test|stable`)
+only ever get backports of that component, so their runs are scoped to it. The
target branch comes from
+the GitHub context (`base_ref` for a pull request, `ref_name` for a push or a
dispatched run) and must
+match one of those names: a feature branch named `chart/...` is not a
component branch, and neither is
+any push in a fork, which is selected against `main`. On such a branch every
+output decorated with `_scoped_to_component_branch` is off, except those
`COMPONENT_BRANCH_OUTPUTS` lists
+for the component: on `chart/`, Helm and Kubernetes tests always run
(kustomize overlays as usual) with
+the CI and PROD images they need; on `airflow-ctl/`, the airflow-ctl tests and
its integration tests run,
+with both images. The PROD image is built for those tests only, so
`run-generic-prod-image-tests` (Docker
+Compose, image-building examples, basic and canary end-to-end tests) is off,
and so is
+`run-generic-basic-tests` (the standalone Airflow and shared distributions
tests). Provider tests are skipped, and docs, when they changed, build only the
component's
+package. Static checks, scripts and Breeze integration tests, and the CodeQL
scans run as usual. A test
+in `test_selective_checks.py` fails when a new `run_*` output is neither
scoped nor listed as a general
+check, so a new job does not start running on these branches by accident.
### Stage 3 — choose which test types
@@ -485,6 +502,9 @@ together using `pytest-xdist` (pytest-xdist distributes the
tests among parallel
* `Python scans`, `Javascript scans`, `API tests/codegen`, `UI`, `WWW`,
`Kubernetes` tests and `DOC builds`
are enabled if any of the relevant files have been changed.
* `Helm` tests are run only if relevant files have been changed and if current
branch is `main`.
+* On a component's release branch (`chart/...`, `airflow-ctl/...`) only that
component's tests run, always,
+ with the images they need; other tests do not run, provider tests are
skipped, and docs build only the
+ component's package. Static checks still run. See [Stage
2](#stage-2--decide-which-jobs-run).
* If no Source files are changed - no tests are run and no further rules below
are checked.
* `CI Image building` is enabled if either test are run, docs are build.
* `PROD Image building` is enabled when kubernetes tests are run.
@@ -676,6 +696,8 @@ GitHub Actions to pass the list of parameters to a command
to execute
| run-amazon-tests | Whether Amazon
tests should be run ("true"/"false")
| true | |
| run-api-codegen | Whether
"api-codegen" are needed to run ("true"/"false")
| true | |
| run-api-tests | Whether
"api-tests" are needed to run ("true"/"false")
| true | |
+| run-generic-prod-image-tests | Whether the PROD
image's own tests run (Docker Compose, examples, basic e2e): off on component
branches | true | |
+| run-generic-basic-tests | Whether the
standalone Airflow and shared distributions tests run: off on component
branches | true | |
| run-helm-tests | Whether Helm tests
are needed to run ("true"/"false")
| true | |
| run-kubernetes-tests | Whether Kubernetes
tests should be run ("true"/"false")
| true | |
| run-mypy | Whether mypy check
is supposed to run in this build
| true | |
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 2d420e8ef90..7e0fdb841e5 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -25,8 +25,9 @@ import re
import sys
import tomllib
from collections import defaultdict
+from collections.abc import Callable
from enum import Enum, auto
-from functools import cached_property
+from functools import cached_property, wraps
from pathlib import Path
from typing import Any, TypeVar
@@ -896,6 +897,56 @@ def _get_test_list_as_json(list_of_list_of_types:
list[list[str]]) -> list[dict[
]
+# The release branches of a single component (the Helm chart, airflow-ctl)
only ever get backports of that
+# component, so CI there runs that component's tests, whatever changed, and
nothing else. Per branch prefix,
+# the outputs decorated with `_scoped_to_component_branch` that run: True
forces one on, None leaves it to
+# its usual computation, and an output missing from the table is off. Static
checks and docs still run.
+# The PROD image is built for those tests alone:
`run_generic_prod_image_tests` stays off.
+COMPONENT_BRANCH_OUTPUTS: dict[str, dict[str, bool | None]] = {
+ "chart": {
+ "run_helm_tests": True,
+ "run_kubernetes_tests": True,
+ "run_kustomize_overlays_tests": None,
+ # Helm unit tests run in the CI image; Kubernetes tests deploy the
PROD image.
+ "ci_image_build": True,
+ "prod_image_build": True,
+ },
+ "airflow-ctl": {
+ "run_airflow_ctl_tests": True,
+ "run_airflow_ctl_integration_tests": True,
+ # The airflow-ctl integration tests run against the PROD image, built
from the CI image.
+ "ci_image_build": True,
+ "prod_image_build": True,
+ },
+}
+# The documentation built on a component's branch, when documentation changed.
+COMPONENT_BRANCH_DOCS = {"chart": "helm-chart", "airflow-ctl":
"apache-airflow-ctl"}
+# The release branches of each component, as in the pull_request branch
filters of ci-amd.yml. A feature
+# branch named after a component, say chart/fix-ingress, is not one of them.
+COMPONENT_BRANCH_PATTERNS = {
+ "chart": re.compile(r"^chart/v\d+-\d+x-(test|stable)$"),
+ "airflow-ctl": re.compile(r"^airflow-ctl/v\d+-\d+-(test|stable)$"),
+}
+
+
+def _scoped_to_component_branch(func: Callable[[SelectiveChecks], bool]) ->
Callable[[SelectiveChecks], bool]:
+ """Restrict an output deciding whether a test job runs to the component of
a component branch."""
+
+ @wraps(func)
+ def wrapper(self: SelectiveChecks) -> bool:
+ component = self._component_branch
+ if component is None:
+ return func(self)
+ outputs = COMPONENT_BRANCH_OUTPUTS[component]
+ if func.__name__ not in outputs:
+ return False
+ forced = outputs[func.__name__]
+ return func(self) if forced is None else forced
+
+ wrapper.scoped_to_component_branch = True # type: ignore[attr-defined]
+ return wrapper
+
+
class SelectiveChecks:
__HASHABLE_FIELDS = {
"_files",
@@ -998,6 +1049,26 @@ class SelectiveChecks:
def latest_versions_only(self) -> bool:
return LATEST_VERSIONS_ONLY_LABEL in self._pr_labels
+ @cached_property
+ def _target_branch(self) -> str:
+ """The branch a pull request targets, or the branch a push or a
dispatched run is on."""
+ return self._github_context_dict.get("base_ref") or
self._github_context_dict.get("ref_name") or ""
+
+ @cached_property
+ def _component_branch(self) -> str | None:
+ """The component in COMPONENT_BRANCH_OUTPUTS whose release branch the
run targets, if any."""
+ # A push in a fork is selected against apache/airflow's main, whatever
its branch is called.
+ if self._fork_push:
+ return None
+ return next(
+ (
+ component
+ for component, pattern in COMPONENT_BRANCH_PATTERNS.items()
+ if pattern.match(self._target_branch)
+ ),
+ None,
+ )
+
@cached_property
def default_python_version(self) -> str:
return (
@@ -1256,6 +1327,7 @@ class SelectiveChecks:
return False
@cached_property
+ @_scoped_to_component_branch
def run_mypy_providers(self) -> bool:
# Non-provider mypy checks run as part of regular static checks (prek
hooks).
# Only provider mypy needs a separate CI job (requires the CI Docker
image with breeze).
@@ -1281,14 +1353,17 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.JAVASCRIPT_PRODUCTION_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_api_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.API_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ol_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.ASSET_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_api_codegen(self) -> bool:
return self._should_be_run(FileGroupForCi.API_CODEGEN_FILES)
@@ -1297,10 +1372,12 @@ class SelectiveChecks:
return
self._should_be_run(FileGroupForCi.BREEZE_INTEGRATION_TEST_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ui_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.UI_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ui_e2e_tests(self) -> bool:
# E2E tests should not be triggered by derived full_tests_needed (push
events,
# env file changes, large PRs, etc.) - only by explicit label or
actual file changes.
@@ -1331,40 +1408,48 @@ class SelectiveChecks:
return False
@cached_property
+ @_scoped_to_component_branch
def run_remote_logging_s3_e2e_tests(self) -> bool:
return
self._should_be_run(FileGroupForCi.REMOTE_LOGGING_E2E_SHARED_FILES) or
self._should_be_run(
FileGroupForCi.REMOTE_LOGGING_E2E_S3_FILES
)
@cached_property
+ @_scoped_to_component_branch
def run_remote_logging_elasticsearch_e2e_tests(self) -> bool:
return
self._should_be_run(FileGroupForCi.REMOTE_LOGGING_E2E_SHARED_FILES) or
self._should_be_run(
FileGroupForCi.REMOTE_LOGGING_E2E_ELASTICSEARCH_FILES
)
@cached_property
+ @_scoped_to_component_branch
def run_remote_logging_opensearch_e2e_tests(self) -> bool:
return
self._should_be_run(FileGroupForCi.REMOTE_LOGGING_E2E_SHARED_FILES) or
self._should_be_run(
FileGroupForCi.REMOTE_LOGGING_E2E_OPENSEARCH_FILES
)
@cached_property
+ @_scoped_to_component_branch
def run_event_driven_e2e_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.EVENT_DRIVEN_E2E_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_java_sdk_e2e_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.JAVA_SDK_E2E_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_go_sdk_e2e_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.GO_SDK_E2E_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_openlineage_e2e_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.OPENLINEAGE_E2E_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_openlineage_e2e_compat_tests(self) -> bool:
# Costly older-Airflow matrix. Like run_ui_e2e_tests it is not
triggered by *derived*
# full_tests_needed (pushes, env changes, large PRs) — only by canary,
an explicit label, or
@@ -1376,10 +1461,12 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.OPENLINEAGE_E2E_COMPAT_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ts_sdk_e2e_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.TS_SDK_E2E_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_amazon_tests(self) -> bool:
if self.providers_test_types_list_as_strings_in_json == "[]":
return False
@@ -1389,16 +1476,19 @@ class SelectiveChecks:
)
@cached_property
+ @_scoped_to_component_branch
def run_task_sdk_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.TASK_SDK_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_task_sdk_integration_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.TASK_SDK_FILES) or
self._should_be_run(
FileGroupForCi.TASK_SDK_INTEGRATION_TEST_FILES
)
@cached_property
+ @_scoped_to_component_branch
def run_agent_framework_tests(self) -> bool:
# Providers are released from main only, as for skip_providers_tests.
if self._default_branch != "main":
@@ -1406,26 +1496,32 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.AGENT_FRAMEWORK_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_go_sdk_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.GO_SDK_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_java_sdk_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.JAVA_SDK_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ts_sdk_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.TS_SDK_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_ts_sdk_docs(self) -> bool:
return self._should_be_run(FileGroupForCi.TS_SDK_DOCS_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_airflow_ctl_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.AIRFLOW_CTL_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_airflow_ctl_integration_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.AIRFLOW_CTL_FILES) or
self._should_be_run(
FileGroupForCi.AIRFLOW_CTL_INTEGRATION_TEST_FILES
@@ -1436,6 +1532,7 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.SCRIPTS_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_kubernetes_tests(self) -> bool:
if AREA_KUBERNETES_TESTS_LABEL in self._pr_labels:
console_print(
@@ -1450,10 +1547,12 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.DOC_FILES)
@cached_property
+ @_scoped_to_component_branch
def run_helm_tests(self) -> bool:
return self._should_be_run(FileGroupForCi.HELM_FILES) and
self._default_branch == "main"
@cached_property
+ @_scoped_to_component_branch
def run_kustomize_overlays_tests(self) -> bool:
"""Gate for the kustomize-overlays smoke test CI job.
@@ -1467,6 +1566,7 @@ class SelectiveChecks:
return self._should_be_run(FileGroupForCi.KUSTOMIZE_OVERLAYS_FILES)
and self._default_branch == "main"
@cached_property
+ @_scoped_to_component_branch
def run_unit_tests(self) -> bool:
def _only_new_ui_files() -> bool:
all_source_files = set(
@@ -1504,6 +1604,7 @@ class SelectiveChecks:
return len(self._files) > 0 and set(self._files) <= text_non_doc_files
@cached_property
+ @_scoped_to_component_branch
def ci_image_build(self) -> bool:
# in case pyproject.toml changed, CI image should be built - even if
no build dependencies
# changes because some of our tests - those that need CI image might
need to be run depending on
@@ -1523,6 +1624,21 @@ class SelectiveChecks:
)
@cached_property
+ @_scoped_to_component_branch
+ def run_generic_basic_tests(self) -> bool:
+ """The basic tests of Airflow itself that run for every change: the
standalone Airflow and the
+ shared distributions."""
+ return True
+
+ @cached_property
+ @_scoped_to_component_branch
+ def run_generic_prod_image_tests(self) -> bool:
+ """The tests of the PROD image itself, run whenever it is built:
Docker Compose, the image-building
+ examples and the basic end-to-end tests."""
+ return self.prod_image_build
+
+ @cached_property
+ @_scoped_to_component_branch
def prod_image_build(self) -> bool:
if AREA_E2E_TESTS_LABEL in self._pr_labels:
console_print(
@@ -1940,6 +2056,8 @@ class SelectiveChecks:
_ALL_DOCS_LIST = ""
if not self.docs_build:
return None
+ if self._component_branch is not None:
+ return COMPONENT_BRANCH_DOCS[self._component_branch]
if self._default_branch != "main":
return "apache-airflow docker-stack"
if self.full_tests_needed:
@@ -2099,7 +2217,7 @@ class SelectiveChecks:
@cached_property
def skip_providers_tests(self) -> bool:
- if self._default_branch != "main":
+ if self._default_branch != "main" or self._component_branch is not
None:
return True
if self.full_tests_needed:
return False
@@ -2296,6 +2414,7 @@ class SelectiveChecks:
return False
@cached_property
+ @_scoped_to_component_branch
def has_migrations(self) -> bool:
return any([file.startswith("airflow-core/src/airflow/migrations/")
for file in self._files])
diff --git a/dev/breeze/src/airflow_breeze/utils/verification_plan.py
b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
index d7fa2041ca8..808bed1f7ff 100644
--- a/dev/breeze/src/airflow_breeze/utils/verification_plan.py
+++ b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
@@ -93,6 +93,8 @@ NOT_RUNNABLE_LOCALLY: frozenset[str] = frozenset(
"run_openlineage_e2e_tests",
"run_openlineage_e2e_compat_tests",
"run_ts_sdk_e2e_tests",
+ "run_generic_prod_image_tests",
+ "run_generic_basic_tests",
}
)
diff --git a/dev/breeze/tests/test_selective_checks.py
b/dev/breeze/tests/test_selective_checks.py
index 90ee9560f0d..efb8baa40a1 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -48,6 +48,7 @@ from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
from airflow_breeze.utils.provider_dependencies import
get_provider_dependencies
from airflow_breeze.utils.selective_checks import (
ALL_CI_SELECTIVE_TEST_TYPES,
+ COMPONENT_BRANCH_OUTPUTS,
LONG_RUNNING_TEST_PROVIDERS,
PROVIDERS_WITH_PROCESS_GLOBAL_TEST_SIDE_EFFECTS,
SelectiveChecks,
@@ -5316,3 +5317,144 @@ def
test_self_hosted_label_ignored_when_reviews_cannot_be_read(
pr_labels=("use self-hosted runners",),
)
assert selective_checks.runner_type == expected
+
+
+# The outputs that stay as computed on the branch of a single component: they
are general checks rather
+# than another component's tests. Any other new `run_*` output must be scoped,
so that it does not run there.
+GENERAL_CHECKS = {
+ "run_breeze_integration_tests",
+ "run_javascript_scans",
+ "run_python_scans",
+ "run_scripts_tests",
+ "run_system_tests", # follows run_unit_tests
+}
+
+
+def test_every_test_output_is_scoped_to_component_branches():
+ names = [
+ name for name in dir(SelectiveChecks) if name.startswith("run_") or
name.endswith("_image_build")
+ ]
+ unscoped = [
+ name
+ for name in names
+ if name not in GENERAL_CHECKS
+ and not getattr(getattr(SelectiveChecks, name).func,
"scoped_to_component_branch", False)
+ ]
+ assert unscoped == []
+ for outputs in COMPONENT_BRANCH_OUTPUTS.values():
+ assert set(outputs) <= set(names)
+
+
+def _component_branch_checks(context: dict, files: tuple[str, ...]) ->
SelectiveChecks:
+ return SelectiveChecks(
+ files=files,
+ commit_ref=NEUTRAL_COMMIT,
+ github_event=GithubEvents.PULL_REQUEST if "base_ref" in context else
GithubEvents.PUSH,
+ github_context_dict=context,
+ )
+
+
[email protected](
+ ("context", "expected_on", "expected_docs"),
+ [
+ pytest.param(
+ {"base_ref": "chart/v1-2x-test"},
+ {"run_helm_tests", "run_kubernetes_tests", "ci_image_build",
"prod_image_build"},
+ "helm-chart",
+ id="pull-request-to-chart-branch",
+ ),
+ pytest.param(
+ {"ref_name": "chart/v1-2x-test"},
+ # A push runs in full, so the kustomize overlays, selected as
usual, run too.
+ {
+ "run_helm_tests",
+ "run_kubernetes_tests",
+ "run_kustomize_overlays_tests",
+ "ci_image_build",
+ "prod_image_build",
+ },
+ "helm-chart",
+ id="push-to-chart-branch",
+ ),
+ pytest.param(
+ {"base_ref": "airflow-ctl/v0-1-test"},
+ {
+ "run_airflow_ctl_tests",
+ "run_airflow_ctl_integration_tests",
+ "ci_image_build",
+ "prod_image_build",
+ },
+ "apache-airflow-ctl",
+ id="pull-request-to-airflow-ctl-branch",
+ ),
+ ],
+)
+def test_component_branch_runs_only_its_component(context, expected_on,
expected_docs):
+ # A change that would run nearly everything on main.
+ checks = _component_branch_checks(
+ context, ("airflow-core/src/airflow/models/dag.py",
"airflow-core/docs/index.rst")
+ )
+ scoped = {
+ name
+ for name in dir(SelectiveChecks)
+ if (name.startswith("run_") or name.endswith("_image_build")) and name
not in GENERAL_CHECKS
+ }
+
+ assert {name for name in scoped if getattr(checks, name)} == expected_on
+ assert checks.skip_providers_tests is True
+ assert checks.has_migrations is False
+ assert checks.docs_list_as_string == expected_docs
+
+
[email protected](
+ ("files", "expected"),
+ [
+ pytest.param(("chart/kustomize-overlays/base/kustomization.yaml",),
True, id="overlay-changed"),
+ pytest.param(("chart/values.yaml",), False, id="overlay-unchanged"),
+ ],
+)
+def test_chart_branch_keeps_the_kustomize_overlay_selection(files, expected):
+ checks = _component_branch_checks({"base_ref": "chart/v1-2x-test"}, files)
+
+ assert checks.run_kustomize_overlays_tests is expected
+
+
[email protected](
+ ("context", "fork_push", "expected"),
+ [
+ pytest.param({"base_ref": "chart/v1-2x-stable"}, False, "chart",
id="chart-stable-branch"),
+ pytest.param({"base_ref": "airflow-ctl/v0-1-stable"}, False,
"airflow-ctl", id="airflow-ctl-stable"),
+ pytest.param({"base_ref": "v3-4-test"}, False, None,
id="airflow-release-branch"),
+ pytest.param({"base_ref": "main"}, False, None, id="main"),
+ # A feature branch named after a component is not its release branch.
+ pytest.param({"ref_name": "chart/fix-ingress"}, False, None,
id="chart-feature-branch"),
+ # A fork push is selected against main, whatever its branch is called.
+ pytest.param({"ref_name": "chart/v1-2x-test"}, True, None,
id="fork-push"),
+ ],
+)
+def test_component_branch_detection(context, fork_push, expected):
+ checks = SelectiveChecks(
+ files=("airflow-core/src/airflow/models/dag.py",),
+ commit_ref=NEUTRAL_COMMIT,
+ github_event=GithubEvents.PUSH if fork_push else
GithubEvents.PULL_REQUEST,
+ github_context_dict=context,
+ fork_push=fork_push,
+ )
+
+ assert checks._component_branch == expected
+
+
[email protected](
+ ("context", "expected"),
+ [
+ pytest.param({"base_ref": "main"}, True, id="main"),
+ # The PROD image is built there for the Kubernetes tests alone.
+ pytest.param({"base_ref": "chart/v1-2x-test"}, False,
id="chart-branch"),
+ ],
+)
+def test_generic_tests_run_outside_component_branches(context, expected):
+ checks = _component_branch_checks(context,
("kubernetes-tests/tests/kubernetes_tests/test_base.py",))
+
+ assert checks.prod_image_build is True
+ assert checks.run_generic_prod_image_tests is expected
+ assert checks.run_generic_basic_tests is expected