From d6d0d71c5125871fe3a001882aa37629388021be Mon Sep 17 00:00:00 2001
From: Kacper Muda <mudakacper@gmail.com>
Date: Fri, 3 Jul 2026 12:26:38 +0200
Subject: [PATCH 1/2] Add providers E2E tests framework and OpenLineage tests

---
 .dockerignore                                 |   1 +
 .../workflows/additional-prod-image-tests.yml |  37 +++
 .github/workflows/ci-amd.yml                  |   6 +-
 .github/workflows/ci-arm.yml                  |   6 +-
 .github/workflows/providers-e2e-tests.yml     | 157 +++++++++++
 .rat-excludes                                 |   1 +
 dev/breeze/doc/05_test_commands.rst           |  21 ++
 dev/breeze/doc/ci/04_selective_checks.md      |   5 +
 ...utput_setup_check-all-params-in-groups.svg |   2 +-
 ...utput_setup_check-all-params-in-groups.txt |   2 +-
 ...output_setup_regenerate-command-images.svg |   8 +-
 ...output_setup_regenerate-command-images.txt |   2 +-
 dev/breeze/doc/images/output_testing.svg      |  18 +-
 dev/breeze/doc/images/output_testing.txt      |   2 +-
 .../output_testing_providers-e2e-tests.svg    | 182 +++++++++++++
 .../output_testing_providers-e2e-tests.txt    |   1 +
 .../commands/testing_commands.py              | 168 ++++++++++++
 .../commands/testing_commands_config.py       |  20 ++
 .../src/airflow_breeze/utils/run_tests.py     |   6 +
 .../airflow_breeze/utils/selective_checks.py  |  13 +
 dev/breeze/tests/test_selective_checks.py     |  36 ++-
 providers-e2e-tests/README.md                 | 111 ++++++++
 providers-e2e-tests/openlineage/.gitignore    |   7 +
 .../openlineage/.pre-commit-config.yaml       |  33 +++
 providers-e2e-tests/openlineage/Dockerfile    |  28 ++
 providers-e2e-tests/openlineage/README.md     |  75 ++++++
 .../dags_extra/e2e_versioned_bundle.py        |  62 +++++
 .../dags_extra/openlineage_warmup_dag.py      |  40 +++
 .../openlineage/docker-compose-local.yaml     |  56 ++++
 .../openlineage/docker-compose.yaml           | 181 +++++++++++++
 .../openlineage/prepare_dags.py               | 121 +++++++++
 .../openlineage/pyproject.toml                |  85 ++++++
 .../openlineage/tests/conftest.py             | 194 ++++++++++++++
 .../openlineage/tests/constants.py            |  42 +++
 .../openlineage/tests/harness.py              | 246 ++++++++++++++++++
 .../openlineage/tests/task_logs.py            | 111 ++++++++
 .../openlineage/tests/test_openlineage_e2e.py |  72 +++++
 .../tests/system/openlineage/constants.py     |  27 ++
 .../example_openlineage_all_facets_dag.py     |   2 +
 .../example_openlineage_base_complex_dag.py   |   2 +
 .../example_openlineage_base_simple_dag.py    |   2 +
 ...openlineage_custom_operator_failure_dag.py |   2 +
 ...nlineage_custom_operator_ol_methods_dag.py |   2 +
 .../example_openlineage_defer_simple_dag.py   |  10 +-
 .../example_openlineage_docs_file_dag.py      |   2 +
 .../example_openlineage_edge_labels_dag.py    |   2 +
 .../example_openlineage_hitl_dag.py           |   2 +
 .../example_openlineage_manual_lineage_dag.py |   2 +
 .../example_openlineage_mapped_simple_dag.py  |   2 +
 ...mple_openlineage_policy_conf_locked_dag.py |   2 +
 ...openlineage_policy_conf_source_code_dag.py |   2 +
 ...e_openlineage_policy_dag_emit_false_dag.py |   2 +
 ...openlineage_policy_dag_events_false_dag.py |   2 +
 ...penlineage_policy_dag_override_task_dag.py |   2 +
 ...openlineage_policy_extract_metadata_dag.py |   2 +
 ...e_openlineage_policy_full_task_info_dag.py |   2 +
 ...ple_openlineage_policy_hook_lineage_dag.py |   2 +
 ...mple_openlineage_policy_source_code_dag.py |   2 +
 ..._openlineage_policy_task_emit_false_dag.py |   2 +
 ..._openlineage_schedule_asset_or_time_dag.py |   2 +
 .../example_openlineage_schedule_cron_dag.py  |   2 +
 ...age_schedule_cron_trigger_timetable_dag.py |   2 +
 ...ge_schedule_delta_trigger_timetable_dag.py |   2 +
 ...ineage_schedule_list_complex_assets_dag.py |   2 +
 ...neage_schedule_list_multiple_assets_dag.py |   2 +
 ...nlineage_schedule_list_single_asset_dag.py |   2 +
 ...penlineage_schedule_multiple_assets_dag.py |   2 +
 ...ule_multiple_cron_trigger_timetable_dag.py |   2 +
 ...e_openlineage_schedule_single_asset_dag.py |   2 +
 ...mple_openlineage_schedule_timedelta_dag.py |   2 +
 ...mple_openlineage_schedule_timetable_dag.py |   2 +
 .../example_openlineage_setup_teardown_dag.py |   2 +
 .../example_openlineage_short_circuit_dag.py  |   2 +
 .../example_openlineage_task_groups_dag.py    |   2 +
 ...example_openlineage_taskflow_simple_dag.py |   9 +-
 .../example_openlineage_trigger_dag.py        |   5 +-
 ...mple_openlineage_trigger_dag_deferrable.py |   4 +
 .../example_openlineage_trigger_failed_dag.py |   3 +
 .../example_openlineage_versioned_dag.py      |   2 +
 ...enlineage_custom_operator_failure_dag.json |   2 +-
 pyproject.toml                                |   7 +
 ...py_full_dist_local_venv_or_breeze_in_ci.py |   4 +
 uv.lock                                       |  29 +++
 83 files changed, 2312 insertions(+), 18 deletions(-)
 create mode 100644 .github/workflows/providers-e2e-tests.yml
 create mode 100644 dev/breeze/doc/images/output_testing_providers-e2e-tests.svg
 create mode 100644 dev/breeze/doc/images/output_testing_providers-e2e-tests.txt
 create mode 100644 providers-e2e-tests/README.md
 create mode 100644 providers-e2e-tests/openlineage/.gitignore
 create mode 100644 providers-e2e-tests/openlineage/.pre-commit-config.yaml
 create mode 100644 providers-e2e-tests/openlineage/Dockerfile
 create mode 100644 providers-e2e-tests/openlineage/README.md
 create mode 100644 providers-e2e-tests/openlineage/dags_extra/e2e_versioned_bundle.py
 create mode 100644 providers-e2e-tests/openlineage/dags_extra/openlineage_warmup_dag.py
 create mode 100644 providers-e2e-tests/openlineage/docker-compose-local.yaml
 create mode 100644 providers-e2e-tests/openlineage/docker-compose.yaml
 create mode 100644 providers-e2e-tests/openlineage/prepare_dags.py
 create mode 100644 providers-e2e-tests/openlineage/pyproject.toml
 create mode 100644 providers-e2e-tests/openlineage/tests/conftest.py
 create mode 100644 providers-e2e-tests/openlineage/tests/constants.py
 create mode 100644 providers-e2e-tests/openlineage/tests/harness.py
 create mode 100644 providers-e2e-tests/openlineage/tests/task_logs.py
 create mode 100644 providers-e2e-tests/openlineage/tests/test_openlineage_e2e.py
 create mode 100644 providers/openlineage/tests/system/openlineage/constants.py

diff --git a/.dockerignore b/.dockerignore
index 401e8fff9fb40..a7e55a8f4e78e 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -51,6 +51,7 @@
 !airflow-ctl-tests
 !shared/
 !airflow-e2e-tests
+!providers-e2e-tests
 
 # Add scripts so that we can use them inside the container
 !scripts
diff --git a/.github/workflows/additional-prod-image-tests.yml b/.github/workflows/additional-prod-image-tests.yml
index bfb21345c4d20..27861ccce5165 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -60,6 +60,14 @@ on:  # yamllint disable-line rule:truthy
         description: "Whether to run Go SDK e2e tests (true/false)"
         required: true
         type: string
+      run-providers-e2e-tests-openlineage:
+        description: "Whether to run OpenLineage e2e tests (true/false)"
+        required: true
+        type: string
+      providers-compatibility-tests-matrix:
+        description: "JSON matrix of released Airflow versions for provider e2e compat tests."
+        required: true
+        type: string
       constraints-branch:
         description: "Branch used to construct constraints URL from."
         required: true
@@ -321,6 +329,35 @@ jobs:
       e2e_test_mode: "go_sdk"
     if: inputs.canary-run == 'true' || inputs.run-go-sdk-e2e-tests == 'true'
 
+  test-providers-e2e-tests-openlineage:
+    name: "Provider E2E tests openlineage"
+    uses: ./.github/workflows/providers-e2e-tests.yml
+    with:
+      provider: "openlineage"
+      provider-display-name: "OpenLineage"
+      runners: ${{ inputs.runners }}
+      platform: ${{ inputs.platform }}
+      default-python-version: "${{ inputs.default-python-version }}"
+      use-uv: ${{ inputs.use-uv }}
+    if: inputs.canary-run == 'true' || inputs.run-providers-e2e-tests-openlineage == 'true'
+
+  test-providers-e2e-tests-openlineage-compat:
+    name: "Provider E2E tests openlineage compat"
+    strategy:
+      fail-fast: false
+      matrix:
+        compat: ${{ fromJSON(inputs.providers-compatibility-tests-matrix) }}
+    uses: ./.github/workflows/providers-e2e-tests.yml
+    with:
+      provider: "openlineage"
+      provider-display-name: "OpenLineage"
+      runners: ${{ inputs.runners }}
+      platform: ${{ inputs.platform }}
+      default-python-version: "${{ inputs.default-python-version }}"
+      use-uv: ${{ inputs.use-uv }}
+      airflow-version: ${{ matrix.compat.airflow-version }}
+    if: inputs.canary-run == 'true' || inputs.run-providers-e2e-tests-openlineage == 'true'
+
   test-ui-e2e-chromium:
     name: "Chromium UI e2e tests with PROD image"
     uses: ./.github/workflows/ui-e2e-tests.yml
diff --git a/.github/workflows/ci-amd.yml b/.github/workflows/ci-amd.yml
index ab0a1a7ff606f..f404b67c35d43 100644
--- a/.github/workflows/ci-amd.yml
+++ b/.github/workflows/ci-amd.yml
@@ -141,6 +141,7 @@ jobs:
       run-event-driven-e2e-tests: ${{ steps.selective-checks.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-java-sdk-e2e-tests }}
       run-go-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-go-sdk-e2e-tests }}
+      run-providers-e2e-tests-openlineage: ${{ steps.selective-checks.outputs.run-providers-e2e-tests-openlineage }}
       run-system-tests: ${{ steps.selective-checks.outputs.run-system-tests }}
       run-task-sdk-tests: ${{ steps.selective-checks.outputs.run-task-sdk-tests }}
       run-task-sdk-integration-tests: ${{ steps.selective-checks.outputs.run-task-sdk-integration-tests }}
@@ -435,7 +436,7 @@ jobs:
           DEFAULT_BRANCH: ${{ needs.build-info.outputs.default-branch }}
 
   providers:
-    name: "provider distributions tests"
+    name: "Provider distributions tests"
     uses: ./.github/workflows/test-providers.yml
     needs: [build-info, build-ci-images]
     permissions:
@@ -913,6 +914,9 @@ jobs:
       run-event-driven-e2e-tests: ${{ needs.build-info.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ needs.build-info.outputs.run-java-sdk-e2e-tests }}
       run-go-sdk-e2e-tests: ${{ needs.build-info.outputs.run-go-sdk-e2e-tests }}
+      run-providers-e2e-tests-openlineage: ${{ needs.build-info.outputs.run-providers-e2e-tests-openlineage }}
+      providers-compatibility-tests-matrix: >
+        ${{ needs.build-info.outputs.providers-compatibility-tests-matrix }}
       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 }}
diff --git a/.github/workflows/ci-arm.yml b/.github/workflows/ci-arm.yml
index 354155192a2d5..4f7dd7b54c4d0 100644
--- a/.github/workflows/ci-arm.yml
+++ b/.github/workflows/ci-arm.yml
@@ -134,6 +134,7 @@ jobs:
       run-event-driven-e2e-tests: ${{ steps.selective-checks.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-java-sdk-e2e-tests }}
       run-go-sdk-e2e-tests: ${{ steps.selective-checks.outputs.run-go-sdk-e2e-tests }}
+      run-providers-e2e-tests-openlineage: ${{ steps.selective-checks.outputs.run-providers-e2e-tests-openlineage }}
       run-system-tests: ${{ steps.selective-checks.outputs.run-system-tests }}
       run-task-sdk-tests: ${{ steps.selective-checks.outputs.run-task-sdk-tests }}
       run-task-sdk-integration-tests: ${{ steps.selective-checks.outputs.run-task-sdk-integration-tests }}
@@ -428,7 +429,7 @@ jobs:
           DEFAULT_BRANCH: ${{ needs.build-info.outputs.default-branch }}
 
   providers:
-    name: "provider distributions tests"
+    name: "Provider distributions tests"
     uses: ./.github/workflows/test-providers.yml
     needs: [build-info, build-ci-images]
     permissions:
@@ -906,6 +907,9 @@ jobs:
       run-event-driven-e2e-tests: ${{ needs.build-info.outputs.run-event-driven-e2e-tests }}
       run-java-sdk-e2e-tests: ${{ needs.build-info.outputs.run-java-sdk-e2e-tests }}
       run-go-sdk-e2e-tests: ${{ needs.build-info.outputs.run-go-sdk-e2e-tests }}
+      run-providers-e2e-tests-openlineage: ${{ needs.build-info.outputs.run-providers-e2e-tests-openlineage }}
+      providers-compatibility-tests-matrix: >
+        ${{ needs.build-info.outputs.providers-compatibility-tests-matrix }}
       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 }}
diff --git a/.github/workflows/providers-e2e-tests.yml b/.github/workflows/providers-e2e-tests.yml
new file mode 100644
index 0000000000000..1ed6d77eb754e
--- /dev/null
+++ b/.github/workflows/providers-e2e-tests.yml
@@ -0,0 +1,157 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+
+name: Providers E2E Tests
+
+permissions:
+  contents: read
+on:  # yamllint disable-line rule:truthy
+  workflow_dispatch:
+    inputs:
+      provider:
+        description: "The provider whose e2e suite to run (a directory under providers-e2e-tests/)."
+        type: string
+        default: 'openlineage'
+      provider-display-name:
+        description: "Display name for the provider, used only in the job name."
+        type: string
+        default: 'OpenLineage'
+      runners:
+        description: "The array of labels (in json form) determining runners."
+        type: string
+        default: '["ubuntu-24.04"]'
+      platform:
+        description: "Platform for the build - 'linux/amd64' or 'linux/arm64'"
+        type: string
+        default: 'linux/amd64'
+      default-python-version:
+        description: "Which version of python should be used by default"
+        type: string
+        default: '3.10'
+      use-uv:
+        description: "Whether to use uv to build the image (true/false)"
+        type: string
+        default: 'true'
+      docker-image-tag:
+        description: "Tag of the Docker image to test"
+        type: string
+        required: true
+      airflow-version:
+        description: "Released Airflow version to test with current providers (empty = default PROD image)"
+        type: string
+        default: ""
+
+  workflow_call:
+    inputs:
+      provider:
+        description: "The provider whose e2e suite to run (a directory under providers-e2e-tests/)."
+        type: string
+        default: 'openlineage'
+      provider-display-name:
+        description: "Display name for the provider, used only in the job name."
+        type: string
+        default: 'OpenLineage'
+      runners:
+        description: "The array of labels (in json form) determining runners."
+        required: true
+        type: string
+      platform:
+        description: "Platform for the build - 'linux/amd64' or 'linux/arm64'"
+        required: true
+        type: string
+      default-python-version:
+        description: "Which version of python should be used by default"
+        required: true
+        type: string
+      use-uv:
+        description: "Whether to use uv to build the image (true/false)"
+        required: true
+        type: string
+      docker-image-tag:
+        description: "Tag of the Docker image to test"
+        type: string
+        default: ""
+      airflow-version:
+        description: "Released Airflow version to test with current providers (empty = default PROD image)"
+        type: string
+        default: ""
+
+jobs:
+  test-providers-e2e-tests:
+    timeout-minutes: 60
+    name: "Provider E2E tests \
+      ${{ inputs.provider-display-name }}\
+      ${{ inputs.airflow-version != '' && format(' Compat {0}', inputs.airflow-version) || '' }}"
+    runs-on: ${{ fromJSON(inputs.runners) }}
+    env:
+      PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}"
+      PROVIDER: ${{ inputs.provider }}
+      GITHUB_REPOSITORY: ${{ github.repository }}
+      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      GITHUB_USERNAME: ${{ github.actor }}
+      VERBOSE: "true"
+    steps:
+      # Gating on job-level `if:` instead of per-step would skip the whole job — and GitHub Actions
+      # does not evaluate a dynamic `name:` (the one above, using `inputs.*`) for a skipped job, so it
+      # renders the raw unevaluated template instead. Gating each step keeps the job itself "running"
+      # (and its name properly resolved), ending in success with every real step skipped for AF2.
+      - name: "Skip notice for Airflow 2.x"
+        if: ${{ startsWith(inputs.airflow-version, '2.') }}
+        run: echo "Skipping $PROVIDER e2e tests — not supported against Airflow 2.x."
+      - name: "Cleanup repo"
+        if: ${{ !startsWith(inputs.airflow-version, '2.') }}
+        shell: bash
+        run: sudo rm -rf ${GITHUB_WORKSPACE}/*
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        if: ${{ !startsWith(inputs.airflow-version, '2.') }}
+        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10  # v6.0.3
+        with:
+          fetch-depth: 2
+          persist-credentials: false
+      - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}"
+        if: ${{ !startsWith(inputs.airflow-version, '2.') }}
+        uses: ./.github/actions/prepare_breeze_and_image
+        with:
+          platform: ${{ inputs.platform }}
+          image-type: "prod"
+          python: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
+          use-uv: ${{ inputs.use-uv }}
+          make-mnt-writeable-and-cleanup: true
+        id: breeze
+      - name: "Run ${{ inputs.provider }} e2e tests"
+        if: ${{ !startsWith(inputs.airflow-version, '2.') }}
+        run: >
+          breeze testing providers-e2e-tests "$PROVIDER"
+          ${AIRFLOW_VERSION:+--airflow-version ${AIRFLOW_VERSION}}
+          --skip-mounting-local-volumes
+        env:
+          DOCKER_IMAGE: "${{ inputs.docker-image-tag }}"
+          AIRFLOW_VERSION: "${{ inputs.airflow-version }}"
+      - name: Zip logs
+        if: ${{ always() && !startsWith(inputs.airflow-version, '2.') }}
+        run: |
+          cd "./providers-e2e-tests/$PROVIDER" && zip -r logs.zip logs
+      - name: "Upload logs"
+        if: ${{ always() && !startsWith(inputs.airflow-version, '2.') }}
+        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a  # v7.0.1
+        with:
+          name: "${{ inputs.provider }}-e2e-test-logs-${{ inputs.airflow-version || 'prod' }}"
+          path: './providers-e2e-tests/${{ inputs.provider }}/logs.zip'
+          retention-days: '7'
+          if-no-files-found: 'error'
diff --git a/.rat-excludes b/.rat-excludes
index f0cd2910f5b27..0510ce24783f9 100644
--- a/.rat-excludes
+++ b/.rat-excludes
@@ -45,6 +45,7 @@ venv
 clients/*
 files/*
 dags/*
+providers-e2e-tests/openlineage/dags/dag_doc.md
 generated/*
 .gitmodules
 prod_image_installed_providers.txt
diff --git a/dev/breeze/doc/05_test_commands.rst b/dev/breeze/doc/05_test_commands.rst
index df3cfbb970f8f..52256c930fbfb 100644
--- a/dev/breeze/doc/05_test_commands.rst
+++ b/dev/breeze/doc/05_test_commands.rst
@@ -373,6 +373,27 @@ You can override the ``DOCKER_IMAGE`` environment variable to point to the image
 
 The Airflow E2E tests are in ``airflow-e2e-tests/`` folder in the main repo.
 
+Running provider E2E tests
+..........................
+
+You can use Breeze to run provider end-to-end tests, run against a real built Airflow image
+(``$DOCKER_IMAGE``) rather than the in-process ``dag.test()`` path used by provider system tests.
+What "e2e" means, and how the stack under test gets stood up, is entirely up to the provider — the
+OpenLineage suite, for example, deploys a real Airflow via docker-compose and runs its system-test
+DAGs against it, but a different provider could drive it a completely different way. Those tests run
+using the Production image by default; pass ``--airflow-version`` to run against an older released
+Airflow version with the current providers installed from main.
+
+.. image:: ./images/output_testing_providers-e2e-tests.svg
+  :target: https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_testing_providers-e2e-tests.svg
+  :width: 100%
+  :alt: Breeze testing providers-e2e-tests
+
+The provider E2E tests are in the ``providers-e2e-tests/`` folder in the main repo; see
+``providers-e2e-tests/README.md`` for how to run any provider's suite, how it's wired into CI, and
+what's involved in adding a new provider (``providers-e2e-tests/openlineage/`` is one
+example implementation).
+
 Running Airflow UI E2E tests
 .............................
 
diff --git a/dev/breeze/doc/ci/04_selective_checks.md b/dev/breeze/doc/ci/04_selective_checks.md
index 13a7e39f66f62..bc131cdd066b7 100644
--- a/dev/breeze/doc/ci/04_selective_checks.md
+++ b/dev/breeze/doc/ci/04_selective_checks.md
@@ -427,6 +427,11 @@ together using `pytest-xdist` (pytest-xdist distributes the tests among parallel
     of affected providers (but not recursively - only direct dependencies are added)
   * if there are any changes to "common" provider code not belonging to any provider (usually system tests
     or tests), then tests for all Providers are run
+* `OpenLineage E2E tests` (the deployed-stack tests under `providers-e2e-tests/openlineage`, exposed as
+  the `run-providers-e2e-tests-openlineage` output) run when the `openlineage` or `common` providers or the
+  `providers-e2e-tests` harness change — and always on `canary` runs (where `full tests needed` also
+  covers core/task-sdk changes). Like the other deployed e2e suites, enabling them forces
+  `PROD Image building`.
 * The specific unit test type is enabled only if changed files match the expected patterns for each type
   (`API`, `CLI`, `WWW`, `Providers` etc.). The `Always` test type is added always if any unit
   tests are run. `Providers` tests are removed if current branch is different than `main`
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index 202eba652be36..22be2e9030b39 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -240,7 +240,7 @@
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="898.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4" textLength="1256.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">setup:config&#160;|&#160;setup:regenerate-command-images&#160;|&#160;setup:self-upgrade&#160;|&#160;setup:synchronize-local-mounts&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="898.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="922.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="922.8" textLength="1098" clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;testing:airflow-ctl-integration-tests&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="922.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="922.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="947.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="947.2" textLength="1085.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">testing:airflow-ctl-tests&#160;|&#160;testing:airflow-e2e-tests&#160;|&#160;testing:core-integration-tests&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="947.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="947.2" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="971.6" textLength="890.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="971.6" textLength="1256.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests&#160;|&#160;testing:providers-e2e-tests&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="996" textLength="1195.6" clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="996" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="996" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1020.4" textLength="1281" clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="1044.8" textLength="1268.8" clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs)</text><text class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
index 92e75f17fd456..ef361b8db62cf 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
@@ -1 +1 @@
-fc5f65d76377357873c6b3ea200f2ec8
+ebfe38ba1a4b30b85d55382436ef1834
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
index de56522e8180b..88faae5eb3574 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -258,10 +258,10 @@
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-39)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="971.6" textLength="1037" clip-path="url(#breeze-setup-regenerate-command-images-line-39)">|&#160;setup:synchronize-local-mounts&#160;|&#160;setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-39)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="971.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-39)">
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="996" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-40)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="996" textLength="1171.2" clip-path="url(#breeze-setup-regenerate-command-images-line-40)">testing:airflow-ctl-integration-tests&#160;|&#160;testing:airflow-ctl-tests&#160;|&#160;testing:airflow-e2e-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="996" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-40)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="996" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-40)">
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-41)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1020.4" textLength="1037" clip-path="url(#breeze-setup-regenerate-command-images-line-41)">testing:core-integration-tests&#160;|&#160;testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-41)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1020.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-41)">
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1044.8" textLength="1037" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">testing:helm-tests&#160;|&#160;testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1069.2" textLength="1146.8" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">testing:python-api-client-tests&#160;|&#160;testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1093.6" textLength="1085.8" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests&#160;|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118" textLength="744.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs)</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1044.8" textLength="1085.8" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">testing:helm-tests&#160;|&#160;testing:providers-e2e-tests&#160;|&#160;testing:providers-integration-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1044.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-42)">
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1069.2" textLength="1012.6" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;testing:system-tests&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1069.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-43)">
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1093.6" textLength="1098" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests&#160;|&#160;ui&#160;|&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1093.6" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-44)">
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118" textLength="1183.4" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs)</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1118" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-45)">
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1142.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text class="breeze-setup-regenerate-command-images-r4" x="24.4" y="1142.4" textLength="146.4" clip-path="url(#breeze-setup-regenerate-command-images-line-46)">--check-only</text><text class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1142.4" textLength="1244.4" clip-path="url(#breeze-setup-regenerate-command-images-line-46)">Only&#160;check&#160;if&#160;some&#160;images&#160;need&#160;to&#160;be&#160;regenerated.&#160;Return&#160;0&#160;if&#160;no&#160;need&#160;or&#160;1&#160;if&#160;needed.&#160;Cannot&#160;be&#160;used&#160;&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1142.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1142.4" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-46)">
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1166.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1166.8" textLength="170.8" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">together&#160;with&#160;</text><text class="breeze-setup-regenerate-command-images-r4" x="366" y="1166.8" textLength="109.8" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">--command</text><text class="breeze-setup-regenerate-command-images-r1" x="475.8" y="1166.8" textLength="109.8" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">&#160;flag&#160;or&#160;</text><text class="breeze-setup-regenerate-command-images-r4" x="585.6" y="1166.8" textLength="85.4" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">--force</text><text class="breeze-setup-regenerate-command-images-r1" x="671" y="1166.8" textLength="768.6" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1166.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">│</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1166.8" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-47)">
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1191.2" textLength="1464" clip-path="url(#breeze-setup-regenerate-command-images-line-48)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-setup-regenerate-command-images-r1" x="1464" y="1191.2" textLength="12.2" clip-path="url(#breeze-setup-regenerate-command-images-line-48)">
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
index fe2e502097d32..81d6cfb046ac2 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
@@ -1 +1 @@
-141b4bc2fbf13eddd1301de1064bf9fe
+021adb1e353dab626f2092d96b2e7b45
diff --git a/dev/breeze/doc/images/output_testing.svg b/dev/breeze/doc/images/output_testing.svg
index 60192bbfea0d5..a05e3458ee1dd 100644
--- a/dev/breeze/doc/images/output_testing.svg
+++ b/dev/breeze/doc/images/output_testing.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 879.5999999999999" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 1482 952.8" xmlns="http://www.w3.org/2000/svg">
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -42,7 +42,7 @@
 
     <defs>
     <clipPath id="breeze-testing-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="828.5999999999999" />
+      <rect x="0" y="0" width="1463.0" height="901.8" />
     </clipPath>
     <clipPath id="breeze-testing-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -143,9 +143,18 @@
 <clipPath id="breeze-testing-line-32">
     <rect x="0" y="782.3" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-testing-line-33">
+    <rect x="0" y="806.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-line-34">
+    <rect x="0" y="831.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-line-35">
+    <rect x="0" y="855.5" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="877.6" rx="8"/><text class="breeze-testing-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;testing</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="950.8" rx="8"/><text class="breeze-testing-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;testing</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -189,6 +198,9 @@
 </text><text class="breeze-testing-r5" x="0" y="776.4" textLength="24.4" clip-path="url(#breeze-testing-line-31)">╭─</text><text class="breeze-testing-r5" x="24.4" y="776.4" textLength="122" clip-path="url(#breeze-testing-line-31)">&#160;UI&#160;Tests&#160;</text><text class="breeze-testing-r5" x="146.4" y="776.4" textLength="1293.2" clip-path="url(#breeze-testing-line-31)">──────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-testing-r5" x="1439.6" y="776.4" textLength="24.4" clip-path="url(#breeze-testing-line-31)">─╮</text><text class="breeze-testing-r1" x="1464" y="776.4" textLength="12.2" clip-path="url(#breeze-testing-line-31)">
 </text><text class="breeze-testing-r5" x="0" y="800.8" textLength="12.2" clip-path="url(#breeze-testing-line-32)">│</text><text class="breeze-testing-r4" x="24.4" y="800.8" textLength="329.4" clip-path="url(#breeze-testing-line-32)">ui-e2e-tests&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-r1" x="378.2" y="800.8" textLength="1061.4" clip-path="url(#breeze-testing-line-32)">Run&#160;UI&#160;End-to-End&#160;tests&#160;using&#160;Playwright.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-r5" x="1451.8" y="800.8" textLength="12.2" clip-path="url(#breeze-testing-line-32)">│</text><text class="breeze-testing-r1" x="1464" y="800.8" textLength="12.2" clip-path="url(#breeze-testing-line-32)">
 </text><text class="breeze-testing-r5" x="0" y="825.2" textLength="1464" clip-path="url(#breeze-testing-line-33)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-testing-r1" x="1464" y="825.2" textLength="12.2" clip-path="url(#breeze-testing-line-33)">
+</text><text class="breeze-testing-r5" x="0" y="849.6" textLength="24.4" clip-path="url(#breeze-testing-line-34)">╭─</text><text class="breeze-testing-r5" x="24.4" y="849.6" textLength="122" clip-path="url(#breeze-testing-line-34)">&#160;Commands&#160;</text><text class="breeze-testing-r5" x="146.4" y="849.6" textLength="1293.2" clip-path="url(#breeze-testing-line-34)">──────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-testing-r5" x="1439.6" y="849.6" textLength="24.4" clip-path="url(#breeze-testing-line-34)">─╮</text><text class="breeze-testing-r1" x="1464" y="849.6" textLength="12.2" clip-path="url(#breeze-testing-line-34)">
+</text><text class="breeze-testing-r5" x="0" y="874" textLength="12.2" clip-path="url(#breeze-testing-line-35)">│</text><text class="breeze-testing-r4" x="24.4" y="874" textLength="329.4" clip-path="url(#breeze-testing-line-35)">providers-e2e-tests&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-r1" x="378.2" y="874" textLength="1061.4" clip-path="url(#breeze-testing-line-35)">Run&#160;provider&#160;end-to-end&#160;tests&#160;against&#160;a&#160;deployed&#160;Airflow&#160;stack.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-r5" x="1451.8" y="874" textLength="12.2" clip-path="url(#breeze-testing-line-35)">│</text><text class="breeze-testing-r1" x="1464" y="874" textLength="12.2" clip-path="url(#breeze-testing-line-35)">
+</text><text class="breeze-testing-r5" x="0" y="898.4" textLength="1464" clip-path="url(#breeze-testing-line-36)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-testing-r1" x="1464" y="898.4" textLength="12.2" clip-path="url(#breeze-testing-line-36)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_testing.txt b/dev/breeze/doc/images/output_testing.txt
index e80be8356c537..bee169a1b6781 100644
--- a/dev/breeze/doc/images/output_testing.txt
+++ b/dev/breeze/doc/images/output_testing.txt
@@ -1 +1 @@
-5c361e73595950fbdbe6d3aa8fd3855b
+f7645849511cf80e1747ddb411a881f5
diff --git a/dev/breeze/doc/images/output_testing_providers-e2e-tests.svg b/dev/breeze/doc/images/output_testing_providers-e2e-tests.svg
new file mode 100644
index 0000000000000..e727fbeb032e4
--- /dev/null
+++ b/dev/breeze/doc/images/output_testing_providers-e2e-tests.svg
@@ -0,0 +1,182 @@
+<svg class="rich-terminal" viewBox="0 0 1482 782.0" xmlns="http://www.w3.org/2000/svg">
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
+                url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .breeze-testing-providers-e2e-tests-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .breeze-testing-providers-e2e-tests-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .breeze-testing-providers-e2e-tests-r1 { fill: #c5c8c6 }
+.breeze-testing-providers-e2e-tests-r2 { fill: #d0b344 }
+.breeze-testing-providers-e2e-tests-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-testing-providers-e2e-tests-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-testing-providers-e2e-tests-r5 { fill: #868887 }
+.breeze-testing-providers-e2e-tests-r6 { fill: #4e707b;font-weight: bold }
+.breeze-testing-providers-e2e-tests-r7 { fill: #98a84b;font-weight: bold }
+.breeze-testing-providers-e2e-tests-r8 { fill: #8d7b39 }
+.breeze-testing-providers-e2e-tests-r9 { fill: #d0b344;font-weight: bold }
+    </style>
+
+    <defs>
+    <clipPath id="breeze-testing-providers-e2e-tests-clip-terminal">
+      <rect x="0" y="0" width="1463.0" height="731.0" />
+    </clipPath>
+    <clipPath id="breeze-testing-providers-e2e-tests-line-0">
+    <rect x="0" y="1.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-1">
+    <rect x="0" y="25.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-2">
+    <rect x="0" y="50.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-3">
+    <rect x="0" y="74.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-4">
+    <rect x="0" y="99.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-5">
+    <rect x="0" y="123.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-6">
+    <rect x="0" y="147.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-7">
+    <rect x="0" y="172.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-8">
+    <rect x="0" y="196.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-9">
+    <rect x="0" y="221.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-11">
+    <rect x="0" y="269.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-12">
+    <rect x="0" y="294.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-13">
+    <rect x="0" y="318.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-14">
+    <rect x="0" y="343.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-15">
+    <rect x="0" y="367.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-16">
+    <rect x="0" y="391.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-17">
+    <rect x="0" y="416.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-18">
+    <rect x="0" y="440.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-19">
+    <rect x="0" y="465.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-20">
+    <rect x="0" y="489.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-21">
+    <rect x="0" y="513.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-22">
+    <rect x="0" y="538.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-23">
+    <rect x="0" y="562.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-24">
+    <rect x="0" y="587.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-25">
+    <rect x="0" y="611.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-26">
+    <rect x="0" y="635.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-27">
+    <rect x="0" y="660.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-providers-e2e-tests-line-28">
+    <rect x="0" y="684.7" width="1464" height="24.65"/>
+            </clipPath>
+    </defs>
+
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="1480" height="780" rx="8"/><text class="breeze-testing-providers-e2e-tests-title" fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;testing&#160;providers-e2e-tests</text>
+            <g transform="translate(26,22)">
+            <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+            <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+            <circle cx="44" cy="0" r="7" fill="#28c840"/>
+            </g>
+        
+    <g transform="translate(9, 41)" clip-path="url(#breeze-testing-providers-e2e-tests-clip-terminal)">
+    
+    <g class="breeze-testing-providers-e2e-tests-matrix">
+    <text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="20" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-0)">
+</text><text class="breeze-testing-providers-e2e-tests-r2" x="12.2" y="44.4" textLength="73.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">Usage:</text><text class="breeze-testing-providers-e2e-tests-r3" x="97.6" y="44.4" textLength="414.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">breeze&#160;testing&#160;providers-e2e-tests</text><text class="breeze-testing-providers-e2e-tests-r1" x="524.6" y="44.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">[</text><text class="breeze-testing-providers-e2e-tests-r4" x="536.8" y="44.4" textLength="85.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">OPTIONS</text><text class="breeze-testing-providers-e2e-tests-r1" x="622.2" y="44.4" textLength="36.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">]&#160;{</text><text class="breeze-testing-providers-e2e-tests-r4" x="658.8" y="44.4" textLength="134.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">openlineage</text><text class="breeze-testing-providers-e2e-tests-r1" x="793" y="44.4" textLength="36.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">}&#160;[</text><text class="breeze-testing-providers-e2e-tests-r4" x="829.6" y="44.4" textLength="207.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">EXTRA_PYTEST_ARGS</text><text class="breeze-testing-providers-e2e-tests-r1" x="1037" y="44.4" textLength="48.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">]...</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="44.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-1)">
+</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="68.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-2)">
+</text><text class="breeze-testing-providers-e2e-tests-r1" x="12.2" y="93.2" textLength="768.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-3)">Run&#160;provider&#160;end-to-end&#160;tests&#160;against&#160;a&#160;deployed&#160;Airflow&#160;stack.</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="93.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-3)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="12.2" y="117.6" textLength="927.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-4)">Defaults&#160;to&#160;the&#160;PROD&#160;image&#160;(current&#160;Airflow&#160;+&#160;providers&#160;from&#160;sources).&#160;Pass&#160;</text><text class="breeze-testing-providers-e2e-tests-r6" x="939.4" y="117.6" textLength="207.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-4)">--airflow-version</text><text class="breeze-testing-providers-e2e-tests-r5" x="1146.8" y="117.6" textLength="305" clip-path="url(#breeze-testing-providers-e2e-tests-line-4)">&#160;to&#160;run&#160;against&#160;an&#160;older&#160;</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="117.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-4)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="12.2" y="142" textLength="878.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-5)">released&#160;Airflow&#160;version&#160;with&#160;the&#160;current&#160;providers&#160;installed&#160;from&#160;main.</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="142" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-5)">
+</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="166.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-6)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="190.8" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-7)">╭─</text><text class="breeze-testing-providers-e2e-tests-r5" x="24.4" y="190.8" textLength="329.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-7)">&#160;Docker-compose&#160;tests&#160;flag&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="353.8" y="190.8" textLength="1085.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-7)">─────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-testing-providers-e2e-tests-r5" x="1439.6" y="190.8" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-7)">─╮</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="190.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-7)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="215.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="215.2" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">--image-name&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r7" x="414.8" y="215.2" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">-n</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="215.2" textLength="475.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">Name&#160;of&#160;the&#160;image&#160;to&#160;verify&#160;(overrides&#160;</text><text class="breeze-testing-providers-e2e-tests-r4" x="939.4" y="215.2" textLength="97.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">--python</text><text class="breeze-testing-providers-e2e-tests-r1" x="1037" y="215.2" textLength="36.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">).&#160;</text><text class="breeze-testing-providers-e2e-tests-r8" x="1073.6" y="215.2" textLength="73.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">(TEXT)</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="215.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="215.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-8)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="239.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="239.6" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">--python&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r7" x="414.8" y="239.6" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">-p</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="239.6" textLength="744.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">Python&#160;major/minor&#160;version&#160;used&#160;in&#160;Airflow&#160;image&#160;for&#160;images.&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1207.8" y="239.6" textLength="183" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">[default:&#160;3.10]</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="239.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="239.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-9)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="264" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-10)">│</text><text class="breeze-testing-providers-e2e-tests-r8" x="463.6" y="264" textLength="439.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-10)">(&gt;3.10&lt;&#160;|&#160;3.11&#160;|&#160;3.12&#160;|&#160;3.13&#160;|&#160;3.14)</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="264" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-10)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="264" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-10)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="288.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-11)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="288.4" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-11)">--airflow-version&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="288.4" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-11)">Run&#160;against&#160;a&#160;released&#160;Airflow&#160;version&#160;(e.g.&#160;3.0.6)&#160;with&#160;current&#160;providers&#160;from&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="288.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-11)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="288.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-11)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="312.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-12)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="312.8" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-12)">main,&#160;instead&#160;of&#160;the&#160;default&#160;PROD&#160;image.&#160;Builds&#160;a&#160;lightweight&#160;image&#160;from&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="312.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-12)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="312.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-12)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="337.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="337.2" textLength="183" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">apache/airflow:</text><text class="breeze-testing-providers-e2e-tests-r9" x="646.6" y="337.2" textLength="109.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">&lt;version&gt;</text><text class="breeze-testing-providers-e2e-tests-r1" x="756.4" y="337.2" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">.&#160;</text><text class="breeze-testing-providers-e2e-tests-r8" x="780.8" y="337.2" textLength="73.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">(TEXT)</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="337.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="337.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-13)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="361.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-14)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="361.6" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-14)">--skip-docker-compose-deletion</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="361.6" textLength="671" clip-path="url(#breeze-testing-providers-e2e-tests-line-14)">Skip&#160;deletion&#160;of&#160;docker-compose&#160;instance&#160;after&#160;the&#160;test</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="361.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-14)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="361.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-14)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="386" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-15)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="386" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-15)">--skip-mounting-local-volumes&#160;</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="386" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-15)">Skip&#160;mounting&#160;local&#160;volumes&#160;-&#160;useful&#160;when&#160;we&#160;do&#160;not&#160;want&#160;to&#160;iterate&#160;with&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="386" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-15)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="386" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-15)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="410.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-16)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="410.4" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-16)">modified&#160;local&#160;files.&#160;For&#160;example,&#160;when&#160;the&#160;PROD&#160;image&#160;is&#160;built&#160;from&#160;packages&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="410.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-16)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="410.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-16)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="434.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-17)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="434.8" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-17)">rather&#160;than&#160;from&#160;local&#160;sources.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="434.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-17)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="434.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-17)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="459.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-18)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="459.2" textLength="366" clip-path="url(#breeze-testing-providers-e2e-tests-line-18)">--down&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="459.2" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-18)">Shuts&#160;down&#160;the&#160;docker-compose&#160;setup&#160;without&#160;running&#160;any&#160;tests.&#160;Useful&#160;to&#160;make&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="459.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-18)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="459.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-18)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="483.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-19)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="463.6" y="483.6" textLength="976" clip-path="url(#breeze-testing-providers-e2e-tests-line-19)">sure&#160;to&#160;free&#160;resources.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="483.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-19)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="483.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-19)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="508" textLength="1464" clip-path="url(#breeze-testing-providers-e2e-tests-line-20)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="508" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-20)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="532.4" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-21)">╭─</text><text class="breeze-testing-providers-e2e-tests-r5" x="24.4" y="532.4" textLength="231.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-21)">&#160;Common&#160;CI&#160;options&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="256.2" y="532.4" textLength="1183.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-21)">─────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-testing-providers-e2e-tests-r5" x="1439.6" y="532.4" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-21)">─╮</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="532.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-21)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="556.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-22)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="556.8" textLength="305" clip-path="url(#breeze-testing-providers-e2e-tests-line-22)">--include-success-outputs</text><text class="breeze-testing-providers-e2e-tests-r1" x="402.6" y="556.8" textLength="841.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-22)">Whether&#160;to&#160;include&#160;outputs&#160;of&#160;successful&#160;runs&#160;(not&#160;shown&#160;by&#160;default).</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="556.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-22)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="556.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-22)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="581.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="581.2" textLength="305" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">--github-repository&#160;&#160;&#160;&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r7" x="353.8" y="581.2" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">-g</text><text class="breeze-testing-providers-e2e-tests-r1" x="402.6" y="581.2" textLength="597.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">GitHub&#160;repository&#160;used&#160;to&#160;pull,&#160;push&#160;run&#160;images.&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="1000.4" y="581.2" textLength="305" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">[default:&#160;apache/airflow]</text><text class="breeze-testing-providers-e2e-tests-r8" x="1317.6" y="581.2" textLength="73.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">(TEXT)</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="581.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="581.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-23)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="605.6" textLength="1464" clip-path="url(#breeze-testing-providers-e2e-tests-line-24)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="605.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-24)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="630" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-25)">╭─</text><text class="breeze-testing-providers-e2e-tests-r5" x="24.4" y="630" textLength="195.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-25)">&#160;Common&#160;options&#160;</text><text class="breeze-testing-providers-e2e-tests-r5" x="219.6" y="630" textLength="1220" clip-path="url(#breeze-testing-providers-e2e-tests-line-25)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text class="breeze-testing-providers-e2e-tests-r5" x="1439.6" y="630" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-25)">─╮</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="630" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-25)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="654.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="654.4" textLength="109.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">--verbose</text><text class="breeze-testing-providers-e2e-tests-r7" x="158.6" y="654.4" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">-v</text><text class="breeze-testing-providers-e2e-tests-r1" x="207.4" y="654.4" textLength="585.6" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">Print&#160;verbose&#160;information&#160;about&#160;performed&#160;steps.</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="654.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="654.4" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-26)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="678.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="678.8" textLength="109.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">--dry-run</text><text class="breeze-testing-providers-e2e-tests-r7" x="158.6" y="678.8" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">-D</text><text class="breeze-testing-providers-e2e-tests-r1" x="207.4" y="678.8" textLength="719.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">If&#160;dry-run&#160;is&#160;set,&#160;commands&#160;are&#160;only&#160;printed,&#160;not&#160;executed.</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="678.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="678.8" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-27)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="703.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">│</text><text class="breeze-testing-providers-e2e-tests-r4" x="24.4" y="703.2" textLength="109.8" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">--help&#160;&#160;&#160;</text><text class="breeze-testing-providers-e2e-tests-r7" x="158.6" y="703.2" textLength="24.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">-h</text><text class="breeze-testing-providers-e2e-tests-r1" x="207.4" y="703.2" textLength="329.4" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">Show&#160;this&#160;message&#160;and&#160;exit.</text><text class="breeze-testing-providers-e2e-tests-r5" x="1451.8" y="703.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">│</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="703.2" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-28)">
+</text><text class="breeze-testing-providers-e2e-tests-r5" x="0" y="727.6" textLength="1464" clip-path="url(#breeze-testing-providers-e2e-tests-line-29)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text class="breeze-testing-providers-e2e-tests-r1" x="1464" y="727.6" textLength="12.2" clip-path="url(#breeze-testing-providers-e2e-tests-line-29)">
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_testing_providers-e2e-tests.txt b/dev/breeze/doc/images/output_testing_providers-e2e-tests.txt
new file mode 100644
index 0000000000000..806839eb74ae3
--- /dev/null
+++ b/dev/breeze/doc/images/output_testing_providers-e2e-tests.txt
@@ -0,0 +1 @@
+236c2c2140bb30b55a1c7a74f6044453
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index c3c210d9e8c6b..fada767ddbcaa 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -115,6 +115,7 @@
 )
 from airflow_breeze.utils.path_utils import AIRFLOW_CTL_ROOT_PATH, FILES_PATH, cleanup_python_generated_files
 from airflow_breeze.utils.run_tests import (
+    PROVIDERS_E2E_TESTS_ROOT_PATH,
     TASK_SDK_INTEGRATION_TESTS_ROOT_PATH,
     are_all_test_paths_excluded,
     file_name_from_test_type,
@@ -922,6 +923,173 @@ def task_sdk_integration_tests(
     sys.exit(return_code)
 
 
+def _available_e2e_providers() -> list[str]:
+    return sorted(
+        d.name
+        for d in PROVIDERS_E2E_TESTS_ROOT_PATH.iterdir()
+        if d.is_dir() and (d / "pyproject.toml").exists()
+    )
+
+
+def _build_providers_e2e_compat_image(provider: str, airflow_version: str, python: str) -> str:
+    """Build a lightweight image: released ``apache/airflow:<version>`` + current providers from main.
+
+    Replicates the provider-compatibility approach (current provider code on an older Airflow core)
+    without a full PROD image build — the released image is pulled and the providers are reinstalled
+    from wheels built from main.
+
+    The list of providers to install is read from ``providers-e2e-tests/<provider>/pyproject.toml``
+    under ``[tool.e2e-tests] required-providers``.
+    """
+    import shutil
+    from pathlib import Path
+
+    try:
+        import tomllib
+    except ImportError:
+        import tomli as tomllib  # type: ignore[no-redef]
+
+    from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
+
+    provider_root = PROVIDERS_E2E_TESTS_ROOT_PATH / provider
+    pyproject_path = provider_root / "pyproject.toml"
+    required_providers: list[str] = tomllib.loads(pyproject_path.read_text())["tool"]["e2e-tests"][
+        "required-providers"
+    ]
+    console_print(f"[info]Building provider wheels from main: {required_providers}[/]")
+    run_command(
+        [
+            "breeze",
+            "release-management",
+            "prepare-provider-distributions",
+            *required_providers,
+            "--distribution-format",
+            "wheel",
+            "--skip-tag-check",
+            "--include-not-ready-providers",
+        ],
+        check=True,
+        cwd=AIRFLOW_ROOT_PATH,
+    )
+    provider_dist = provider_root / "provider_dist"
+    if provider_dist.exists():
+        shutil.rmtree(provider_dist)
+    provider_dist.mkdir(parents=True)
+    for provider_id in required_providers:
+        wheel_prefix = f"apache_airflow_providers_{provider_id.replace('.', '_')}-"
+        for wheel in (AIRFLOW_ROOT_PATH / "dist").glob(f"{wheel_prefix}*.whl"):
+            shutil.copy2(wheel, provider_dist / wheel.name)
+
+    base_image = f"apache/airflow:{airflow_version}-python{python}"
+    image_name = f"{provider}-e2e/airflow:{airflow_version}-python{python}"
+    console_print(f"[info]Building {image_name} from {base_image}[/]")
+    run_command(
+        [
+            "docker",
+            "build",
+            "--build-arg",
+            f"AIRFLOW_BASE_IMAGE={base_image}",
+            "-t",
+            image_name,
+            "-f",
+            (provider_root / "Dockerfile").as_posix(),
+            Path(provider_root).as_posix(),
+        ],
+        check=True,
+    )
+    return image_name
+
+
+@testing_group.command(
+    name="providers-e2e-tests",
+    context_settings=dict(
+        ignore_unknown_options=True,
+        allow_extra_args=True,
+    ),
+)
+@click.argument("provider", type=click.Choice(_available_e2e_providers()))
+@option_python
+@option_image_name
+@click.option(
+    "--airflow-version",
+    help="Run against a released Airflow version (e.g. 3.0.6) with current providers from main, "
+    "instead of the default PROD image. Builds a lightweight image from apache/airflow:<version>.",
+    default=None,
+)
+@option_skip_docker_compose_deletion
+@option_skip_mounting_local_volumes
+@click.option(
+    "--down",
+    help="Shuts down the docker-compose setup without running any tests. "
+    "Useful to make sure to free resources.",
+    is_flag=True,
+)
+@option_github_repository
+@option_include_success_outputs
+@option_verbose
+@option_dry_run
+@click.argument("extra_pytest_args", nargs=-1, type=click.Path(path_type=str))
+def providers_e2e_tests(
+    provider: str,
+    python: str,
+    image_name: str | None,
+    airflow_version: str | None,
+    down: bool,
+    skip_docker_compose_deletion: bool,
+    skip_mounting_local_volumes: bool,
+    github_repository: str,
+    include_success_outputs: bool,
+    extra_pytest_args: tuple,
+):
+    """Run provider end-to-end tests against a deployed Airflow stack.
+
+    Defaults to the PROD image (current Airflow + providers from sources). Pass --airflow-version to
+    run against an older released Airflow version with the current providers installed from main.
+    """
+    perform_environment_checks()
+
+    if airflow_version:
+        # prepare_dags.py reads this to drop DAGs that need a newer Airflow than the target.
+        os.environ["E2E_TARGET_AIRFLOW_VERSION"] = airflow_version
+        if image_name is None:
+            image_name = _build_providers_e2e_compat_image(provider, airflow_version, python)
+    else:
+        image_name = image_name or os.environ.get("DOCKER_IMAGE")
+        if not image_name:
+            build_params = BuildProdParams(python=python, github_repository=github_repository)
+            image_name = build_params.airflow_image_name
+
+    if down:
+        env = {
+            **os.environ,
+            "DOCKER_IMAGE": image_name,
+        }
+        down_cmd = [
+            "docker",
+            "compose",
+            "down",
+            "--remove-orphans",
+            "--volumes",
+        ]
+        console_print("[info]Running docker-compose down[/]")
+        run_command(down_cmd, output=None, check=False, env=env, cwd=PROVIDERS_E2E_TESTS_ROOT_PATH / provider)
+        sys.exit(0)
+
+    console_print(f"[info]Running {provider} e2e tests with image: {image_name}[/]")
+    return_code, info = run_docker_compose_tests(
+        image_name=image_name,
+        python_version=python,
+        include_success_outputs=include_success_outputs,
+        extra_pytest_args=extra_pytest_args,
+        skip_docker_compose_deletion=skip_docker_compose_deletion,
+        skip_mounting_local_volumes=skip_mounting_local_volumes,
+        test_type="providers-e2e-tests",
+        provider=provider,
+        skip_image_check=bool(airflow_version),
+    )
+    sys.exit(return_code)
+
+
 @testing_group.command(
     name="airflow-ctl-integration-tests",
     context_settings=dict(
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
index 56a4b17450239..4e4c1601a7443 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
@@ -215,6 +215,26 @@
             ],
         },
     ],
+    "breeze testing providers-e2e-tests": [
+        {
+            "name": "Docker-compose tests flag",
+            "options": [
+                "--image-name",
+                "--python",
+                "--airflow-version",
+                "--skip-docker-compose-deletion",
+                "--skip-mounting-local-volumes",
+                "--down",
+            ],
+        },
+        {
+            "name": "Common CI options",
+            "options": [
+                "--include-success-outputs",
+                "--github-repository",
+            ],
+        },
+    ],
     "breeze testing airflow-ctl-tests": [
         {
             "name": "Test environment",
diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py b/dev/breeze/src/airflow_breeze/utils/run_tests.py
index ef87dd1f6e3a7..d90b5f63dad93 100644
--- a/dev/breeze/src/airflow_breeze/utils/run_tests.py
+++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py
@@ -49,6 +49,8 @@
 
 AIRFLOW_E2E_TESTS_ROOT_PATH = AIRFLOW_ROOT_PATH / "airflow-e2e-tests"
 
+PROVIDERS_E2E_TESTS_ROOT_PATH = AIRFLOW_ROOT_PATH / "providers-e2e-tests"
+
 AIRFLOW_CTL_TESTS_ROOT_PATH = AIRFLOW_ROOT_PATH / "airflow-ctl-tests"
 
 IGNORE_DB_INIT_FOR_TEST_GROUPS = [
@@ -123,6 +125,7 @@ def run_docker_compose_tests(
     test_type: str = "docker-compose",
     skip_image_check: bool = False,
     test_mode: str = "basic",
+    provider: str = "",
 ) -> tuple[int, str]:
     if not skip_image_check:
         command_result = run_command(
@@ -165,6 +168,9 @@ def run_docker_compose_tests(
     elif test_type == "airflow-e2e-tests":
         test_path = Path("tests") / "airflow_e2e_tests" / f"{test_mode}_tests"
         cwd = AIRFLOW_E2E_TESTS_ROOT_PATH.as_posix()
+    elif test_type == "providers-e2e-tests":
+        test_path = Path("tests")
+        cwd = (PROVIDERS_E2E_TESTS_ROOT_PATH / provider).as_posix()
     elif test_type == "airflow-ctl-integration":
         test_path = Path("tests") / "airflowctl_tests"
         cwd = AIRFLOW_CTL_TESTS_ROOT_PATH.as_posix()
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 89baca951458e..1b70412b51402 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -132,6 +132,7 @@ class FileGroupForCi(Enum):
     EVENT_DRIVEN_E2E_FILES = auto()
     JAVA_SDK_E2E_FILES = auto()
     GO_SDK_E2E_FILES = auto()
+    PROVIDERS_E2E_OPENLINEAGE_FILES = auto()
     ALL_PYPROJECT_TOML_FILES = auto()
     ALL_PYTHON_FILES = auto()
     ALL_SOURCE_FILES = auto()
@@ -250,6 +251,13 @@ def __hash__(self):
             r"^task-sdk/src/airflow/sdk/coordinators/_subprocess\.py$",
             r"^task-sdk/src/airflow/sdk/coordinators/executable/.*",
         ],
+        FileGroupForCi.PROVIDERS_E2E_OPENLINEAGE_FILES: [
+            r"^providers-e2e-tests/openlineage/.*",
+            r"^providers/openlineage/.*",
+            r"^providers/common/compat/.*",
+            r"^providers/common/io/.*",
+            r"^providers/common/sql/.*",
+        ],
         FileGroupForCi.PYTHON_PRODUCTION_FILES: [
             # Production Python source the runtime ships — excludes tests, docs,
             # dev tooling, and generated files within those trees. Used by
@@ -1037,6 +1045,10 @@ def run_java_sdk_e2e_tests(self) -> bool:
     def run_go_sdk_e2e_tests(self) -> bool:
         return self._should_be_run(FileGroupForCi.GO_SDK_E2E_FILES)
 
+    @cached_property
+    def run_providers_e2e_tests_openlineage(self) -> bool:
+        return self._should_be_run(FileGroupForCi.PROVIDERS_E2E_OPENLINEAGE_FILES)
+
     @cached_property
     def run_amazon_tests(self) -> bool:
         if self.providers_test_types_list_as_strings_in_json == "[]":
@@ -1179,6 +1191,7 @@ def prod_image_build(self) -> bool:
             or self.run_event_driven_e2e_tests
             or self.run_java_sdk_e2e_tests
             or self.run_go_sdk_e2e_tests
+            or self.run_providers_e2e_tests_openlineage
             or self.run_ui_e2e_tests
         )
 
diff --git a/dev/breeze/tests/test_selective_checks.py b/dev/breeze/tests/test_selective_checks.py
index 43a5e0081bccf..8a6dbd8f7bb25 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -1201,7 +1201,9 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str):
                 "python-versions": f"['{DEFAULT_PYTHON_MAJOR_MINOR_VERSION}']",
                 "python-versions-list-as-string": DEFAULT_PYTHON_MAJOR_MINOR_VERSION,
                 "ci-image-build": "true",
-                "prod-image-build": "false",
+                # common providers feed OpenLineage e2e tests, which need the PROD image.
+                "prod-image-build": "true",
+                "run-providers-e2e-tests-openlineage": "true",
                 "run-helm-tests": "false",
                 "run-unit-tests": "true",
                 "run-amazon-tests": "false",
@@ -1472,6 +1474,38 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str):
             },
             id="Run go e2e tests when ExecutableCoordinator changes",
         ),
+        pytest.param(
+            ("providers/openlineage/src/airflow/providers/openlineage/plugins/adapter.py",),
+            {
+                "run-providers-e2e-tests-openlineage": "true",
+                "prod-image-build": "true",
+            },
+            id="Run OpenLineage e2e tests for openlineage provider change",
+        ),
+        pytest.param(
+            ("providers/common/compat/src/airflow/providers/common/compat/sdk.py",),
+            {
+                "run-providers-e2e-tests-openlineage": "true",
+                "prod-image-build": "true",
+            },
+            id="Run OpenLineage e2e tests for common provider change",
+        ),
+        pytest.param(
+            ("providers-e2e-tests/openlineage/tests/openlineage_e2e_tests/test_openlineage_e2e.py",),
+            {
+                "run-providers-e2e-tests-openlineage": "true",
+                "prod-image-build": "true",
+            },
+            id="Run OpenLineage e2e tests when the e2e harness changes",
+        ),
+        pytest.param(
+            ("providers/ftp/src/airflow/providers/ftp/hooks/ftp.py",),
+            {
+                "run-providers-e2e-tests-openlineage": "false",
+                "prod-image-build": "false",
+            },
+            id="Do not run OpenLineage e2e tests for unrelated provider change",
+        ),
         (
             pytest.param(
                 ("devel-common/pyproject.toml",),
diff --git a/providers-e2e-tests/README.md b/providers-e2e-tests/README.md
new file mode 100644
index 0000000000000..588e13be45943
--- /dev/null
+++ b/providers-e2e-tests/README.md
@@ -0,0 +1,111 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# Provider end-to-end tests
+
+Each subdirectory here is a self-contained e2e suite for one provider, that can be run against a real built
+Airflow image (`$DOCKER_IMAGE`) rather than the in-process `dag.test()` path used by provider system tests.
+
+What "e2e" means, and how the stack under test gets stood up, is entirely up to the provider — see that provider's
+own `README.md` for what its suite tests, how it works, and how to interpret its results. For an example implementation
+(a docker-compose deployed stack), see [`openlineage`](openlineage) and its [README](openlineage/README.md).
+
+## Running locally
+
+Docker must be running. Every provider is driven by the same breeze command:
+
+```bash
+# Default: PROD image (current Airflow from sources + providers from sources)
+breeze testing providers-e2e-tests <provider>
+
+# An older released Airflow version, with the current providers installed from main
+# (builds apache/airflow:<ver> + current provider wheels — no full PROD build)
+breeze testing providers-e2e-tests <provider> --airflow-version 3.1.8
+
+# Tear the stack down
+breeze testing providers-e2e-tests <provider> --down
+```
+
+`<provider>` is any subdirectory of `providers-e2e-tests/` with a `pyproject.toml` — run
+`breeze testing providers-e2e-tests --help` to see the current choices.
+
+Useful flags, passed to every provider's pytest run as env vars for its own harness to honor:
+
+- `--skip-docker-compose-deletion` (`SKIP_DOCKER_COMPOSE_DELETION`) — keep the stack up after the run to inspect it.
+- `--skip-mounting-local-volumes` (`SKIP_MOUNTING_LOCAL_VOLUMES`) — run against the image only (what CI does).
+Omit it during dev to mount and hot-reload local sources.
+- Anything after the provider is passed through to pytest (e.g. `... <provider> -- -s`).
+
+The first run builds the PROD image; subsequent runs are much faster.
+When running non-interactively without a pre-built image, prefix `ANSWER=yes` so the image-build prompt auto-confirms.
+
+## How this is wired into CI
+
+```text
+ci-amd.yml / ci-arm.yml
+  └─ additional-prod-image-tests (job) ──uses──▶ additional-prod-image-tests.yml
+       └─ test-providers-e2e-tests-<provider>[-compat] (job) ──uses──▶ providers-e2e-tests.yml
+            └─ runs: breeze testing providers-e2e-tests <provider> [--airflow-version <ver>]
+```
+
+`additional-prod-image-tests.yml` is a single reusable workflow called identically from both
+`ci-amd.yml` and `ci-arm.yml`, so any job defined there — including each provider's e2e tests —
+automatically gets both ARM and AMD coverage from one job definition.
+[`scripts/ci/prek/check_ci_workflows_in_sync.py`](../scripts/ci/prek/check_ci_workflows_in_sync.py)
+enforces that the two `ci-*.yml` files stay identical outside a small documented allowlist, so
+don't add provider-e2e jobs directly to `ci-amd.yml`/`ci-arm.yml` — they belong in
+`additional-prod-image-tests.yml`.
+
+Each provider's suite is gated by its own `run-providers-e2e-tests-<provider>` selective-checks output
+(computed in `dev/breeze/src/airflow_breeze/utils/selective_checks.py` from the files changed in a
+PR) — it always runs on canary/`main` runs, and on a PR only when that provider (or a file group it
+depends on, e.g. `common` providers) changed.
+
+## Adding a new provider
+
+1. Create `providers-e2e-tests/<provider>/`. The breeze harness (`breeze testing
+   providers-e2e-tests`, see `dev/breeze/src/airflow_breeze/commands/testing_commands.py` and
+   `run_docker_compose_tests` in `dev/breeze/src/airflow_breeze/utils/run_tests.py`) only requires:
+   - `pyproject.toml` — its presence is what makes `<provider>` a valid choice
+     (`_available_e2e_providers()`). It only needs a `[tool.e2e-tests] required-providers = [...]`
+     entry if the provider supports `--airflow-version` compat mode — that lists the provider
+     distributions to build from `main` for that mode (see `_build_providers_e2e_compat_image`).
+   - `tests/` — a pytest suite. Breeze runs `pytest tests/` from this directory with `$DOCKER_IMAGE`
+     (and the flags above) set as env vars; what it does with that image is entirely up to the
+     provider.
+   - `Dockerfile` — only needed if the provider supports `--airflow-version` compat mode: breeze
+     builds `apache/airflow:<ver>` + the provider wheels from `main` using it.
+2. Wire the trigger into selective checks
+   (`dev/breeze/src/airflow_breeze/utils/selective_checks.py`): add a `FileGroupForCi` member and
+   its regex patterns, a `run_providers_e2e_tests_<provider>` cached property, and expose it as a
+   `run-providers-e2e-tests-<provider>` output — mirror the existing `PROVIDERS_E2E_OPENLINEAGE_FILES` /
+   `run_providers_e2e_tests_openlineage` pair. Update
+   [`dev/breeze/tests/test_selective_checks.py`](../dev/breeze/tests/test_selective_checks.py) and
+   [`dev/breeze/doc/ci/04_selective_checks.md`](../dev/breeze/doc/ci/04_selective_checks.md) in the
+   same PR.
+3. Thread the new output through **both** `ci-amd.yml` and `ci-arm.yml` identically: add it to the
+   `build-info` job's `outputs:` and to the `additional-prod-image-tests` job's `with:` block.
+4. Add the job(s) to `.github/workflows/additional-prod-image-tests.yml`, named
+   `test-providers-e2e-tests-<provider>` (and `test-providers-e2e-tests-<provider>-compat` for the
+   compat matrix job, if any) — mirror the existing `test-providers-e2e-tests-openlineage` pair.
+   Call `providers-e2e-tests.yml` with `provider: "<name>"` and
+   `provider-display-name: "<Display Name>"`, gated on
+   `inputs.canary-run == 'true' || inputs.run-providers-e2e-tests-<provider> == 'true'`. The compat
+   job additionally needs a `strategy.matrix` over `inputs.providers-compatibility-tests-matrix` and
+   `airflow-version: ${{ matrix.compat.airflow-version }}`.
diff --git a/providers-e2e-tests/openlineage/.gitignore b/providers-e2e-tests/openlineage/.gitignore
new file mode 100644
index 0000000000000..fdf48551bf27f
--- /dev/null
+++ b/providers-e2e-tests/openlineage/.gitignore
@@ -0,0 +1,7 @@
+.env
+*.iml
+# DAGs are sourced from providers/openlineage/tests/system at runtime by prepare_dags.py
+dags/
+logs/
+# Provider wheels built from main for older-version (compat) image builds
+provider_dist/
diff --git a/providers-e2e-tests/openlineage/.pre-commit-config.yaml b/providers-e2e-tests/openlineage/.pre-commit-config.yaml
new file mode 100644
index 0000000000000..85fd7040301b9
--- /dev/null
+++ b/providers-e2e-tests/openlineage/.pre-commit-config.yaml
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+default_stages: [pre-commit, pre-push]
+minimum_prek_version: '0.3.4'
+default_language_version:
+  python: python3
+repos:
+  - repo: local
+    hooks:
+      - id: mypy-openlineage-e2e-tests
+        name: Run mypy for providers-e2e-tests/openlineage
+        language: python
+        entry: >-
+          ../../scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py
+          providers-e2e-tests/openlineage
+        pass_filenames: false
+        files: ^.*\.py$
+        require_serial: true
diff --git a/providers-e2e-tests/openlineage/Dockerfile b/providers-e2e-tests/openlineage/Dockerfile
new file mode 100644
index 0000000000000..3c1d772cf4b06
--- /dev/null
+++ b/providers-e2e-tests/openlineage/Dockerfile
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Lightweight image for running the OpenLineage e2e tests against an OLDER, released Airflow version.
+# Instead of building a full PROD image, it starts from the published apache/airflow:<version> image
+# and reinstalls the providers under test (OpenLineage + the providers the system-test DAGs rely on)
+# from wheels built from main — so the current provider code runs on an older Airflow core, the same
+# idea as the provider compatibility tests. The default (prod) run does NOT use this Dockerfile; it
+# uses the breeze PROD image with current sources.
+ARG AIRFLOW_BASE_IMAGE
+FROM ${AIRFLOW_BASE_IMAGE}
+
+COPY --chown=airflow:0 provider_dist/ /tmp/provider_dist/
+RUN pip install --no-cache-dir --upgrade /tmp/provider_dist/*.whl
diff --git a/providers-e2e-tests/openlineage/README.md b/providers-e2e-tests/openlineage/README.md
new file mode 100644
index 0000000000000..76c1bc5a56485
--- /dev/null
+++ b/providers-e2e-tests/openlineage/README.md
@@ -0,0 +1,75 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# OpenLineage provider end-to-end tests
+
+These tests deploy a **real, multi-process Airflow** (via docker-compose) and run the OpenLineage
+provider's system-test DAGs against it — exercising the integration the way it actually runs in a
+deployment (separate scheduler / dag-processor / triggerer / api-server / workers), rather than the
+in-process `dag.test()` path.
+
+See the [top-level README](../README.md) for how to run any provider's e2e suite with breeze, how
+this is wired into CI, and what's involved in adding a new provider.
+
+## How it works
+
+- The DAGs, `OpenLineageTestOperator`, `VariableTransport`, and expected-event JSON are the ones in
+  [`providers/openlineage/tests/system/openlineage`](../../providers/openlineage/tests/system/openlineage) —
+  that directory stays the single source of truth. `prepare_dags.py` copies them into a (gitignored)
+  `dags/` folder at runtime and strips the pytest-only `get_test_run` footer so they parse in a
+  deployment.
+- OpenLineage is configured to emit through the test `VariableTransport` (events land in Airflow
+  Variables); each DAG's terminal `OpenLineageTestOperator` task validates the emitted events against
+  the expected templates. **A DAG run that ends `success` means its lineage matched.**
+- `harness.py` triggers every DAG through the REST API, waits for completion, retries a failed DAG
+  once, and asserts all expected DAGs succeeded.
+- A small `MockVersionedLocalDagBundle` provides a versioned bundle (so `dag_bundle_version` is
+  populated) while still serving DAGs from the local dags folder.
+
+## Checking results
+
+- **pytest summary:** `1 passed` means every expected DAG is green. On failure it prints the
+  offenders, e.g. `... not successful: {'openlineage_docs_file_dag': 'failed'}`, and logs
+  `⚠ DAGs that passed only on retry (flaky first run): [...]`.
+- **On failure, the relevant task logs are printed right in the pytest output** (`check_events` plus
+  any task that didn't succeed, filtered to warning/error lines — see `tests/task_logs.py`) — no
+  need to open the log artifact for the common case, e.g. the exact mismatch
+  (``Path `job > facets > ...`: expected X but got Y``).
+- **Per-DAG logs** are still written in full to `logs/dag_id=<dag>/.../task_id=<task>/attempt=1.log`
+  for deeper digging beyond what gets printed.
+- With `--skip-docker-compose-deletion`, the stack stays up: get a token from
+  `curl -s http://localhost:8080/auth/token` and check `GET /api/v2/importErrors` for DAG parse
+  errors, or browse the runs in the UI at <http://localhost:8080> (login `airflow` / `airflow`).
+
+## Version-specific DAGs
+
+Some DAGs require a newer Airflow than the compat targets and are dropped for older versions in
+`prepare_dags.py` (`MIN_AIRFLOW_VERSION_FOR_DAG`) — e.g. `example_openlineage_hitl_dag` needs 3.1+
+(its operators import-raise on older cores). Add an entry there when a new DAG is version-gated.
+
+## Layout
+
+```text
+docker-compose.yaml         # the deployed stack (postgres + apiserver + scheduler + dag-processor + triggerer)
+docker-compose-local.yaml   # overlay that mounts local sources (used unless --skip-mounting-local-volumes)
+Dockerfile                  # lightweight image for --airflow-version: apache/airflow:<ver> + current providers
+prepare_dags.py             # sources DAGs from the provider system tests into dags/ at runtime
+dags_extra/                 # harness-only DAGs/modules (warmup DAG, versioned bundle) copied into dags/
+tests/                      # conftest (compose lifecycle), harness, constants, and the pytest entrypoint
+```
diff --git a/providers-e2e-tests/openlineage/dags_extra/e2e_versioned_bundle.py b/providers-e2e-tests/openlineage/dags_extra/e2e_versioned_bundle.py
new file mode 100644
index 0000000000000..20d1c022e4582
--- /dev/null
+++ b/providers-e2e-tests/openlineage/dags_extra/e2e_versioned_bundle.py
@@ -0,0 +1,62 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""A minimal *versioned* DAG bundle for the OpenLineage e2e deployment.
+
+example_openlineage_versioned_dag asserts that OpenLineage emits a non-null ``dag_bundle_version``,
+which Airflow only populates when the bundle reports ``supports_versioning = True``. The real
+versioned bundle (the git provider's GitDagBundle) serves DAGs from a separate bundle storage path,
+which would break the ``PYTHONPATH=/opt/airflow/dags`` that the test ``VariableTransport`` class path
+relies on.
+
+This bundle instead keeps serving DAGs straight from the local dags folder (so PYTHONPATH stays
+valid) and reports a deterministic, content-derived version string — enough to exercise the
+version-related OpenLineage attributes without standing up a real VCS-backed bundle. The module is
+copied into the dags folder by prepare_dags.py so it is importable via PYTHONPATH.
+"""
+
+from __future__ import annotations
+
+import hashlib
+from typing import TYPE_CHECKING
+
+from airflow.dag_processing.bundles.local import LocalDagBundle
+
+if TYPE_CHECKING:
+    from airflow.dag_processing.bundles.base import BundleVersion
+
+
+class MockVersionedLocalDagBundle(LocalDagBundle):
+    """LocalDagBundle that reports a content-derived version so ``dag_bundle_version`` is populated."""
+
+    supports_versioning = True
+
+    # Intentionally broadens LocalDagBundle.get_current_version (which returns None, as the base
+    # bundle is non-versioned) to report a real version.
+    def get_current_version(self) -> str | BundleVersion:  # type: ignore[override]
+        digest = hashlib.sha1(usedforsecurity=False)
+        for dag_file in sorted(self.path.rglob("*.py")):
+            digest.update(dag_file.relative_to(self.path).as_posix().encode())
+            digest.update(str(dag_file.stat().st_size).encode())
+        version = digest.hexdigest()[:12]
+        try:
+            # Preferred on newer cores; bare str is deprecated for versioned bundles.
+            from airflow.dag_processing.bundles.base import BundleVersion
+
+            return BundleVersion(version=version)
+        except ImportError:
+            # Older Airflow cores (compat runs) predate BundleVersion — a bare string still works.
+            return version
diff --git a/providers-e2e-tests/openlineage/dags_extra/openlineage_warmup_dag.py b/providers-e2e-tests/openlineage/dags_extra/openlineage_warmup_dag.py
new file mode 100644
index 0000000000000..22ce6bb2a92fe
--- /dev/null
+++ b/providers-e2e-tests/openlineage/dags_extra/openlineage_warmup_dag.py
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Warmup DAG triggered before all others to verify the worker is ready to pick up tasks.
+
+This DAG is specific to the deployed e2e harness (it is not part of the provider system
+tests). It is copied into the generated dags folder by prepare_dags.py.
+"""
+
+from __future__ import annotations
+
+from datetime import datetime
+
+from airflow import DAG
+from airflow.providers.standard.operators.python import PythonOperator
+
+DAG_ID = "openlineage_warmup_dag"
+
+with DAG(
+    dag_id=DAG_ID,
+    start_date=datetime(2021, 1, 1),
+    schedule=None,
+    catchup=False,
+    default_args={"retries": 0},
+) as dag:
+    PythonOperator(task_id="noop", python_callable=lambda: None)
diff --git a/providers-e2e-tests/openlineage/docker-compose-local.yaml b/providers-e2e-tests/openlineage/docker-compose-local.yaml
new file mode 100644
index 0000000000000..70d543f809a96
--- /dev/null
+++ b/providers-e2e-tests/openlineage/docker-compose-local.yaml
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+# If the image is locally built from the repo we can mount sources of Airflow and the
+# OpenLineage provider so we do not have to rebuild the image on every change.
+services:
+  airflow-init:
+    volumes:
+      - ../../airflow-core/src:/opt/airflow/airflow-core/src
+      - ../../task-sdk/src:/opt/airflow/task-sdk/src
+      - ../../providers/openlineage/src:/opt/airflow/providers/openlineage/src
+    environment:
+      - DEV_MODE=true
+  airflow-apiserver:
+    volumes:
+      - ../../airflow-core/src:/opt/airflow/airflow-core/src
+      - ../../task-sdk/src:/opt/airflow/task-sdk/src
+      - ../../providers/openlineage/src:/opt/airflow/providers/openlineage/src
+    environment:
+      - DEV_MODE=true
+  airflow-scheduler:
+    volumes:
+      - ../../airflow-core/src:/opt/airflow/airflow-core/src
+      - ../../task-sdk/src:/opt/airflow/task-sdk/src
+      - ../../providers/openlineage/src:/opt/airflow/providers/openlineage/src
+    environment:
+      - DEV_MODE=true
+  airflow-dag-processor:
+    volumes:
+      - ../../airflow-core/src:/opt/airflow/airflow-core/src
+      - ../../task-sdk/src:/opt/airflow/task-sdk/src
+      - ../../providers/openlineage/src:/opt/airflow/providers/openlineage/src
+    environment:
+      - DEV_MODE=true
+  airflow-triggerer:
+    volumes:
+      - ../../airflow-core/src:/opt/airflow/airflow-core/src
+      - ../../task-sdk/src:/opt/airflow/task-sdk/src
+      - ../../providers/openlineage/src:/opt/airflow/providers/openlineage/src
+    environment:
+      - DEV_MODE=true
diff --git a/providers-e2e-tests/openlineage/docker-compose.yaml b/providers-e2e-tests/openlineage/docker-compose.yaml
new file mode 100644
index 0000000000000..8adcc95e80884
--- /dev/null
+++ b/providers-e2e-tests/openlineage/docker-compose.yaml
@@ -0,0 +1,181 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+---
+x-airflow-common:
+  &airflow-common
+  image: ${AIRFLOW_IMAGE_NAME}
+  env_file:
+    - ${ENV_FILE_PATH:-.env}
+  environment:
+    &airflow-common-env
+    AIRFLOW__CORE__EXECUTOR: LocalExecutor
+    # yamllint disable rule:line-length
+    AIRFLOW__CORE__AUTH_MANAGER: 'airflow.api_fastapi.auth.managers.simple.simple_auth_manager.SimpleAuthManager'
+    AIRFLOW__CORE__SIMPLE_AUTH_MANAGER_ALL_ADMINS: 'true'
+    AIRFLOW__DATABASE__SQL_ALCHEMY_CONN: postgresql+psycopg2://airflow:airflow@postgres/airflow
+    AIRFLOW__CORE__FERNET_KEY: ${FERNET_KEY}
+    AIRFLOW__CORE__LOAD_EXAMPLES: 'false'
+    AIRFLOW__CORE__DAGS_FOLDER: '/opt/airflow/dags'
+    AIRFLOW__CORE__EXECUTION_API_SERVER_URL: 'http://airflow-apiserver:8080/execution/'
+    AIRFLOW__API__BASE_URL: 'http://airflow-apiserver:8080/'
+    # Do not reuse this secret in any deployment — it exists only for this ephemeral CI/test stack.
+    AIRFLOW__API_AUTH__JWT_SECRET: 'test-secret-key-for-testing'
+    # The OpenLineage system-test DAGs (and the custom VariableTransport / OpenLineageTestOperator
+    # they import) live under <dags>/system/openlineage; the dags folder is added to PYTHONPATH so
+    # both DAG parsing and the dotted-path transport class resolve in every component.
+    PYTHONPATH: '/opt/airflow/dags'
+    # Route OpenLineage events to the test VariableTransport (stores events in Airflow Variables,
+    # read back and validated by the in-DAG OpenLineageTestOperator). The transport type is the
+    # fully-qualified class path; the OpenLineage client imports it directly.
+    AIRFLOW__OPENLINEAGE__TRANSPORT: '{"type": "system.openlineage.transport.variable.VariableTransport"}'
+    AIRFLOW__OPENLINEAGE__DISABLED: 'false'
+    # Deployment-wide policy required by the two conf-driven policy DAGs. Each rule is scoped by
+    # dag_id, so both rules coexist and do not affect any other DAG.
+    AIRFLOW__OPENLINEAGE__EMISSION_POLICY: '[{"scope": {"dag_id": "openlineage_policy_conf_locked_dag"}, "locked": true, "controls": {"include_source_code": false}}, {"scope": {"dag_id": "openlineage_policy_conf_source_code_dag"}, "controls": {"include_source_code": false}}]'
+    AIRFLOW__OPENLINEAGE__EXECUTION_TIMEOUT: '60'
+    # Both control how quickly the dag-processor picks up dags: REFRESH_INTERVAL is how often it
+    # re-scans the folder for new/removed files, MIN_FILE_PROCESS_INTERVAL is how often it re-parses
+    # a file it already knows about. Tightened from the 300s/30s defaults so the e2e run doesn't wait
+    # on the dag-processor's own cadence on top of everything else.
+    AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL: '3'
+    AIRFLOW__DAG_PROCESSOR__MIN_FILE_PROCESS_INTERVAL: '3'
+    # openlineage_defer_simple_dag's TimeDeltaSensor defaults to a 180s delta so it reliably defers at least
+    # once even before a worker picks up the task; tighten it here since we do not need to wait for worker.
+    SYSTEM_TESTS_OL_TIMEDELTA: '60'
+    # Every DAG's dagrun_timeout (system.openlineage.constants.DEFAULT_DAGRUN_TIMEOUT)
+    # defaults to 10 minutes; tighten it here since we do not need to wait for worker.
+    SYSTEM_TESTS_OL_DAGRUN_TIMEOUT_MINUTES: '5'
+    AIRFLOW__CORE__TASK_SUCCESS_OVERTIME: '70'
+    # Serve the dags folder through a versioned bundle so example_openlineage_versioned_dag gets a
+    # non-null dag_bundle_version. MockVersionedLocalDagBundle still serves from /opt/airflow/dags
+    # (keeping PYTHONPATH valid); it is copied into the dags folder by prepare_dags.py.
+    AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST: '[{"name": "dags-folder", "classpath": "e2e_versioned_bundle.MockVersionedLocalDagBundle", "kwargs": {}}]'
+    # The breeze PROD image installs every provider from source, including informatica, whose
+    # listener fires on every task and errors without its connection — suppressing OL events.
+    # A curated deployment (e.g. Astro) does not ship it; disable its listener to match.
+    AIRFLOW__INFORMATICA__LISTENER_DISABLED: 'true'
+    HOST_OS: ${HOST_OS:-linux}
+  user: "${AIRFLOW_UID:-50000}:0"
+  volumes:
+    - ./dags:/opt/airflow/dags
+    - ./logs:/opt/airflow/logs
+    # example_openlineage_docs_file_dag sets doc_md="dag_doc.md", which Airflow resolves with a
+    # CWD-relative open(); components run with CWD=/opt/airflow. prepare_dags.py generates a clean,
+    # license-free dag_doc.md (matching the expected event) in the dags folder; expose it here.
+    - ./dags/dag_doc.md:/opt/airflow/dag_doc.md
+  depends_on:
+    &airflow-common-depends-on
+    postgres:
+      condition: service_healthy
+
+services:
+  postgres:
+    image: postgres:13
+    environment:
+      POSTGRES_USER: airflow
+      POSTGRES_PASSWORD: airflow
+      POSTGRES_DB: airflow
+    healthcheck:
+      test: ["CMD", "pg_isready", "-U", "airflow"]
+      interval: 10s
+      retries: 5
+      start_period: 5s
+    restart: always
+
+  airflow-init:
+    <<: *airflow-common
+    entrypoint: /bin/bash
+    command:
+      - -c
+      - |
+        echo "Creating missing opt dirs if missing:"
+        mkdir -v -p /opt/airflow/{logs,dags,plugins,config}
+        echo "Airflow version:"
+        /entrypoint airflow version
+        echo "Running airflow config list to create default config file if missing."
+        /entrypoint airflow config list >/dev/null
+        if [ "${HOST_OS}" == "linux" ]; then
+            echo "Change ownership of files in /opt/airflow to ${AIRFLOW_UID}:0"
+            chown -R "${AIRFLOW_UID}:0" /opt/airflow/
+        else
+            echo "Skipping changing ownership as platform is not linux (${HOST_OS})"
+        fi
+    environment:
+      <<: *airflow-common-env
+      _AIRFLOW_DB_MIGRATE: 'true'
+      _AIRFLOW_WWW_USER_CREATE: 'true'
+      _AIRFLOW_WWW_USER_USERNAME: ${_AIRFLOW_WWW_USER_USERNAME:-airflow}
+      _AIRFLOW_WWW_USER_PASSWORD: ${_AIRFLOW_WWW_USER_PASSWORD:-airflow}
+      _PIP_ADDITIONAL_REQUIREMENTS: ''
+    user: "0:0"
+    depends_on:
+      <<: *airflow-common-depends-on
+
+  airflow-apiserver:
+    <<: *airflow-common
+    command: api-server
+    ports:
+      - "8080:8080"
+    healthcheck:
+      test: ["CMD", "curl", "--fail", "http://localhost:8080/api/v2/monitor/health"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+      start_period: 30s
+    restart: always
+    depends_on:
+      <<: *airflow-common-depends-on
+      airflow-init:
+        condition: service_completed_successfully
+
+  airflow-scheduler:
+    <<: *airflow-common
+    command: scheduler
+    healthcheck:
+      test: ["CMD", "airflow", "jobs", "check", "--job-type", "SchedulerJob"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+    restart: always
+    depends_on:
+      postgres:
+        condition: service_healthy
+      airflow-init:
+        condition: service_completed_successfully
+
+  airflow-dag-processor:
+    <<: *airflow-common
+    command: dag-processor
+    depends_on:
+      <<: *airflow-common-depends-on
+      airflow-init:
+        condition: service_completed_successfully
+
+  airflow-triggerer:
+    <<: *airflow-common
+    command: triggerer
+    healthcheck:
+      test: ["CMD", "airflow", "jobs", "check", "--job-type", "TriggererJob"]
+      interval: 30s
+      timeout: 10s
+      retries: 5
+    restart: always
+    depends_on:
+      <<: *airflow-common-depends-on
+      airflow-init:
+        condition: service_completed_successfully
diff --git a/providers-e2e-tests/openlineage/prepare_dags.py b/providers-e2e-tests/openlineage/prepare_dags.py
new file mode 100644
index 0000000000000..228cd16943d72
--- /dev/null
+++ b/providers-e2e-tests/openlineage/prepare_dags.py
@@ -0,0 +1,121 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Generate the dags folder for the OpenLineage e2e deployment from the provider system tests.
+
+The OpenLineage system-test DAGs in ``providers/openlineage/tests/system/openlineage`` are the
+single source of truth. They are *designed* to run in-process via pytest, so each one ends with a
+``get_test_run`` block that imports ``tests_common`` — which is not present in the PROD image. This
+script copies the whole ``system`` package into the e2e dags folder and strips that pytest-only
+footer so the DAGs parse and run in a real deployment.
+
+The package layout is preserved as ``<dags>/system/openlineage/...`` and the dags folder is added to
+``PYTHONPATH`` in docker-compose, so the DAGs' ``from system.openlineage... import ...`` and the
+dotted-path ``VariableTransport`` both resolve unchanged — no import rewriting needed.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+from pathlib import Path
+
+HERE = Path(__file__).resolve().parent
+AIRFLOW_ROOT_PATH = HERE.parents[1]
+
+# Example DAGs that need a minimum Airflow version. When running against an older version, the DAG
+# file is removed entirely — a module-level import (e.g. the HITL operators) would otherwise raise at
+# parse time and take the whole dag-processor down, not just skip that one DAG.
+MIN_AIRFLOW_VERSION_FOR_DAG: dict[str, tuple[int, int]] = {
+    # HITL operators import-raise on Airflow < 3.1 (taking the dag-processor down at parse time).
+    "example_openlineage_hitl_dag.py": (3, 1),
+}
+
+SYSTEM_TESTS_SOURCE = AIRFLOW_ROOT_PATH / "providers" / "openlineage" / "tests" / "system"
+DAGS_DEST = HERE / "dags"
+# Harness-only modules (warmup DAG, versioned bundle) that are not part of the provider system tests.
+DAGS_EXTRA_SOURCE = HERE / "dags_extra"
+
+# The marker after which everything is Airflow's in-process test harness (pytest-only).
+PYTEST_FOOTER_MARKER = "from tests_common.test_utils.system_tests import get_test_run"
+
+
+def _target_airflow_version() -> tuple[int, int] | None:
+    """Parse the (major, minor) of the targeted Airflow version, or None for the default/prod run."""
+    raw = os.environ.get("E2E_TARGET_AIRFLOW_VERSION", "").strip()
+    if not raw:
+        return None
+    parts = raw.split(".")
+    if len(parts) < 2 or not (parts[0].isdigit() and parts[1].isdigit()):
+        return None
+    return int(parts[0]), int(parts[1])
+
+
+def _strip_pytest_footer(dag_file: Path) -> None:
+    lines = dag_file.read_text().splitlines(keepends=True)
+    kept: list[str] = []
+    for line in lines:
+        if PYTEST_FOOTER_MARKER in line:
+            break
+        kept.append(line)
+    dag_file.write_text("".join(kept))
+
+
+def prepare_dags() -> Path:
+    """Populate the e2e dags folder from the provider system tests. Returns the dags folder path."""
+    if not SYSTEM_TESTS_SOURCE.is_dir():
+        raise FileNotFoundError(f"OpenLineage system tests not found at {SYSTEM_TESTS_SOURCE}")
+
+    if DAGS_DEST.exists():
+        shutil.rmtree(DAGS_DEST)
+    DAGS_DEST.mkdir(parents=True)
+
+    # Copy the whole `system` package so `system.openlineage.{operator,transport,expected_events}`
+    # imports resolve from the dags folder.
+    shutil.copytree(SYSTEM_TESTS_SOURCE, DAGS_DEST / "system")
+
+    openlineage_dir = DAGS_DEST / "system" / "openlineage"
+
+    # The pytest conftest imports `pytest`, which is absent from the PROD image; drop it so the
+    # dag-processor does not choke on it.
+    (openlineage_dir / "conftest.py").unlink(missing_ok=True)
+
+    target_version = _target_airflow_version()
+    if target_version is not None:
+        for dag_file_name, min_version in MIN_AIRFLOW_VERSION_FOR_DAG.items():
+            if target_version < min_version:
+                (openlineage_dir / dag_file_name).unlink(missing_ok=True)
+
+    for dag_file in openlineage_dir.glob("example_openlineage_*.py"):
+        _strip_pytest_footer(dag_file)
+
+    for extra_file in DAGS_EXTRA_SOURCE.glob("*.py"):
+        shutil.copy2(extra_file, DAGS_DEST / extra_file.name)
+
+    # example_openlineage_docs_file_dag uses doc_md="dag_doc.md" and the expected event's
+    # documentation.description is exactly "# MD doc file". The provider's own dag_doc.md carries an
+    # Apache license header (required for repo files), so OpenLineage would emit the header too.
+    # Generate a clean, license-free copy here (it lives only in the gitignored dags folder, never in
+    # the repo); docker-compose mounts it into the parsing CWD so doc_md resolves to this content.
+    (DAGS_DEST / "dag_doc.md").write_text("# MD doc file")
+
+    return DAGS_DEST
+
+
+if __name__ == "__main__":
+    dest = prepare_dags()
+    print(f"Prepared OpenLineage e2e dags in {dest}")
diff --git a/providers-e2e-tests/openlineage/pyproject.toml b/providers-e2e-tests/openlineage/pyproject.toml
new file mode 100644
index 0000000000000..981a424dfab15
--- /dev/null
+++ b/providers-e2e-tests/openlineage/pyproject.toml
@@ -0,0 +1,85 @@
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+[build-system]
+requires = [
+    "hatchling==1.30.1",
+    "packaging==26.2",
+    "pathspec==1.1.1",
+    "pluggy==1.6.0",
+    "tomli==2.4.1; python_version < '3.11'",
+    "trove-classifiers==2026.6.1.19",
+]
+build-backend = "hatchling.build"
+
+[project]
+name = "apache-airflow-openlineage-e2e-tests"
+description = "OpenLineage provider end-to-end tests for Apache Airflow"
+classifiers = [
+    "Private :: Do Not Upload",
+]
+requires-python = ">=3.10,!=3.15"
+authors = [
+    { name = "Apache Software Foundation", email = "dev@airflow.apache.org" },
+]
+maintainers = [
+    { name = "Apache Software Foundation", email="dev@airflow.apache.org" },
+]
+version = "0.0.1"
+
+dependencies = [
+    "apache-airflow-core",
+    "apache-airflow-devel-common",
+]
+
+[tool.pytest]
+addopts = [
+    "-rasl",
+    "--verbosity=2",
+    "-p", "no:flaky",
+    "-p", "no:nose",
+    "-p", "no:legacypath",
+]
+norecursedirs = [
+    ".eggs",
+]
+log_level = "INFO"
+filterwarnings = [
+    "error::pytest.PytestCollectionWarning",
+]
+python_files = [
+    "*.py",
+]
+
+# Keep temporary directories (created by `tmp_path`) for 2 recent runs only failed tests.
+tmp_path_retention_count = "2"
+tmp_path_retention_policy = "failed"
+
+[tool.e2e-tests]
+required-providers = ["openlineage", "standard", "common.compat", "common.sql", "common.io"]
+
+[tool.hatch.build.targets.sdist]
+exclude = ["*"]
+
+[tool.hatch.build.targets.wheel]
+bypass-selection = true
+
+[dependency-groups]
+mypy = [
+    "apache-airflow-devel-common[mypy]",
+]
diff --git a/providers-e2e-tests/openlineage/tests/conftest.py b/providers-e2e-tests/openlineage/tests/conftest.py
new file mode 100644
index 0000000000000..917ac4a677283
--- /dev/null
+++ b/providers-e2e-tests/openlineage/tests/conftest.py
@@ -0,0 +1,194 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import platform
+import re
+import subprocess
+import sys
+import time
+
+import pytest
+import requests
+from constants import (
+    API_BASE_URL,
+    DOCKER_COMPOSE_FILE_PATH,
+    DOCKER_IMAGE,
+    ENV_FILE,
+    LOCAL_DOCKER_COMPOSE_FILE_PATH,
+    MIN_DOCKER_COMPOSE_VERSION,
+    MIN_DOCKER_VERSION,
+    OPENLINEAGE_E2E_TESTS_ROOT,
+    console,
+)
+from packaging import version
+
+from tests_common.test_utils.fernet import generate_fernet_key_string
+
+COMPOSE_PROJECT_NAME = "breeze-openlineage-e2e"
+FALSEY_VALUES = ("false", "0")
+
+
+def _env_flag(name: str) -> bool:
+    """True when the environment variable is set to a non-falsey value."""
+    value = os.environ.get(name, "")
+    return bool(value) and value.lower() not in FALSEY_VALUES
+
+
+def _compare_gte(a: str, b: str) -> bool:
+    try:
+        return version.parse(a) >= version.parse(b)
+    except Exception:
+        return False
+
+
+def _docker_is_running() -> bool:
+    try:
+        return (
+            subprocess.run(["docker", "info"], capture_output=True, check=False, timeout=10).returncode == 0
+        )
+    except Exception:
+        return False
+
+
+def _tool_version(command: list[str]) -> str:
+    try:
+        result = subprocess.run(command, capture_output=True, text=True, check=False, timeout=10)
+        if result.returncode != 0:
+            return "unknown"
+        match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout.strip())
+        return ".".join(match.groups()) if match else "unknown"
+    except Exception:
+        return "unknown"
+
+
+def check_docker_requirements() -> None:
+    """Verify Docker is running and Docker / Docker Compose meet the minimum versions."""
+    console.print("[yellow]Checking Docker requirements...[/]")
+    if not _docker_is_running():
+        console.print("[red]Docker is not running. Please start Docker and try again.[/]")
+        sys.exit(1)
+
+    docker_version = _tool_version(["docker", "version", "--format", "{{.Client.Version}}"])
+    if not _compare_gte(docker_version, MIN_DOCKER_VERSION):
+        console.print(f"[red]Docker version {docker_version} is too old (min {MIN_DOCKER_VERSION}).[/]")
+        sys.exit(1)
+    console.print(f"[green]Docker version {docker_version} meets requirements[/]")
+
+    compose_version = _tool_version(["docker", "compose", "version"])
+    if not _compare_gte(compose_version, MIN_DOCKER_COMPOSE_VERSION):
+        console.print(
+            f"[red]Docker Compose version {compose_version} is too old (min {MIN_DOCKER_COMPOSE_VERSION}).[/]"
+        )
+        sys.exit(1)
+    console.print(f"[green]Docker Compose version {compose_version} meets requirements[/]")
+
+
+def _print_diagnostics(compose) -> None:
+    console.print("[red]=== DIAGNOSTIC INFORMATION ===[/]")
+    try:
+        for container in compose.compose.ps():
+            console.print(f"  {container.name}: {container.state}")
+        console.print("\n[yellow]Container Logs:[/]")
+        console.print(compose.compose.logs())
+    except Exception as exc:
+        console.print(f"  Error collecting diagnostics: {exc}")
+
+
+def pytest_sessionstart(session):
+    """Check Docker and source the OpenLineage DAGs into the dags folder before the stack starts."""
+    check_docker_requirements()
+
+    # prepare_dags lives at the project root (next to docker-compose.yaml), not in the test package.
+    sys.path.insert(0, OPENLINEAGE_E2E_TESTS_ROOT.as_posix())
+    from prepare_dags import prepare_dags
+
+    console.print("[yellow]Preparing OpenLineage DAGs from the provider system tests...[/]")
+    dags_folder = prepare_dags()
+    console.print(f"[green]Prepared dags in {dags_folder}[/]")
+
+
+@pytest.fixture(scope="session")
+def docker_compose_setup():
+    """Start docker-compose once per session and tear it down at the end."""
+    from python_on_whales import DockerClient
+
+    debugging_on = _env_flag("VERBOSE")
+    mount_volumes = not _env_flag("SKIP_MOUNTING_LOCAL_VOLUMES")
+    delete_compose = not _env_flag("SKIP_DOCKER_COMPOSE_DELETION")
+    if mount_volumes:
+        delete_compose = False
+
+    with open(ENV_FILE, "w") as f:
+        print(f"AIRFLOW_IMAGE_NAME={DOCKER_IMAGE}", file=f)
+        print(f"AIRFLOW_UID={os.getuid()}", file=f)
+        print(f"HOST_OS={platform.system().lower()}", file=f)
+        # Please do not reuse this Fernet key in any deployment — it is generated only for these tests.
+        print(f"FERNET_KEY={generate_fernet_key_string()}", file=f)
+
+    docker_compose_files = [DOCKER_COMPOSE_FILE_PATH.as_posix()]
+    if mount_volumes:
+        docker_compose_files.append(LOCAL_DOCKER_COMPOSE_FILE_PATH.as_posix())
+    log_level = "debug" if debugging_on else "info"
+
+    compose = DockerClient(
+        compose_files=docker_compose_files,
+        debug=debugging_on,
+        log_level=log_level,
+        compose_project_name=COMPOSE_PROJECT_NAME,
+    )
+
+    start_new_compose = True
+    processes = compose.compose.ps(["airflow-apiserver"])
+    if processes and processes[0].state.status == "running":
+        if mount_volumes:
+            console.print(
+                "[yellow]Docker compose already running. Reusing it. Run `docker compose down` to reset.[/]"
+            )
+            start_new_compose = False
+        else:
+            compose.compose.down(remove_orphans=True, volumes=True, quiet=True)
+
+    try:
+        if start_new_compose:
+            console.print("[yellow]Starting docker-compose for session...[/]")
+            files = " ".join(f'-f "{file}"' for file in docker_compose_files)
+            console.print(f"[info]Equivalent manual command: docker compose {files} up --detach[/]")
+            compose.compose.up(detach=True, wait=True)
+            console.print("[green]Docker compose started successfully![/]")
+        yield compose
+    except Exception as exc:
+        console.print(f"[red]Docker compose failed to start: {exc}[/]")
+        _print_diagnostics(compose)
+        raise
+    finally:
+        if delete_compose:
+            console.print("[yellow]Cleaning up docker-compose...[/]")
+            compose.compose.down(remove_orphans=True, volumes=True, quiet=True)
+            console.print("[green]Docker compose cleaned up[/]")
+
+
+@pytest.fixture(scope="session")
+def auth_headers(docker_compose_setup) -> dict[str, str]:
+    """Wait for Airflow and return REST API auth headers (SimpleAuthManager grants an admin token)."""
+    time.sleep(15)
+    auth_response = requests.get(f"{API_BASE_URL}/auth/token", timeout=30)
+    auth_response.raise_for_status()
+    token = auth_response.json()["access_token"]
+    console.print("[green]✅ Got Airflow auth token[/]")
+    return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
diff --git a/providers-e2e-tests/openlineage/tests/constants.py b/providers-e2e-tests/openlineage/tests/constants.py
new file mode 100644
index 0000000000000..cce0aa92e91bd
--- /dev/null
+++ b/providers-e2e-tests/openlineage/tests/constants.py
@@ -0,0 +1,42 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+from rich.console import Console
+
+console = Console(width=400, color_system="standard")
+
+AIRFLOW_ROOT_PATH = Path(__file__).resolve().parents[3]
+OPENLINEAGE_E2E_TESTS_ROOT = AIRFLOW_ROOT_PATH / "providers-e2e-tests" / "openlineage"
+
+DEFAULT_PYTHON_MAJOR_MINOR_VERSION = "3.10"
+MIN_DOCKER_VERSION = "25.0.0"
+MIN_DOCKER_COMPOSE_VERSION = "2.20.2"
+DEFAULT_DOCKER_IMAGE = f"ghcr.io/apache/airflow/main/prod/python{DEFAULT_PYTHON_MAJOR_MINOR_VERSION}:latest"
+DOCKER_IMAGE = os.environ.get("DOCKER_IMAGE") or DEFAULT_DOCKER_IMAGE
+
+API_HOST_PORT = os.environ.get("HOST_PORT", "localhost:8080")
+API_BASE_URL = f"http://{API_HOST_PORT}"
+
+DOCKER_COMPOSE_FILE_PATH = OPENLINEAGE_E2E_TESTS_ROOT / "docker-compose.yaml"
+LOCAL_DOCKER_COMPOSE_FILE_PATH = OPENLINEAGE_E2E_TESTS_ROOT / "docker-compose-local.yaml"
+DAGS_FOLDER = OPENLINEAGE_E2E_TESTS_ROOT / "dags"
+LOGS_FOLDER = OPENLINEAGE_E2E_TESTS_ROOT / "logs"
+ENV_FILE = OPENLINEAGE_E2E_TESTS_ROOT / ".env"
diff --git a/providers-e2e-tests/openlineage/tests/harness.py b/providers-e2e-tests/openlineage/tests/harness.py
new file mode 100644
index 0000000000000..b45a7c67ad218
--- /dev/null
+++ b/providers-e2e-tests/openlineage/tests/harness.py
@@ -0,0 +1,246 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Drive all OpenLineage DAGs in the deployed stack and collect their final run state.
+
+Adapted from the OpenLineage dags-dashboard ``trigger_dag.py`` integration harness: it talks to a
+local Airflow REST API v2 (rather than an Astro deployment), authenticates with a SimpleAuthManager
+bearer token, and reports through logs only. The actual OpenLineage event validation happens inside
+each DAG's terminal ``OpenLineageTestOperator`` task, so a run that ends ``success`` means its
+emitted events matched the expected templates.
+"""
+
+from __future__ import annotations
+
+import datetime as dt
+import re
+import time
+from pathlib import Path
+
+import requests
+from constants import console
+from requests.adapters import HTTPAdapter
+from urllib3.util.retry import Retry
+
+WARMUP_DAG_ID = "openlineage_warmup_dag"
+TRIGGER_DELAY_SECONDS = 2
+# Shared wall-clock budget for one _trigger_and_wait() call, covering every DAG triggered in that
+# batch — bounds total polling time regardless of how many DAGs are involved
+BATCH_TIMEOUT_SECONDS = 360  # 6 min, each dagrun timeout is set to 5 minutes
+
+# DAGs that take longer to complete — trigger these first so they run while the rest are triggered.
+LONG_RUNNING_DAG_IDS = ("openlineage_defer_simple_dag",)
+
+# Child DAGs spawned by TriggerDagRunOperator carry this marker; they must not be triggered or
+# state-checked directly (their parent triggers them).
+NO_TRIGGER_MARKER = "__notrigger"
+
+
+def discover_expected_dag_ids(dags_folder: Path) -> set[str]:
+    """
+    Top-level ``DAG_ID = "..."`` declarations across the prepared example DAGs.
+
+    Used as a coverage check: every expected DAG must actually load in the deployment (a missing one
+    signals an import/parse failure rather than a test failure).
+    """
+    pattern = re.compile(r'^DAG_ID\s*=\s*["\'](?P<dag_id>[^"\']+)["\']')
+    dag_ids: set[str] = set()
+    for dag_file in (dags_folder / "system" / "openlineage").glob("example_openlineage_*.py"):
+        for line in dag_file.read_text().splitlines():
+            match = pattern.match(line)
+            if match:
+                dag_ids.add(match.group("dag_id"))
+    return dag_ids
+
+
+def _session_with_retries() -> requests.Session:
+    retry = Retry(
+        total=10,
+        connect=5,
+        read=10,
+        backoff_factor=2,
+        raise_on_status=False,
+        allowed_methods=["GET", "POST", "PATCH", "DELETE"],
+        status_forcelist=[429, 500, 502, 503, 504],
+    )
+    session = requests.Session()
+    session.mount("http://", HTTPAdapter(max_retries=retry))
+    session.mount("https://", HTTPAdapter(max_retries=retry))
+    return session
+
+
+class OpenLineageE2ERunner:
+    """Triggers all OpenLineage DAGs against a running deployment and collects their final states."""
+
+    def __init__(self, api_base_url: str, headers: dict[str, str]):
+        self.api_url = f"{api_base_url}/api/v2"
+        self.headers = headers
+        self.session = _session_with_retries()
+        now = dt.datetime.now(tz=dt.timezone.utc)
+        self.run_id = f"ci_triggered_{now.isoformat()}"
+        self.retry_run_id = f"{self.run_id}_retry1"
+        # dag_id -> whether its final state came from retry_run_id rather than run_id; populated by run().
+        self.retried_dag_ids: set[str] = set()
+
+    def wait_for_airflow_api(self, max_attempts: int = 30, poll_interval: int = 10) -> None:
+        for attempt in range(1, max_attempts + 1):
+            try:
+                response = requests.get(f"{self.api_url}/dags?limit=1", headers=self.headers, timeout=15)
+                if response.status_code < 500:
+                    console.print(f"[green]Airflow API ready after {attempt} attempt(s)")
+                    return
+            except requests.exceptions.RequestException as exc:
+                console.print(f"[yellow]Airflow API not ready (attempt {attempt}/{max_attempts}): {exc}")
+            time.sleep(poll_interval)
+        raise RuntimeError(f"Airflow API did not become ready after {max_attempts} attempts.")
+
+    def list_dags(self) -> list[str]:
+        response = self.session.get(f"{self.api_url}/dags?limit=500", headers=self.headers)
+        response.raise_for_status()
+        return [dag["dag_id"] for dag in response.json()["dags"]]
+
+    def get_task_states(self, dag_id: str, run_id: str) -> dict[str, str]:
+        """task_id -> state for every task instance in a dag run."""
+        response = self.session.get(
+            f"{self.api_url}/dags/{dag_id}/dagRuns/{run_id}/taskInstances", headers=self.headers
+        )
+        response.raise_for_status()
+        return {ti["task_id"]: ti["state"] for ti in response.json()["task_instances"]}
+
+    def wait_for_dags_loaded(self, timeout: int = 60, poll_interval: int = 3) -> list[str]:
+        """Poll until the dag-processor has parsed the DAGs (the warmup DAG is the readiness marker)."""
+        deadline = time.monotonic() + timeout
+        dag_ids: list[str] = []
+        while time.monotonic() < deadline:
+            dag_ids = self.list_dags()
+            if WARMUP_DAG_ID in dag_ids:
+                console.print(f"[green]DAGs loaded ({len(dag_ids)} found)")
+                return dag_ids
+            console.print(f"[yellow]Waiting for DAGs to load (have {len(dag_ids)})...")
+            time.sleep(poll_interval)
+        raise RuntimeError(f"DAGs did not load within {timeout}s (warmup DAG missing; have {dag_ids}).")
+
+    def unpause_dag(self, dag_id: str) -> None:
+        response = self.session.patch(
+            f"{self.api_url}/dags/{dag_id}", headers=self.headers, json={"is_paused": False}
+        )
+        if response.status_code != 200:
+            console.print(f"[red]Failed to unpause DAG `{dag_id}`: {response.text}")
+
+    def trigger_dag_run(self, dag_id: str, run_id: str) -> bool:
+        now = dt.datetime.now(tz=dt.timezone.utc).isoformat()
+        payload = {"dag_run_id": run_id, "logical_date": now, "conf": {}}
+        response = self.session.post(
+            f"{self.api_url}/dags/{dag_id}/dagRuns", headers=self.headers, json=payload
+        )
+        if response.status_code not in (200, 201):
+            console.print(f"[red]Failed to trigger DAG `{dag_id}`: {response.text}")
+            return False
+        return True
+
+    def wait_for_dag_run_to_complete(self, dag_id: str, run_id: str, timeout: int = 300) -> str:
+        url = f"{self.api_url}/dags/{dag_id}/dagRuns/{run_id}"
+        deadline = time.monotonic() + timeout
+        state = "unknown"
+        while time.monotonic() < deadline:
+            response = self.session.get(url, headers=self.headers)
+            response.raise_for_status()
+            state = response.json()["state"]
+            if state not in ("running", "queued"):
+                break
+            time.sleep(5)
+        console.print(f"[blue]DAG `{dag_id}` finished in state: {state}")
+        return state
+
+    def clear_airflow_variables(self) -> None:
+        response = self.session.get(f"{self.api_url}/variables?limit=500", headers=self.headers)
+        response.raise_for_status()
+        keys = [variable["key"] for variable in response.json()["variables"]]
+        for key in keys:
+            self.session.delete(f"{self.api_url}/variables/{key}", headers=self.headers)
+
+    def warmup(self, dag_ids: list[str]) -> None:
+        """Unpause all DAGs and run the warmup DAG so the worker is confirmed ready."""
+        if WARMUP_DAG_ID not in dag_ids:
+            raise KeyError(f"Warmup DAG `{WARMUP_DAG_ID}` not found in deployment.")
+        for dag_id in dag_ids:
+            self.unpause_dag(dag_id)
+            time.sleep(TRIGGER_DELAY_SECONDS)
+        if not self.trigger_dag_run(WARMUP_DAG_ID, self.run_id):
+            raise RuntimeError(f"Failed to trigger warmup DAG `{WARMUP_DAG_ID}`")
+        self.wait_for_dag_run_to_complete(WARMUP_DAG_ID, self.run_id)
+
+    def _trigger_and_wait(
+        self, dag_ids: list[str], run_id: str, batch_timeout: int = BATCH_TIMEOUT_SECONDS
+    ) -> dict[str, str]:
+        statuses: dict[str, str] = {}
+        ordered = [d for d in LONG_RUNNING_DAG_IDS if d in dag_ids]
+        ordered += [d for d in dag_ids if d not in LONG_RUNNING_DAG_IDS]
+        for dag_id in ordered:
+            if not self.trigger_dag_run(dag_id, run_id):
+                statuses[dag_id] = "trigger_error"
+            time.sleep(TRIGGER_DELAY_SECONDS)
+        deadline = time.monotonic() + batch_timeout
+        for dag_id in ordered:
+            if dag_id in statuses:
+                continue
+            remaining = deadline - time.monotonic()
+            if remaining <= 0:
+                console.print(
+                    f"[red]Batch deadline exceeded before checking `{dag_id}`; marking as timed_out."
+                )
+                statuses[dag_id] = "timed_out"
+                continue
+            statuses[dag_id] = self.wait_for_dag_run_to_complete(dag_id, run_id, timeout=int(remaining))
+        return statuses
+
+    def run(self, expected_dag_ids: set[str]) -> dict[str, str]:
+        """Run the full cycle and return ``{dag_id: final_state}`` for every triggered DAG."""
+        self.wait_for_airflow_api()
+        dag_ids = self.wait_for_dags_loaded()
+        if not dag_ids:
+            raise ValueError("No DAGs found in the deployment.")
+
+        self.warmup(dag_ids)
+        # Auto-runs of cron/timetable DAGs may have started while unpaused — clear their events so
+        # the test runs start from a clean slate.
+        time.sleep(10)
+        self.clear_airflow_variables()
+
+        triggerable = [
+            dag_id for dag_id in dag_ids if dag_id != WARMUP_DAG_ID and NO_TRIGGER_MARKER not in dag_id
+        ]
+        statuses = self._trigger_and_wait(triggerable, self.run_id)
+        self.clear_airflow_variables()
+
+        failed = [dag_id for dag_id, state in statuses.items() if state != "success"]
+        if failed:
+            self.retried_dag_ids = set(failed)
+            console.print(f"[yellow]Retrying {len(failed)} failed DAG(s) once: {failed}")
+            retry_statuses = self._trigger_and_wait(failed, self.retry_run_id)
+            self.clear_airflow_variables()
+            recovered = sorted(dag_id for dag_id, state in retry_statuses.items() if state == "success")
+            if recovered:
+                # The retry counts as a pass, but surface it so first-run flakiness is not hidden.
+                console.print(f"[yellow]⚠ DAGs that passed only on retry (flaky first run): {recovered}")
+            statuses.update(retry_statuses)
+
+        for dag_id in expected_dag_ids:
+            if dag_id not in statuses:
+                statuses[dag_id] = "missing"
+
+        return statuses
diff --git a/providers-e2e-tests/openlineage/tests/task_logs.py b/providers-e2e-tests/openlineage/tests/task_logs.py
new file mode 100644
index 0000000000000..f5d3c19170b9f
--- /dev/null
+++ b/providers-e2e-tests/openlineage/tests/task_logs.py
@@ -0,0 +1,111 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Read and format Airflow task logs for display in pytest output on failure.
+
+Task logs are structlog JSON-lines written by the deployed Airflow stack; they land on the host via
+the ``./logs:/opt/airflow/logs`` bind mount in ``docker-compose.yaml``. Rather than requiring someone
+to unzip the CI log artifact and open a specific ``attempt=N.log`` file, the warning/error highlights
+are printed straight into the failing test's captured output.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from collections.abc import Iterable
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from rich.console import Console
+
+LOG_LEVELS_OF_INTEREST = frozenset({"warning", "error", "critical"})
+MAX_HIGHLIGHT_LINES = 100
+
+_ATTEMPT_NUMBER_PATTERN = re.compile(r"attempt=(\d+)\.log$")
+
+
+def format_log_line(record: dict) -> str:
+    """Render one parsed structlog record as a compact, human-readable line."""
+    level = str(record.get("level", "info")).upper()
+    event = record.get("event", "")
+    lines = [f"[{level}] {event}"]
+    for exc in record.get("error_detail") or []:
+        exc_type = exc.get("exc_type", "Exception")
+        exc_value = exc.get("exc_value", "")
+        lines.append(f"    {exc_type}: {exc_value}")
+        frames = exc.get("frames") or []
+        if frames:
+            frame = frames[-1]
+            lines.append(f"    at {Path(frame['filename']).name}:{frame['lineno']} in {frame['name']}")
+    return "\n".join(lines)
+
+
+def filter_log_highlights(raw_lines: Iterable[str], max_lines: int = MAX_HIGHLIGHT_LINES) -> list[str]:
+    """Parse structlog JSON lines and keep only warning/error/critical ones, formatted for display."""
+    highlights = []
+    for raw_line in raw_lines:
+        stripped_line = raw_line.strip()
+        if not stripped_line:
+            continue
+        try:
+            record = json.loads(stripped_line)
+        except json.JSONDecodeError:
+            # Task logs aren't guaranteed to be 100% JSON (e.g. stray print output) — skip those lines.
+            continue
+        if record.get("level") in LOG_LEVELS_OF_INTEREST:
+            highlights.append(format_log_line(record))
+    if not highlights:
+        return ["(no warning/error lines in this task's log)"]
+    if len(highlights) > max_lines:
+        dropped = len(highlights) - max_lines
+        highlights = highlights[:max_lines] + [f"... ({dropped} more lines suppressed)"]
+    return highlights
+
+
+def _attempt_number(path: Path) -> int:
+    match = _ATTEMPT_NUMBER_PATTERN.search(path.name)
+    return int(match.group(1)) if match else -1
+
+
+def find_log_paths(logs_folder: Path, dag_id: str, run_id: str, task_id: str) -> list[Path]:
+    """Latest-attempt log file(s) for a task — one per map index if the task is dynamically mapped."""
+    task_root = logs_folder / f"dag_id={dag_id}" / f"run_id={run_id}" / f"task_id={task_id}"
+    latest_by_dir: dict[Path, Path] = {}
+    for attempt_path in task_root.glob("**/attempt=*.log"):
+        current = latest_by_dir.get(attempt_path.parent)
+        if current is None or _attempt_number(attempt_path) > _attempt_number(current):
+            latest_by_dir[attempt_path.parent] = attempt_path
+    return sorted(latest_by_dir.values())
+
+
+def print_task_log(console: Console, logs_folder: Path, dag_id: str, run_id: str, task_id: str) -> None:
+    """Print the warning/error highlights of a task's log(s), or a note if none are found."""
+    console.print(f"[bold cyan]--- {dag_id} / {task_id} / START---[/]")
+    log_paths = find_log_paths(logs_folder, dag_id, run_id, task_id)
+    if not log_paths:
+        console.print(
+            f"[yellow](no log file found under dag_id={dag_id}/run_id={run_id}/task_id={task_id})[/]"
+        )
+        return
+    for log_path in log_paths:
+        if len(log_paths) > 1:
+            console.print(f"[cyan]{log_path.relative_to(logs_folder)}[/]")
+        for line in filter_log_highlights(log_path.read_text().splitlines()):
+            console.print(line)
+    console.print(f"[bold cyan]--- {dag_id} / {task_id} / END---[/]")
diff --git a/providers-e2e-tests/openlineage/tests/test_openlineage_e2e.py b/providers-e2e-tests/openlineage/tests/test_openlineage_e2e.py
new file mode 100644
index 0000000000000..277ce2a942214
--- /dev/null
+++ b/providers-e2e-tests/openlineage/tests/test_openlineage_e2e.py
@@ -0,0 +1,72 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pytest
+import requests
+from constants import API_BASE_URL, DAGS_FOLDER, LOGS_FOLDER, console
+from harness import OpenLineageE2ERunner, discover_expected_dag_ids
+from task_logs import find_log_paths, print_task_log
+
+
+def _print_failure_logs(runner: OpenLineageE2ERunner, failed: dict[str, str]) -> None:
+    """Print the relevant task logs for each failed DAG, so CI output doesn't require the log artifact."""
+    for dag_id, state in failed.items():
+        if state == "missing":
+            console.print(f"[yellow]{dag_id} never loaded — check the dag-processor logs, not task logs.[/]")
+            continue
+        run_id = runner.retry_run_id if dag_id in runner.retried_dag_ids else runner.run_id
+        try:
+            task_states = runner.get_task_states(dag_id, run_id)
+        except requests.exceptions.RequestException as exc:
+            console.print(f"[yellow]Could not fetch task states for {dag_id}: {exc}[/]")
+            task_states = {}
+        # check_events always included: it shows what was actually compared, even if a different
+        # task is what actually failed the DAG. Every other non-success task is shown too, EXCEPT a
+        # routine SKIPPED task (e.g. a branch not taken) never ran and has no log — showing it would
+        # look like a false-positive failure. A task SKIPPED by dagrun_timeout cutting off a
+        # still-running task, though, usually does have a partial log, so still show those.
+        tasks_to_show = {"check_events"}
+        for task_id, ti_state in task_states.items():
+            if ti_state == "success" or task_id in tasks_to_show:
+                continue
+            if ti_state == "skipped" and not find_log_paths(LOGS_FOLDER, dag_id, run_id, task_id):
+                continue
+            tasks_to_show.add(task_id)
+        for task_id in sorted(tasks_to_show):
+            print_task_log(console, LOGS_FOLDER, dag_id, run_id, task_id)
+
+
+@pytest.mark.execution_timeout(900)  # 15 min
+def test_all_openlineage_dags_succeed(auth_headers):
+    """
+    Trigger every OpenLineage system-test DAG in the deployment and require each run to succeed.
+
+    The terminal ``OpenLineageTestOperator`` task inside each DAG validates the emitted OpenLineage
+    events against the expected templates, so a ``success`` run state means the lineage matched.
+    """
+    expected_dag_ids = discover_expected_dag_ids(DAGS_FOLDER)
+    assert expected_dag_ids, "No expected OpenLineage DAG ids were discovered from the prepared dags"
+
+    runner = OpenLineageE2ERunner(API_BASE_URL, auth_headers)
+    statuses = runner.run(expected_dag_ids)
+
+    console.print(f"[blue]OpenLineage e2e results ({len(statuses)} DAGs): {statuses}")
+    failed = {dag_id: state for dag_id, state in sorted(statuses.items()) if state != "success"}
+    if failed:
+        _print_failure_logs(runner, failed)
+    assert not failed, f"OpenLineage e2e DAG runs were not successful: {failed}"
diff --git a/providers/openlineage/tests/system/openlineage/constants.py b/providers/openlineage/tests/system/openlineage/constants.py
new file mode 100644
index 0000000000000..15a650d94266f
--- /dev/null
+++ b/providers/openlineage/tests/system/openlineage/constants.py
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Shared configuration constants for the OpenLineage system-test DAGs."""
+
+from __future__ import annotations
+
+import os
+from datetime import timedelta
+
+# Defensive per-DagRun timeout so a hung/misbehaving DAG fails fast instead of running forever.
+# Overridable via env var — the e2e harness runs every DAG in the same deployment and wants a
+# tighter value than a single real system-test run needs.
+DEFAULT_DAGRUN_TIMEOUT = timedelta(minutes=int(os.getenv("SYSTEM_TESTS_OL_DAGRUN_TIMEOUT_MINUTES", "10")))
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_all_facets_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_all_facets_dag.py
index dce37a1d91448..ed6560fc99cea 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_all_facets_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_all_facets_dag.py
@@ -71,6 +71,7 @@
 from airflow.providers.common.compat.sdk import BaseOperator
 from airflow.providers.openlineage.extractors.base import OperatorLineage
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -487,6 +488,7 @@ def get_openlineage_facets_on_start(self) -> OperatorLineage:
 DAG_ID = "openlineage_all_facets_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_base_complex_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_base_complex_dag.py
index b31bd57bd3610..d843fb6c7c454 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_base_complex_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_base_complex_dag.py
@@ -45,6 +45,7 @@
 except ImportError:
     from airflow.utils.task_group import TaskGroup  # type: ignore[no-redef]
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import AIRFLOW_VERSION, get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -74,6 +75,7 @@ def execute(self, context):
 DAG_ID = "openlineage_base_complex_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_base_simple_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_base_simple_dag.py
index f58f500c1d159..71be41773ca0f 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_base_simple_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_base_simple_dag.py
@@ -30,6 +30,7 @@
 from airflow import DAG
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -46,6 +47,7 @@ def do_nothing():
 DAG_ID = "openlineage_base_simple_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_failure_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_failure_dag.py
index 6767985c47aff..1829b058c87b2 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_failure_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_failure_dag.py
@@ -34,6 +34,7 @@
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.utils.trigger_rule import TriggerRule
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -62,6 +63,7 @@ def get_openlineage_facets_on_failure(self, task_instance) -> OperatorLineage:
 DAG_ID = "openlineage_custom_operator_failure_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_ol_methods_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_ol_methods_dag.py
index 846c8a8e7b98a..c8a4377de3eb6 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_ol_methods_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_custom_operator_ol_methods_dag.py
@@ -39,6 +39,7 @@
 from airflow.providers.common.compat.sdk import BaseOperator
 from airflow.providers.openlineage.extractors.base import OperatorLineage
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -321,6 +322,7 @@ def get_openlineage_facets_on_failure(self, task_instance) -> OperatorLineage:
 DAG_ID = "openlineage_custom_operator_ol_methods_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_defer_simple_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_defer_simple_dag.py
index 1925d0209373e..d0937e0964ab3 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_defer_simple_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_defer_simple_dag.py
@@ -24,17 +24,20 @@
 
 from __future__ import annotations
 
+import os
 from datetime import datetime, timedelta
 
 from airflow import DAG
 from airflow.providers.standard.sensors.time_delta import TimeDeltaSensor
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_defer_simple_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
@@ -43,7 +46,12 @@
 ) as dag:
     # Timedelta is compared to the DAGRun start timestamp, which can occur long before a worker picks up the
     # task. We need to ensure the sensor gets deferred at least once, so setting 180s.
-    wait = TimeDeltaSensor(task_id="wait", delta=timedelta(seconds=180), poke_interval=10, deferrable=True)
+    wait = TimeDeltaSensor(
+        task_id="wait",
+        delta=timedelta(seconds=int(os.getenv("SYSTEM_TESTS_OL_TIMEDELTA", "180"))),
+        poke_interval=10,
+        deferrable=True,
+    )
 
     check_events = OpenLineageTestOperator(
         task_id="check_events",
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_docs_file_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_docs_file_dag.py
index a30bf400c34a7..70acb73ed5b73 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_docs_file_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_docs_file_dag.py
@@ -28,12 +28,14 @@
 from airflow import DAG
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_docs_file_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_edge_labels_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_edge_labels_dag.py
index d869005bbb7ec..8822e0bf7f786 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_edge_labels_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_edge_labels_dag.py
@@ -34,12 +34,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.utils.edgemodifier import Label
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_edge_labels_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_hitl_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_hitl_dag.py
index 6227c12fed861..b4f0644e13fb3 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_hitl_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_hitl_dag.py
@@ -41,12 +41,14 @@
 from airflow.sdk.definitions.param import Param
 from airflow.utils.trigger_rule import TriggerRule
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_hitl_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_manual_lineage_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_manual_lineage_dag.py
index a829184c989d9..f754dcdfb87d4 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_manual_lineage_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_manual_lineage_dag.py
@@ -46,6 +46,7 @@
     from airflow.decorators import dag, task  # type: ignore[no-redef, attr-defined]
     from airflow.operators.python import get_current_context  # type: ignore[no-redef]
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -71,6 +72,7 @@ def _dataset_with_assertions(namespace: str, name: str) -> Dataset:
 
 
 @dag(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=dt.datetime(2024, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_mapped_simple_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_mapped_simple_dag.py
index c58651974c828..dcc7d4bab80a5 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_mapped_simple_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_mapped_simple_dag.py
@@ -35,12 +35,14 @@
     # Airflow 2 path
     from airflow.decorators import task  # type: ignore[attr-defined,no-redef]
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_mapped_simple_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_locked_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_locked_dag.py
index 42420c199afc0..9ce8bd6c178dc 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_locked_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_locked_dag.py
@@ -30,12 +30,14 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_policy_conf_locked_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_source_code_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_source_code_dag.py
index 8900f11fcf2e8..7740267ee69e3 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_source_code_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_conf_source_code_dag.py
@@ -30,12 +30,14 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_policy_conf_source_code_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_emit_false_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_emit_false_dag.py
index 11f8b9d64254d..0f1ec517150ad 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_emit_false_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_emit_false_dag.py
@@ -28,6 +28,7 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -39,6 +40,7 @@ def _say_hello():
 DAG_ID = "openlineage_policy_dag_emit_false_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_events_false_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_events_false_dag.py
index 52d754b3132e2..f550d162d6fc9 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_events_false_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_events_false_dag.py
@@ -30,6 +30,7 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -47,6 +48,7 @@ def _register_hook_lineage():
 DAG_ID = "openlineage_policy_dag_events_false_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_override_task_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_override_task_dag.py
index b326e49330143..1ae0d8f8c569e 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_override_task_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_dag_override_task_dag.py
@@ -28,12 +28,14 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_policy_dag_override_task_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_extract_metadata_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_extract_metadata_dag.py
index 112638a587ba8..f30cc112b1d74 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_extract_metadata_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_extract_metadata_dag.py
@@ -31,6 +31,7 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.openlineage.extractors.base import OperatorLineage
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -51,6 +52,7 @@ def get_openlineage_facets_on_complete(self, task_instance):
 DAG_ID = "openlineage_policy_extract_metadata_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_full_task_info_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_full_task_info_dag.py
index 1bf264af68b93..38d48a55137bc 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_full_task_info_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_full_task_info_dag.py
@@ -28,12 +28,14 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_policy_full_task_info_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_hook_lineage_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_hook_lineage_dag.py
index 6f1e8a76f972f..d04641530ea47 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_hook_lineage_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_hook_lineage_dag.py
@@ -32,6 +32,7 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -61,6 +62,7 @@ def _register_sql_hook_lineage():
 DAG_ID = "openlineage_policy_hook_lineage_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_source_code_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_source_code_dag.py
index 7ccff61e7a44c..0f194a222a616 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_source_code_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_source_code_dag.py
@@ -32,12 +32,14 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_policy_source_code_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_task_emit_false_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_task_emit_false_dag.py
index 19569c0f82b7f..e964424cf7873 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_policy_task_emit_false_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_policy_task_emit_false_dag.py
@@ -28,6 +28,7 @@
 from airflow.providers.openlineage.api.emission_policy import extend_global_openlineage_emission_policy
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -39,6 +40,7 @@ def _say_hello():
 DAG_ID = "openlineage_policy_task_emit_false_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_asset_or_time_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_asset_or_time_dag.py
index f72c35d61d251..9eacc660b5ff5 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_asset_or_time_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_asset_or_time_dag.py
@@ -30,6 +30,7 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.timetables.trigger import CronTriggerTimetable
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import AIRFLOW_VERSION, get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -57,6 +58,7 @@
     )
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2025, 5, 1),
     schedule=schedule,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_dag.py
index d44c3e01719c6..7d29ac4d33d6b 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_dag.py
@@ -28,12 +28,14 @@
 from airflow import DAG
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_cron_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule="21 13 29 2 4",  # Unlikely to ever get triggered by itself, February 29th and Thursday
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_trigger_timetable_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_trigger_timetable_dag.py
index 2a0979c2d8e7a..f8dee74e93ac0 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_trigger_timetable_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_cron_trigger_timetable_dag.py
@@ -31,12 +31,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.timetables.trigger import CronTriggerTimetable
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_cron_trigger_timetable_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=CronTriggerTimetable("21 13 29 2 4", timezone="UTC"),
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_delta_trigger_timetable_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_delta_trigger_timetable_dag.py
index 7ed88d38e6811..ead0ddf95ec14 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_delta_trigger_timetable_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_delta_trigger_timetable_dag.py
@@ -31,12 +31,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.timetables.trigger import DeltaTriggerTimetable
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_delta_trigger_timetable_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=DeltaTriggerTimetable(timedelta(days=987)),
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_complex_assets_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_complex_assets_dag.py
index 88e5399fd619d..558dcb47e792d 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_complex_assets_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_complex_assets_dag.py
@@ -30,6 +30,7 @@
 from airflow.providers.common.compat.assets import Asset
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import AIRFLOW_VERSION, get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -46,6 +47,7 @@
 DAG_ID = "openlineage_schedule_list_complex_assets_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=schedule,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_multiple_assets_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_multiple_assets_dag.py
index e51f18619ac55..649c377d9b064 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_multiple_assets_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_multiple_assets_dag.py
@@ -29,12 +29,14 @@
 from airflow.providers.common.compat.assets import Asset
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_list_multiple_assets_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=[Asset(uri="s3://bucket/file.txt", extra={"a": 1}), Asset(uri="s3://bucket2/file.txt")],
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_single_asset_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_single_asset_dag.py
index 1af5a6112b8e6..e1f0832114d23 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_single_asset_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_list_single_asset_dag.py
@@ -29,12 +29,14 @@
 from airflow.providers.common.compat.assets import Asset
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_list_single_asset_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=[Asset(uri="s3://bucket/file.txt", extra={"some_extra": 123})],
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_assets_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_assets_dag.py
index a0e1646430088..3625133c38b8e 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_assets_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_assets_dag.py
@@ -29,12 +29,14 @@
 from airflow.providers.common.compat.assets import Asset
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_multiple_assets_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=(
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_cron_trigger_timetable_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_cron_trigger_timetable_dag.py
index 7fe5e06d48b34..f296fad0a6fd9 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_cron_trigger_timetable_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_multiple_cron_trigger_timetable_dag.py
@@ -31,12 +31,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.timetables.trigger import MultipleCronTriggerTimetable
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_multiple_cron_trigger_timetable_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=MultipleCronTriggerTimetable("21 13 29 2 4", "9 27 29 2 4", timezone="UTC"),
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_single_asset_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_single_asset_dag.py
index 64cd6ed34ecb9..277dab5adb6d9 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_single_asset_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_single_asset_dag.py
@@ -29,12 +29,14 @@
 from airflow.providers.common.compat.assets import Asset
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_single_asset_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=Asset(uri="s3://bucket/file.txt", extra={"some_extra": 123}),
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timedelta_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timedelta_dag.py
index db2370e855108..f28f4eac955ba 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timedelta_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timedelta_dag.py
@@ -30,12 +30,14 @@
 from airflow import DAG
 from airflow.providers.standard.operators.bash import BashOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_timedelta_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=timedelta(days=987),
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timetable_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timetable_dag.py
index c90c0b16a915f..bf7d3ec8e3ab2 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timetable_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_schedule_timetable_dag.py
@@ -31,12 +31,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.timetables.events import EventsTimetable
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_schedule_timetable_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=EventsTimetable(  # Unlikely to ever get triggered by itself
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_setup_teardown_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_setup_teardown_dag.py
index 46c7c980481a8..8e61a55c952c8 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_setup_teardown_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_setup_teardown_dag.py
@@ -29,6 +29,7 @@
 from airflow import DAG
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -40,6 +41,7 @@ def do_nothing():
 DAG_ID = "openlineage_setup_teardown_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_short_circuit_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_short_circuit_dag.py
index 5ce68293ed43e..f404977fd4d6f 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_short_circuit_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_short_circuit_dag.py
@@ -29,6 +29,7 @@
 from airflow import DAG
 from airflow.providers.standard.operators.python import PythonOperator, ShortCircuitOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -40,6 +41,7 @@ def do_nothing():
 DAG_ID = "openlineage_short_circuit_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_task_groups_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_task_groups_dag.py
index 9c081991d1dda..01f3c9a029f2e 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_task_groups_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_task_groups_dag.py
@@ -33,12 +33,14 @@
 except ImportError:
     from airflow.utils.task_group import TaskGroup  # type: ignore[no-redef]
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_task_groups_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_taskflow_simple_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_taskflow_simple_dag.py
index 7008331ceb47a..ab0c26d3cb42c 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_taskflow_simple_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_taskflow_simple_dag.py
@@ -27,6 +27,7 @@
 
 from datetime import datetime
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -39,7 +40,13 @@
 DAG_ID = "openlineage_taskflow_simple_dag"
 
 
-@dag(schedule=None, start_date=datetime(2021, 1, 1), catchup=False, default_args={"retries": 0})
+@dag(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
+    schedule=None,
+    start_date=datetime(2021, 1, 1),
+    catchup=False,
+    default_args={"retries": 0},
+)
 def openlineage_taskflow_simple_dag():
     @task
     def do_nothing_task(**context):
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag.py
index 1fa1474a99280..779b13ce9b500 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag.py
@@ -31,12 +31,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_trigger_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
@@ -59,7 +61,7 @@
                 "rootParentJobName": "generate_report_sales_e2e",
             },
         },
-        poke_interval=10,
+        poke_interval=5,
     )
 
     check_events = OpenLineageTestOperator(
@@ -70,6 +72,7 @@
 
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id="openlineage_trigger_dag_child__notrigger",
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag_deferrable.py b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag_deferrable.py
index ebe79dab4c04f..5546baca4defd 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag_deferrable.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_dag_deferrable.py
@@ -32,12 +32,14 @@
 from airflow.providers.standard.operators.bash import BashOperator
 from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_trigger_dag_deferrable"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
@@ -63,6 +65,7 @@
 
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id="openlineage_trigger_dag_deferrable_child__notrigger",
     start_date=datetime(2021, 1, 1),
     schedule=None,
@@ -78,6 +81,7 @@
 
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id="openlineage_trigger_dag_deferrable_child2__notrigger",
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_failed_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_failed_dag.py
index 6f25749a87f26..b62bcd4708c1f 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_failed_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_trigger_failed_dag.py
@@ -35,12 +35,14 @@
 from airflow.providers.standard.operators.trigger_dagrun import TriggerDagRunOperator
 from airflow.utils.trigger_rule import TriggerRule
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
 DAG_ID = "openlineage_trigger_failed_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
@@ -80,6 +82,7 @@
 
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id="openlineage_trigger_failed_dag_child__notrigger",
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/example_openlineage_versioned_dag.py b/providers/openlineage/tests/system/openlineage/example_openlineage_versioned_dag.py
index d26d8d7cdea35..df6b6dc2db288 100644
--- a/providers/openlineage/tests/system/openlineage/example_openlineage_versioned_dag.py
+++ b/providers/openlineage/tests/system/openlineage/example_openlineage_versioned_dag.py
@@ -30,6 +30,7 @@
 from airflow import DAG
 from airflow.providers.standard.operators.python import PythonOperator
 
+from system.openlineage.constants import DEFAULT_DAGRUN_TIMEOUT
 from system.openlineage.expected_events import get_expected_event_file_path
 from system.openlineage.operator import OpenLineageTestOperator
 
@@ -41,6 +42,7 @@ def do_nothing():
 DAG_ID = "openlineage_versioned_dag"
 
 with DAG(
+    dagrun_timeout=DEFAULT_DAGRUN_TIMEOUT,
     dag_id=DAG_ID,
     start_date=datetime(2021, 1, 1),
     schedule=None,
diff --git a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
index 448e53e09b9a9..b66a4acae336e 100644
--- a/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
+++ b/providers/openlineage/tests/system/openlineage/expected_events/openlineage_custom_operator_failure_dag.json
@@ -35,7 +35,7 @@
                 "errorMessage": {
                     "message": "Intentional failure for OpenLineage on_failure testing",
                     "programmingLanguage": "python",
-                    "stackTrace": "{{ result.startswith('Traceback (most recent call last):\n') and result.endswith('example_openlineage_custom_operator_failure_dag.py\", line 48, in execute\n    raise ValueError(\"Intentional failure for OpenLineage on_failure testing\")\nValueError: Intentional failure for OpenLineage on_failure testing\n') }}"
+                    "stackTrace": "{{ result.startswith('Traceback (most recent call last):\n') and result.endswith('example_openlineage_custom_operator_failure_dag.py\", line 49, in execute\n    raise ValueError(\"Intentional failure for OpenLineage on_failure testing\")\nValueError: Intentional failure for OpenLineage on_failure testing\n') }}"
                 },
                 "parent": {
                     "job": {
diff --git a/pyproject.toml b/pyproject.toml
index 633a208e4dcfe..d197d7a7d7bdc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -819,6 +819,7 @@ testing = ["dev", "providers.tests", "tests_common", "tests", "system", "unit",
 "airflow-core/tests/*" = ["D", "TID253", "S101", "TRY002"]
 "airflow-ctl-tests/tests/*" = ["D", "TID253", "S101", "TRY002"]
 "airflow-e2e-tests/tests/*" = ["D", "TID253", "S101", "TRY002"]
+"providers-e2e-tests/*" = ["D", "TID253", "S101", "TRY002"]
 "docker-tests/*" = ["D", "TID253", "S101", "TRY002"]
 "task-sdk-integration-tests/*" = ["D", "TID253", "S101", "TRY002"]
 "kubernetes-tests/*" = ["D", "TID253", "S101", "TRY002"]
@@ -1085,6 +1086,7 @@ mypy_path = [
     "$MYPY_CONFIG_FILE_DIR/kubernetes-tests/tests",
     "$MYPY_CONFIG_FILE_DIR/docker-tests/tests",
     "$MYPY_CONFIG_FILE_DIR/task-sdk-integration-tests/tests",
+    "$MYPY_CONFIG_FILE_DIR/providers-e2e-tests/openlineage/tests",
     # Automatically generated mypy paths (update_airflow_pyproject_toml.py)
     "$MYPY_CONFIG_FILE_DIR/providers/airbyte/src",
     "$MYPY_CONFIG_FILE_DIR/providers/airbyte/tests",
@@ -1344,6 +1346,7 @@ dev = [
     "apache-airflow-devel-common[no-doc]",
     "apache-airflow-docker-tests",
     "apache-airflow-task-sdk-integration-tests",
+    "apache-airflow-openlineage-e2e-tests",
     "apache-airflow-helm-chart",
     "apache-airflow-kubernetes-tests",
     "apache-airflow-task-sdk[all]",
@@ -1428,6 +1431,7 @@ apache-airflow-e2e-tests = false
 apache-airflow-helm-chart = false
 apache-airflow-kubernetes-tests = false
 apache-airflow-mypy = false
+apache-airflow-openlineage-e2e-tests = false
 apache-airflow-providers = false
 apache-airflow-providers-airbyte = false
 apache-airflow-providers-akeyless = false
@@ -1573,6 +1577,7 @@ apache-airflow-e2e-tests = false
 apache-airflow-helm-chart = false
 apache-airflow-kubernetes-tests = false
 apache-airflow-mypy = false
+apache-airflow-openlineage-e2e-tests = false
 apache-airflow-providers = false
 apache-airflow-providers-airbyte = false
 apache-airflow-providers-akeyless = false
@@ -1715,6 +1720,7 @@ apache-airflow-devel-common = { workspace = true }
 apache-airflow-docker-tests = { workspace = true }
 apache-airflow-e2e-tests = { workspace = true }
 apache-airflow-task-sdk-integration-tests = { workspace = true }
+apache-airflow-openlineage-e2e-tests = { workspace = true }
 apache-airflow-helm-chart = { workspace = true }
 apache-airflow-kubernetes-tests = { workspace = true }
 apache-airflow-providers = { workspace = true }
@@ -1854,6 +1860,7 @@ members = [
     "scripts",
     "docker-tests",
     "task-sdk-integration-tests",
+    "providers-e2e-tests/openlineage",
     "chart",
     "kubernetes-tests",
     "task-sdk",
diff --git a/scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py b/scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py
index ca9f09adae6c7..ce1f79af0ac4e 100755
--- a/scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py
+++ b/scripts/ci/prek/run_mypy_full_dist_local_venv_or_breeze_in_ci.py
@@ -68,6 +68,7 @@
     "chart/tests",
     "airflow-e2e-tests",
     "task-sdk-integration-tests",
+    "providers-e2e-tests/openlineage",
     "docker-tests",
     "kubernetes-tests",
 ]
@@ -127,6 +128,9 @@
         r"^.*/node_modules/.*",
         r"^.*\\..*",
         r"^.*/src/airflow/__init__.py$",
+        # Generated-at-runtime artifacts of the provider e2e tests (gitignored; not source to check).
+        r"^.*/providers-e2e-tests/[^/]+/dags/.*",
+        r"^.*/providers-e2e-tests/[^/]+/provider_dist/.*",
     ]
 ]
 
diff --git a/uv.lock b/uv.lock
index 1ff0a8117e71f..351a0abdde7f7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -21,6 +21,7 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for
 exclude-newer-span = "P4D"
 
 [options.exclude-newer-package]
+apache-airflow-openlineage-e2e-tests = false
 apache-airflow-providers-informatica = false
 apache-airflow-providers-clickhousedb = false
 apache-airflow-providers-amazon = false
@@ -171,6 +172,7 @@ members = [
     "apache-airflow-helm-chart",
     "apache-airflow-kubernetes-tests",
     "apache-airflow-mypy",
+    "apache-airflow-openlineage-e2e-tests",
     "apache-airflow-providers",
     "apache-airflow-providers-airbyte",
     "apache-airflow-providers-akeyless",
@@ -1504,6 +1506,7 @@ ci-image = [
     { name = "apache-airflow-docker-tests" },
     { name = "apache-airflow-helm-chart" },
     { name = "apache-airflow-kubernetes-tests" },
+    { name = "apache-airflow-openlineage-e2e-tests" },
     { name = "apache-airflow-scripts" },
     { name = "apache-airflow-shared-configuration" },
     { name = "apache-airflow-shared-dagnode" },
@@ -1533,6 +1536,7 @@ dev = [
     { name = "apache-airflow-docker-tests" },
     { name = "apache-airflow-helm-chart" },
     { name = "apache-airflow-kubernetes-tests" },
+    { name = "apache-airflow-openlineage-e2e-tests" },
     { name = "apache-airflow-scripts" },
     { name = "apache-airflow-shared-configuration" },
     { name = "apache-airflow-shared-dagnode" },
@@ -1814,6 +1818,7 @@ ci-image = [
     { name = "apache-airflow-docker-tests", editable = "docker-tests" },
     { name = "apache-airflow-helm-chart", editable = "chart" },
     { name = "apache-airflow-kubernetes-tests", editable = "kubernetes-tests" },
+    { name = "apache-airflow-openlineage-e2e-tests", editable = "providers-e2e-tests/openlineage" },
     { name = "apache-airflow-scripts", editable = "scripts" },
     { name = "apache-airflow-shared-configuration", editable = "shared/configuration" },
     { name = "apache-airflow-shared-dagnode", editable = "shared/dagnode" },
@@ -1843,6 +1848,7 @@ dev = [
     { name = "apache-airflow-docker-tests", editable = "docker-tests" },
     { name = "apache-airflow-helm-chart", editable = "chart" },
     { name = "apache-airflow-kubernetes-tests", editable = "kubernetes-tests" },
+    { name = "apache-airflow-openlineage-e2e-tests", editable = "providers-e2e-tests/openlineage" },
     { name = "apache-airflow-scripts", editable = "scripts" },
     { name = "apache-airflow-shared-configuration", editable = "shared/configuration" },
     { name = "apache-airflow-shared-dagnode", editable = "shared/dagnode" },
@@ -2885,6 +2891,29 @@ dependencies = [
 [package.metadata]
 requires-dist = [{ name = "mypy", specifier = ">=1.0.0" }]
 
+[[package]]
+name = "apache-airflow-openlineage-e2e-tests"
+version = "0.0.1"
+source = { editable = "providers-e2e-tests/openlineage" }
+dependencies = [
+    { name = "apache-airflow-core" },
+    { name = "apache-airflow-devel-common" },
+]
+
+[package.dev-dependencies]
+mypy = [
+    { name = "apache-airflow-devel-common", extra = ["mypy"] },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "apache-airflow-core", editable = "airflow-core" },
+    { name = "apache-airflow-devel-common", editable = "devel-common" },
+]
+
+[package.metadata.requires-dev]
+mypy = [{ name = "apache-airflow-devel-common", extras = ["mypy"], editable = "devel-common" }]
+
 [[package]]
 name = "apache-airflow-providers"
 version = "0.0.1"

From c1cccfdb157ad22b2c464c3a327645fa630c8780 Mon Sep 17 00:00:00 2001
From: Kacper Muda <mudakacper@gmail.com>
Date: Wed, 8 Jul 2026 11:29:17 +0200
Subject: [PATCH 2/2] Address review comments

---
 .../commands/testing_commands.py              |  9 ++--
 dev/breeze/tests/test_selective_checks.py     |  2 +-
 .../openlineage/prepare_dags.py               |  9 +++-
 .../openlineage/tests/harness.py              | 48 +++++++++----------
 4 files changed, 37 insertions(+), 31 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index fada767ddbcaa..504ac0f8a5757 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -924,6 +924,8 @@ def task_sdk_integration_tests(
 
 
 def _available_e2e_providers() -> list[str]:
+    if not PROVIDERS_E2E_TESTS_ROOT_PATH.exists():
+        return []
     return sorted(
         d.name
         for d in PROVIDERS_E2E_TESTS_ROOT_PATH.iterdir()
@@ -967,6 +969,7 @@ def _build_providers_e2e_compat_image(provider: str, airflow_version: str, pytho
             "wheel",
             "--skip-tag-check",
             "--include-not-ready-providers",
+            "--clean-dist",
         ],
         check=True,
         cwd=AIRFLOW_ROOT_PATH,
@@ -975,10 +978,8 @@ def _build_providers_e2e_compat_image(provider: str, airflow_version: str, pytho
     if provider_dist.exists():
         shutil.rmtree(provider_dist)
     provider_dist.mkdir(parents=True)
-    for provider_id in required_providers:
-        wheel_prefix = f"apache_airflow_providers_{provider_id.replace('.', '_')}-"
-        for wheel in (AIRFLOW_ROOT_PATH / "dist").glob(f"{wheel_prefix}*.whl"):
-            shutil.copy2(wheel, provider_dist / wheel.name)
+    for wheel in (AIRFLOW_ROOT_PATH / "dist").glob("*.whl"):  # With --clean-dist above we can copy all
+        shutil.copy2(wheel, provider_dist / wheel.name)
 
     base_image = f"apache/airflow:{airflow_version}-python{python}"
     image_name = f"{provider}-e2e/airflow:{airflow_version}-python{python}"
diff --git a/dev/breeze/tests/test_selective_checks.py b/dev/breeze/tests/test_selective_checks.py
index 8a6dbd8f7bb25..1e0e2644ffd5d 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -1491,7 +1491,7 @@ def assert_outputs_are_printed(expected_outputs: dict[str, str], stderr: str):
             id="Run OpenLineage e2e tests for common provider change",
         ),
         pytest.param(
-            ("providers-e2e-tests/openlineage/tests/openlineage_e2e_tests/test_openlineage_e2e.py",),
+            ("providers-e2e-tests/openlineage/tests/harness.py",),
             {
                 "run-providers-e2e-tests-openlineage": "true",
                 "prod-image-build": "true",
diff --git a/providers-e2e-tests/openlineage/prepare_dags.py b/providers-e2e-tests/openlineage/prepare_dags.py
index 228cd16943d72..7d6b51f45f855 100644
--- a/providers-e2e-tests/openlineage/prepare_dags.py
+++ b/providers-e2e-tests/openlineage/prepare_dags.py
@@ -81,8 +81,13 @@ def prepare_dags() -> Path:
         raise FileNotFoundError(f"OpenLineage system tests not found at {SYSTEM_TESTS_SOURCE}")
 
     if DAGS_DEST.exists():
-        shutil.rmtree(DAGS_DEST)
-    DAGS_DEST.mkdir(parents=True)
+        for child in DAGS_DEST.iterdir():
+            if child.is_dir():
+                shutil.rmtree(child)
+            else:
+                child.unlink()
+    else:
+        DAGS_DEST.mkdir(parents=True)
 
     # Copy the whole `system` package so `system.openlineage.{operator,transport,expected_events}`
     # imports resolve from the dags folder.
diff --git a/providers-e2e-tests/openlineage/tests/harness.py b/providers-e2e-tests/openlineage/tests/harness.py
index b45a7c67ad218..05c2b23674273 100644
--- a/providers-e2e-tests/openlineage/tests/harness.py
+++ b/providers-e2e-tests/openlineage/tests/harness.py
@@ -15,12 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 """
-Drive all OpenLineage DAGs in the deployed stack and collect their final run state.
+Drive all OpenLineage Dags in the deployed stack and collect their final run state.
 
 Adapted from the OpenLineage dags-dashboard ``trigger_dag.py`` integration harness: it talks to a
 local Airflow REST API v2 (rather than an Astro deployment), authenticates with a SimpleAuthManager
 bearer token, and reports through logs only. The actual OpenLineage event validation happens inside
-each DAG's terminal ``OpenLineageTestOperator`` task, so a run that ends ``success`` means its
+each Dag's terminal ``OpenLineageTestOperator`` task, so a run that ends ``success`` means its
 emitted events matched the expected templates.
 """
 
@@ -38,23 +38,23 @@
 
 WARMUP_DAG_ID = "openlineage_warmup_dag"
 TRIGGER_DELAY_SECONDS = 2
-# Shared wall-clock budget for one _trigger_and_wait() call, covering every DAG triggered in that
-# batch — bounds total polling time regardless of how many DAGs are involved
+# Shared wall-clock budget for one _trigger_and_wait() call, covering every Dag triggered in that
+# batch — bounds total polling time regardless of how many Dags are involved
 BATCH_TIMEOUT_SECONDS = 360  # 6 min, each dagrun timeout is set to 5 minutes
 
-# DAGs that take longer to complete — trigger these first so they run while the rest are triggered.
+# Dags that take longer to complete — trigger these first so they run while the rest are triggered.
 LONG_RUNNING_DAG_IDS = ("openlineage_defer_simple_dag",)
 
-# Child DAGs spawned by TriggerDagRunOperator carry this marker; they must not be triggered or
+# Child Dags spawned by TriggerDagRunOperator carry this marker; they must not be triggered or
 # state-checked directly (their parent triggers them).
 NO_TRIGGER_MARKER = "__notrigger"
 
 
 def discover_expected_dag_ids(dags_folder: Path) -> set[str]:
     """
-    Top-level ``DAG_ID = "..."`` declarations across the prepared example DAGs.
+    Top-level ``DAG_ID = "..."`` declarations across the prepared example Dags.
 
-    Used as a coverage check: every expected DAG must actually load in the deployment (a missing one
+    Used as a coverage check: every expected Dag must actually load in the deployment (a missing one
     signals an import/parse failure rather than a test failure).
     """
     pattern = re.compile(r'^DAG_ID\s*=\s*["\'](?P<dag_id>[^"\']+)["\']')
@@ -84,7 +84,7 @@ def _session_with_retries() -> requests.Session:
 
 
 class OpenLineageE2ERunner:
-    """Triggers all OpenLineage DAGs against a running deployment and collects their final states."""
+    """Triggers all OpenLineage Dags against a running deployment and collects their final states."""
 
     def __init__(self, api_base_url: str, headers: dict[str, str]):
         self.api_url = f"{api_base_url}/api/v2"
@@ -122,24 +122,24 @@ def get_task_states(self, dag_id: str, run_id: str) -> dict[str, str]:
         return {ti["task_id"]: ti["state"] for ti in response.json()["task_instances"]}
 
     def wait_for_dags_loaded(self, timeout: int = 60, poll_interval: int = 3) -> list[str]:
-        """Poll until the dag-processor has parsed the DAGs (the warmup DAG is the readiness marker)."""
+        """Poll until the dag-processor has parsed the Dags (the warmup Dag is the readiness marker)."""
         deadline = time.monotonic() + timeout
         dag_ids: list[str] = []
         while time.monotonic() < deadline:
             dag_ids = self.list_dags()
             if WARMUP_DAG_ID in dag_ids:
-                console.print(f"[green]DAGs loaded ({len(dag_ids)} found)")
+                console.print(f"[green]Dags loaded ({len(dag_ids)} found)")
                 return dag_ids
-            console.print(f"[yellow]Waiting for DAGs to load (have {len(dag_ids)})...")
+            console.print(f"[yellow]Waiting for Dags to load (have {len(dag_ids)})...")
             time.sleep(poll_interval)
-        raise RuntimeError(f"DAGs did not load within {timeout}s (warmup DAG missing; have {dag_ids}).")
+        raise RuntimeError(f"Dags did not load within {timeout}s (warmup Dag missing; have {dag_ids}).")
 
     def unpause_dag(self, dag_id: str) -> None:
         response = self.session.patch(
             f"{self.api_url}/dags/{dag_id}", headers=self.headers, json={"is_paused": False}
         )
         if response.status_code != 200:
-            console.print(f"[red]Failed to unpause DAG `{dag_id}`: {response.text}")
+            console.print(f"[red]Failed to unpause Dag `{dag_id}`: {response.text}")
 
     def trigger_dag_run(self, dag_id: str, run_id: str) -> bool:
         now = dt.datetime.now(tz=dt.timezone.utc).isoformat()
@@ -148,7 +148,7 @@ def trigger_dag_run(self, dag_id: str, run_id: str) -> bool:
             f"{self.api_url}/dags/{dag_id}/dagRuns", headers=self.headers, json=payload
         )
         if response.status_code not in (200, 201):
-            console.print(f"[red]Failed to trigger DAG `{dag_id}`: {response.text}")
+            console.print(f"[red]Failed to trigger Dag `{dag_id}`: {response.text}")
             return False
         return True
 
@@ -163,7 +163,7 @@ def wait_for_dag_run_to_complete(self, dag_id: str, run_id: str, timeout: int =
             if state not in ("running", "queued"):
                 break
             time.sleep(5)
-        console.print(f"[blue]DAG `{dag_id}` finished in state: {state}")
+        console.print(f"[blue]Dag `{dag_id}` finished in state: {state}")
         return state
 
     def clear_airflow_variables(self) -> None:
@@ -174,14 +174,14 @@ def clear_airflow_variables(self) -> None:
             self.session.delete(f"{self.api_url}/variables/{key}", headers=self.headers)
 
     def warmup(self, dag_ids: list[str]) -> None:
-        """Unpause all DAGs and run the warmup DAG so the worker is confirmed ready."""
+        """Unpause all Dags and run the warmup Dag so the worker is confirmed ready."""
         if WARMUP_DAG_ID not in dag_ids:
-            raise KeyError(f"Warmup DAG `{WARMUP_DAG_ID}` not found in deployment.")
+            raise KeyError(f"Warmup Dag `{WARMUP_DAG_ID}` not found in deployment.")
         for dag_id in dag_ids:
             self.unpause_dag(dag_id)
             time.sleep(TRIGGER_DELAY_SECONDS)
         if not self.trigger_dag_run(WARMUP_DAG_ID, self.run_id):
-            raise RuntimeError(f"Failed to trigger warmup DAG `{WARMUP_DAG_ID}`")
+            raise RuntimeError(f"Failed to trigger warmup Dag `{WARMUP_DAG_ID}`")
         self.wait_for_dag_run_to_complete(WARMUP_DAG_ID, self.run_id)
 
     def _trigger_and_wait(
@@ -209,14 +209,14 @@ def _trigger_and_wait(
         return statuses
 
     def run(self, expected_dag_ids: set[str]) -> dict[str, str]:
-        """Run the full cycle and return ``{dag_id: final_state}`` for every triggered DAG."""
+        """Run the full cycle and return ``{dag_id: final_state}`` for every triggered Dag."""
         self.wait_for_airflow_api()
         dag_ids = self.wait_for_dags_loaded()
         if not dag_ids:
-            raise ValueError("No DAGs found in the deployment.")
+            raise ValueError("No Dags found in the deployment.")
 
         self.warmup(dag_ids)
-        # Auto-runs of cron/timetable DAGs may have started while unpaused — clear their events so
+        # Auto-runs of cron/timetable Dags may have started while unpaused — clear their events so
         # the test runs start from a clean slate.
         time.sleep(10)
         self.clear_airflow_variables()
@@ -230,13 +230,13 @@ def run(self, expected_dag_ids: set[str]) -> dict[str, str]:
         failed = [dag_id for dag_id, state in statuses.items() if state != "success"]
         if failed:
             self.retried_dag_ids = set(failed)
-            console.print(f"[yellow]Retrying {len(failed)} failed DAG(s) once: {failed}")
+            console.print(f"[yellow]Retrying {len(failed)} failed Dag(s) once: {failed}")
             retry_statuses = self._trigger_and_wait(failed, self.retry_run_id)
             self.clear_airflow_variables()
             recovered = sorted(dag_id for dag_id, state in retry_statuses.items() if state == "success")
             if recovered:
                 # The retry counts as a pass, but surface it so first-run flakiness is not hidden.
-                console.print(f"[yellow]⚠ DAGs that passed only on retry (flaky first run): {recovered}")
+                console.print(f"[yellow]⚠ Dags that passed only on retry (flaky first run): {recovered}")
             statuses.update(retry_statuses)
 
         for dag_id in expected_dag_ids:
