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

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


The following commit(s) were added to refs/heads/main by this push:
     new fdddc181370 Vendor K8s JSON schemas for helm tests and add 
multi-version validation (#62820)
fdddc181370 is described below

commit fdddc181370a0a1c85080fe1eac1d094189cb0db
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed Mar 4 21:51:02 2026 +0100

    Vendor K8s JSON schemas for helm tests and add multi-version validation 
(#62820)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .github/actions/breeze/action.yml                  |   2 +-
 .github/actions/install-prek/action.yml            |   2 +-
 .github/workflows/basic-tests.yml                  |   2 +-
 .github/workflows/ci-amd-arm.yml                   |   4 +-
 .github/workflows/helm-tests.yml                   |  17 +-
 .github/workflows/release_dockerhub_image.yml      |   2 +-
 .pre-commit-config.yaml                            |   8 +
 AGENTS.md                                          |   3 +
 Dockerfile                                         |   2 +-
 Dockerfile.ci                                      |   2 +-
 dev/breeze/doc/ci/02_images.md                     |   2 +-
 dev/breeze/doc/images/output_ci_upgrade.svg        |  84 +++++-
 dev/breeze/doc/images/output_ci_upgrade.txt        |   2 +-
 .../doc/images/output_testing_helm-tests.svg       |  52 ++--
 .../doc/images/output_testing_helm-tests.txt       |   2 +-
 .../src/airflow_breeze/commands/ci_commands.py     | 189 +++++++++++++-
 .../airflow_breeze/commands/ci_commands_config.py  |  14 +-
 .../commands/release_management_commands.py        |   2 +-
 .../airflow_breeze/commands/testing_commands.py    |  14 +
 .../commands/testing_commands_config.py            |   1 +
 dev/breeze/src/airflow_breeze/global_constants.py  |   2 +-
 .../src/airflow_breeze/utils/selective_checks.py   |  10 +
 dev/breeze/tests/test_selective_checks.py          |  61 +++++
 dev/breeze/uv.lock                                 |  54 ++--
 .../tests/chart_utils/helm_template_generator.py   | 110 +++-----
 pyproject.toml                                     |   2 +-
 scripts/ci/prek/check_k8s_schemas_published.py     |  83 ++++++
 scripts/ci/prek/common_prek_utils.py               |  20 ++
 scripts/ci/prek/download_k8s_schemas.py            | 281 +++++++++++++++++++++
 scripts/tools/setup_breeze                         |   2 +-
 30 files changed, 873 insertions(+), 158 deletions(-)

diff --git a/.github/actions/breeze/action.yml 
b/.github/actions/breeze/action.yml
index 605e1737f31..53d94793c0d 100644
--- a/.github/actions/breeze/action.yml
+++ b/.github/actions/breeze/action.yml
@@ -24,7 +24,7 @@ inputs:
     default: "3.10"
   uv-version:
     description: 'uv version to use'
-    default: "0.10.7"  # Keep this comment to allow automatic replacement of 
uv version
+    default: "0.10.8"  # Keep this comment to allow automatic replacement of 
uv version
 outputs:
   host-python-version:
     description: Python version used in host
diff --git a/.github/actions/install-prek/action.yml 
b/.github/actions/install-prek/action.yml
index 3a1b4864b4b..00821eb44c7 100644
--- a/.github/actions/install-prek/action.yml
+++ b/.github/actions/install-prek/action.yml
@@ -24,7 +24,7 @@ inputs:
     default: "3.10"
   uv-version:
     description: 'uv version to use'
-    default: "0.10.7"  # Keep this comment to allow automatic replacement of 
uv version
+    default: "0.10.8"  # Keep this comment to allow automatic replacement of 
uv version
   prek-version:
     description: 'prek version to use'
     default: "0.3.4"  # Keep this comment to allow automatic replacement of 
prek version
diff --git a/.github/workflows/basic-tests.yml 
b/.github/workflows/basic-tests.yml
index 47672b895dc..d197df3e746 100644
--- a/.github/workflows/basic-tests.yml
+++ b/.github/workflows/basic-tests.yml
@@ -70,7 +70,7 @@ on:  # yamllint disable-line rule:truthy
         type: string
       uv-version:
         description: 'uv version to use'
-        default: "0.10.7"  # Keep this comment to allow automatic replacement 
of uv version
+        default: "0.10.8"  # Keep this comment to allow automatic replacement 
of uv version
         type: string
       platform:
         description: 'Platform for the build - linux/amd64 or linux/arm64'
diff --git a/.github/workflows/ci-amd-arm.yml b/.github/workflows/ci-amd-arm.yml
index ca5229a8cc1..3643392e011 100644
--- a/.github/workflows/ci-amd-arm.yml
+++ b/.github/workflows/ci-amd-arm.yml
@@ -40,7 +40,7 @@ env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   GITHUB_USERNAME: ${{ github.actor }}
   SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
-  UV_VERSION: "0.10.7"  # Keep this comment to allow automatic replacement of 
uv version
+  UV_VERSION: "0.10.8"  # Keep this comment to allow automatic replacement of 
uv version
   VERBOSE: "true"
 
 concurrency:
@@ -81,6 +81,7 @@ jobs:
       full-tests-needed: ${{ steps.selective-checks.outputs.full-tests-needed 
}}
       has-migrations: ${{ steps.selective-checks.outputs.has-migrations }}
       helm-test-packages: ${{ 
steps.selective-checks.outputs.helm-test-packages }}
+      helm-test-kubernetes-versions: ${{ 
steps.selective-checks.outputs.helm-test-kubernetes-versions }}
       include-success-outputs: ${{ 
steps.selective-checks.outputs.include-success-outputs }}
       individual-providers-test-types-list-as-strings-in-json: >-
         ${{ 
steps.selective-checks.outputs.individual-providers-test-types-list-as-strings-in-json
 }}
@@ -392,6 +393,7 @@ jobs:
       runners: ${{ needs.build-info.outputs.runner-type }}
       platform: ${{ needs.build-info.outputs.platform }}
       helm-test-packages: ${{ needs.build-info.outputs.helm-test-packages }}
+      helm-test-kubernetes-versions: ${{ 
needs.build-info.outputs.helm-test-kubernetes-versions }}
       default-python-version: "${{ 
needs.build-info.outputs.default-python-version }}"
       use-uv: ${{ needs.build-info.outputs.use-uv }}
     if: >
diff --git a/.github/workflows/helm-tests.yml b/.github/workflows/helm-tests.yml
index 89bd7c7c0bb..14d5fe6f944 100644
--- a/.github/workflows/helm-tests.yml
+++ b/.github/workflows/helm-tests.yml
@@ -32,6 +32,10 @@ on:  # yamllint disable-line rule:truthy
         description: "Stringified JSON array of helm test packages to test"
         required: true
         type: string
+      helm-test-kubernetes-versions:
+        description: "Stringified JSON array of Kubernetes versions to 
validate against"
+        required: true
+        type: string
       default-python-version:
         description: "Which version of python should be used by default"
         required: true
@@ -45,13 +49,16 @@ permissions:
 jobs:
   tests-helm:
     timeout-minutes: 80
-    name: "Unit tests Helm: ${{ matrix.helm-test-package }}"
+    name: >-
+      Unit tests Helm: ${{ matrix.helm-test-package }}
+      (K8S ${{ matrix.kubernetes-version }})
     runs-on: ${{ fromJSON(inputs.runners) }}
     strategy:
       fail-fast: false
       max-parallel: 20
       matrix:
         helm-test-package: ${{ fromJSON(inputs.helm-test-packages) }}
+        kubernetes-version: ${{ fromJSON(inputs.helm-test-kubernetes-versions) 
}}
     env:
       # Always use default Python version of CI image for preparing packages
       PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}"
@@ -79,10 +86,14 @@ jobs:
           python: "${{ inputs.default-python-version }}"
           use-uv: ${{ inputs.use-uv }}
           make-mnt-writeable-and-cleanup: true
-      - name: "Helm Unit Tests: ${{ matrix.helm-test-package }}"
+      - name: "Helm Unit Tests: ${{ matrix.helm-test-package }} (K8S ${{ 
matrix.kubernetes-version }})"
         env:
           HELM_TEST_PACKAGE: "${{ matrix.helm-test-package }}"
-        run: breeze testing helm-tests --test-type "${HELM_TEST_PACKAGE}"
+          HELM_TEST_KUBERNETES_VERSION: "${{ matrix.kubernetes-version }}"
+        run: >
+          breeze testing helm-tests
+          --test-type "${HELM_TEST_PACKAGE}"
+          --kubernetes-version "${HELM_TEST_KUBERNETES_VERSION}"
 
   tests-helm-release:
     timeout-minutes: 80
diff --git a/.github/workflows/release_dockerhub_image.yml 
b/.github/workflows/release_dockerhub_image.yml
index 3d6ab532387..7fac56a7d19 100644
--- a/.github/workflows/release_dockerhub_image.yml
+++ b/.github/workflows/release_dockerhub_image.yml
@@ -58,7 +58,7 @@ jobs:
       AIRFLOW_VERSION: ${{ github.event.inputs.airflowVersion }}
       AMD_ONLY: ${{ github.event.inputs.amdOnly }}
       LIMIT_PYTHON_VERSIONS: ${{ github.event.inputs.limitPythonVersions }}
-      UV_VERSION: "0.10.7"  # Keep this comment to allow automatic replacement 
of uv version
+      UV_VERSION: "0.10.8"  # Keep this comment to allow automatic replacement 
of uv version
     if: contains(fromJSON('[
       "ashb",
       "bugraoz93",
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 694f2706173..aca03e5f2cb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -658,6 +658,7 @@ repos:
           ^scripts/ci/docker-compose/integration-keycloak\.yml$|
           ^scripts/ci/docker-compose/keycloak/keycloak-entrypoint\.sh$|
           ^scripts/ci/prek/upgrade_important_versions.py$|
+          ^scripts/ci/prek/download_k8s_schemas\.py$|
           ^scripts/ci/prek/vendor_k8s_json_schema\.py$
       - id: check-template-context-variable-in-sync
         name: Sync template context variable refs
@@ -953,6 +954,13 @@ repos:
           - "B101,B301,B324,B403,B404,B603"
           - "--severity-level"
           - "high"  # TODO: remove this line when we fix all the issues
+      - id: check-k8s-schemas-published
+        name: Check K8s schemas are published on airflow.apache.org
+        entry: ./scripts/ci/prek/check_k8s_schemas_published.py
+        language: python
+        pass_filenames: false
+        files: ^dev/breeze/src/airflow_breeze/global_constants\.py$
+        require_serial: true
         ## ADD MOST PREK HOOK ABOVE THAT LINE
         # The below prek hooks are those requiring CI image to be built
       - id: mypy-dev
diff --git a/AGENTS.md b/AGENTS.md
index 22724b78dad..8427eacf54c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,6 +26,9 @@
 `v3-1-test` when creating a PR for the 3.1 branch.
 
 - **Build docs:** `breeze build-docs`
+- **Run Helm chart tests:** `breeze testing helm-tests --use-xdist`
+- **Run Helm tests with specific K8s version:** `breeze testing helm-tests 
--use-xdist --kubernetes-version 1.35.0`
+- **Run specific Helm test type:** `breeze testing helm-tests --use-xdist 
--test-type <type>` (types: `airflow_aux`, `airflow_core`, `apiserver`, 
`dagprocessor`, `other`, `redis`, `security`, `statsd`, `webserver`)
 
 SQLite is the default backend. Use `--backend postgres` or `--backend mysql` 
for integration tests that need those databases. If Docker networking fails, 
run `docker network prune`.
 
diff --git a/Dockerfile b/Dockerfile
index b04509c0353..faeef2b3b54 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -73,7 +73,7 @@ ARG PYTHON_LTO="true"
 # Also use `force pip` label on your PR to swap all places we use `uv` to `pip`
 ARG AIRFLOW_PIP_VERSION=26.0.1
 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main";
-ARG AIRFLOW_UV_VERSION=0.10.7
+ARG AIRFLOW_UV_VERSION=0.10.8
 ARG AIRFLOW_USE_UV="false"
 ARG AIRFLOW_IMAGE_REPOSITORY="https://github.com/apache/airflow";
 ARG 
AIRFLOW_IMAGE_README_URL="https://raw.githubusercontent.com/apache/airflow/main/docs/docker-stack/README.md";
diff --git a/Dockerfile.ci b/Dockerfile.ci
index e641015d57a..3919a04c5d5 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -1733,7 +1733,7 @@ COPY --from=scripts common.sh install_packaging_tools.sh 
install_additional_depe
 # Also use `force pip` label on your PR to swap all places we use `uv` to `pip`
 ARG AIRFLOW_PIP_VERSION=26.0.1
 # ARG AIRFLOW_PIP_VERSION="git+https://github.com/pypa/pip.git@main";
-ARG AIRFLOW_UV_VERSION=0.10.7
+ARG AIRFLOW_UV_VERSION=0.10.8
 ARG AIRFLOW_PREK_VERSION="0.3.4"
 
 # UV_LINK_MODE=copy is needed since we are using cache mounted from the host
diff --git a/dev/breeze/doc/ci/02_images.md b/dev/breeze/doc/ci/02_images.md
index 4b184d6fbed..ac9772f4a0f 100644
--- a/dev/breeze/doc/ci/02_images.md
+++ b/dev/breeze/doc/ci/02_images.md
@@ -443,7 +443,7 @@ can be used for CI images:
 | `ADDITIONAL_DEV_APT_DEPS`         |                             | Additional 
apt dev dependencies installed in the first part of the image                   
                       |
 | `ADDITIONAL_DEV_APT_ENV`          |                             | Additional 
env variables defined when installing dev deps                                  
                       |
 | `AIRFLOW_PIP_VERSION`             | `26.0.1`                    | `pip` 
version used.                                                                   
                            |
-| `AIRFLOW_UV_VERSION`              | `0.10.7`                    | `uv` 
version used.                                                                   
                             |
+| `AIRFLOW_UV_VERSION`              | `0.10.8`                    | `uv` 
version used.                                                                   
                             |
 | `AIRFLOW_PREK_VERSION`            | `0.3.4`                     | `prek` 
version used.                                                                   
                           |
 | `AIRFLOW_USE_UV`                  | `true`                      | Whether to 
use UV for installation.                                                        
                       |
 | `PIP_PROGRESS_BAR`                | `on`                        | Progress 
bar for PIP installation                                                        
                         |
diff --git a/dev/breeze/doc/images/output_ci_upgrade.svg 
b/dev/breeze/doc/images/output_ci_upgrade.svg
index 0287487009f..2fee555288b 100644
--- a/dev/breeze/doc/images/output_ci_upgrade.svg
+++ b/dev/breeze/doc/images/output_ci_upgrade.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 513.5999999999999" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 904.0" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-ci-upgrade-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="462.59999999999997" />
+      <rect x="0" y="0" width="1463.0" height="853.0" />
     </clipPath>
     <clipPath id="breeze-ci-upgrade-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -99,9 +99,57 @@
 <clipPath id="breeze-ci-upgrade-line-17">
     <rect x="0" y="416.3" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-ci-upgrade-line-18">
+    <rect x="0" y="440.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-19">
+    <rect x="0" y="465.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-20">
+    <rect x="0" y="489.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-21">
+    <rect x="0" y="513.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-22">
+    <rect x="0" y="538.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-23">
+    <rect x="0" y="562.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-24">
+    <rect x="0" y="587.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-25">
+    <rect x="0" y="611.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-26">
+    <rect x="0" y="635.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-27">
+    <rect x="0" y="660.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-28">
+    <rect x="0" y="684.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-29">
+    <rect x="0" y="709.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-30">
+    <rect x="0" y="733.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-31">
+    <rect x="0" y="757.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-32">
+    <rect x="0" y="782.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-ci-upgrade-line-33">
+    <rect x="0" y="806.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="511.6" rx="8"/><text 
class="breeze-ci-upgrade-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;ci&#160;upgrade</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="902" rx="8"/><text 
class="breeze-ci-upgrade-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;ci&#160;upgrade</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -123,13 +171,29 @@
 </text><text class="breeze-ci-upgrade-r5" x="0" y="239.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-9)">│</text><text 
class="breeze-ci-upgrade-r1" x="488" y="239.6" textLength="951.6" 
clip-path="url(#breeze-ci-upgrade-line-9)">ask)&#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-ci-upgrade-r5" x="0" y="264" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-10)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="264" textLength="195.2" 
clip-path="url(#breeze-ci-upgrade-line-10)">--switch-to-base</text><text 
class="breeze-ci-upgrade-r1" x="219.6" y="264" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-10)">/</text><text 
class="breeze-ci-upgrade-r4" x="231.8" y="264" textLength="231.8" 
clip-path="url(#breeze-ci-upgrade [...]
 </text><text class="breeze-ci-upgrade-r5" x="0" y="288.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-11)">│</text><text 
class="breeze-ci-upgrade-r1" x="488" y="288.4" textLength="951.6" 
clip-path="url(#breeze-ci-upgrade-line-11)">specified,&#160;will&#160;ask)&#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-ci-upgrade-r5" x="0" y="312.8" textLength="1464" 
clip-path="url(#breeze-ci-upgrade-line-12)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-ci-upgrade-r1" x="1464" y="312.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-12)">
-</text><text class="breeze-ci-upgrade-r5" x="0" y="337.2" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-13)">╭─</text><text 
class="breeze-ci-upgrade-r5" x="24.4" y="337.2" textLength="195.2" 
clip-path="url(#breeze-ci-upgrade-line-13)">&#160;Common&#160;options&#160;</text><text
 class="breeze-ci-upgrade-r5" x="219.6" y="337.2" textLength="1220" 
clip-path="url(#breeze-ci-upgrade-line-13)">───────────────────────────────────────────────────────────────────────────────────────────
 [...]
-</text><text class="breeze-ci-upgrade-r5" x="0" y="361.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-14)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="361.6" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-14)">--answer&#160;</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="361.6" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-14)">-a</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="361.6" textLength="329.4" 
clip-path="url(#breeze-ci- [...]
-</text><text class="breeze-ci-upgrade-r5" x="0" y="386" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-15)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="386" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-15)">--verbose</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="386" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-15)">-v</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="386" textLength="585.6" 
clip-path="url(#breeze-ci-upgrade-line- [...]
-</text><text class="breeze-ci-upgrade-r5" x="0" y="410.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-16)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="410.4" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-16)">--dry-run</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="410.4" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-16)">-D</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="410.4" textLength="719.8" 
clip-path="url(#breeze-ci-upgra [...]
-</text><text class="breeze-ci-upgrade-r5" x="0" y="434.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-17)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="434.8" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-17)">--help&#160;&#160;&#160;</text><text
 class="breeze-ci-upgrade-r7" x="158.6" y="434.8" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-17)">-h</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="434.8" textLength="329.4" 
clip-path="url(# [...]
-</text><text class="breeze-ci-upgrade-r5" x="0" y="459.2" textLength="1464" 
clip-path="url(#breeze-ci-upgrade-line-18)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-ci-upgrade-r1" x="1464" y="459.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-18)">
+</text><text class="breeze-ci-upgrade-r5" x="0" y="312.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-12)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="312.8" textLength="439.2" 
clip-path="url(#breeze-ci-upgrade-line-12)">--airflow-site&#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-ci-upgrade-r1" x="488" y="312.8" textLength="695.4" 
clip-path="url(#breeze [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="337.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-13)">│</text><text 
class="breeze-ci-upgrade-r5" x="488" y="337.2" textLength="195.2" 
clip-path="url(#breeze-ci-upgrade-line-13)">../airflow-site]</text><text 
class="breeze-ci-upgrade-r6" x="695.4" y="337.2" textLength="134.2" 
clip-path="url(#breeze-ci-upgrade-line-13)">(DIRECTORY)</text><text 
class="breeze-ci-upgrade-r5" x="1451.8" y="337.2" textLength="12.2" 
clip-path="url( [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="361.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-14)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="361.6" textLength="439.2" 
clip-path="url(#breeze-ci-upgrade-line-14)">--force-k8s-schema-sync&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-ci-upgrade-r1" x="488" y="361.6" textLength="951.6" 
clip-path="url(#breeze-ci-upgrade-line-14)">Force&#160;syncing&#160 [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="386" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-15)">│</text><text 
class="breeze-ci-upgrade-r1" x="488" y="386" textLength="951.6" 
clip-path="url(#breeze-ci-upgrade-line-15)">published&#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;&#1
 [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="410.4" textLength="1464" 
clip-path="url(#breeze-ci-upgrade-line-16)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-ci-upgrade-r1" x="1464" y="410.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-16)">
+</text><text class="breeze-ci-upgrade-r5" x="0" y="434.8" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-17)">╭─</text><text 
class="breeze-ci-upgrade-r5" x="24.4" y="434.8" textLength="183" 
clip-path="url(#breeze-ci-upgrade-line-17)">&#160;Upgrade&#160;steps&#160;</text><text
 class="breeze-ci-upgrade-r5" x="207.4" y="434.8" textLength="1232.2" 
clip-path="url(#breeze-ci-upgrade-line-17)">────────────────────────────────────────────────────────────────────────────────────────────
 [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="459.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-18)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="459.2" textLength="146.4" 
clip-path="url(#breeze-ci-upgrade-line-18)">--autoupdate</text><text 
class="breeze-ci-upgrade-r1" x="170.8" y="459.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-18)">/</text><text 
class="breeze-ci-upgrade-r4" x="183" y="459.2" textLength="183" 
clip-path="url(#breeze-ci-upgrade [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="483.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-19)">│</text><text 
class="breeze-ci-upgrade-r5" x="744.2" y="483.6" textLength="134.2" 
clip-path="url(#breeze-ci-upgrade-line-19)">autoupdate]</text><text 
class="breeze-ci-upgrade-r5" x="1451.8" y="483.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-19)">│</text><text 
class="breeze-ci-upgrade-r1" x="1464" y="483.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgr [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="508" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-20)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="508" textLength="170.8" 
clip-path="url(#breeze-ci-upgrade-line-20)">--pin-versions</text><text 
class="breeze-ci-upgrade-r1" x="195.2" y="508" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-20)">/</text><text 
class="breeze-ci-upgrade-r4" x="207.4" y="508" textLength="207.4" 
clip-path="url(#breeze-ci-upgrade-l [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="532.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-21)">│</text><text 
class="breeze-ci-upgrade-r5" x="744.2" y="532.4" textLength="158.6" 
clip-path="url(#breeze-ci-upgrade-line-21)">pin-versions]</text><text 
class="breeze-ci-upgrade-r5" x="1451.8" y="532.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-21)">│</text><text 
class="breeze-ci-upgrade-r1" x="1464" y="532.4" textLength="12.2" 
clip-path="url(#breeze-ci-up [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="556.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-22)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="556.8" textLength="329.4" 
clip-path="url(#breeze-ci-upgrade-line-22)">--update-chart-dependencies</text><text
 class="breeze-ci-upgrade-r1" x="353.8" y="556.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-22)">/</text><text 
class="breeze-ci-upgrade-r4" x="366" y="556.8" textLength="353.8" 
clip-path="url(# [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="581.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-23)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="581.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-23)">s</text><text 
class="breeze-ci-upgrade-r1" x="744.2" y="581.2" textLength="158.6" 
clip-path="url(#breeze-ci-upgrade-line-23)">dependencies&#160;</text><text 
class="breeze-ci-upgrade-r5" x="902.8" y="581.2" textLength="439.2" 
clip-path="url(#breeze- [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="605.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-24)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="605.6" textLength="341.6" 
clip-path="url(#breeze-ci-upgrade-line-24)">--upgrade-important-versions</text><text
 class="breeze-ci-upgrade-r1" x="366" y="605.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-24)">/</text><text 
class="breeze-ci-upgrade-r4" x="378.2" y="605.6" textLength="341.6" 
clip-path="url( [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="630" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-25)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="630" textLength="36.6" 
clip-path="url(#breeze-ci-upgrade-line-25)">ons</text><text 
class="breeze-ci-upgrade-r1" x="744.2" y="630" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-25)">versions&#160;</text><text 
class="breeze-ci-upgrade-r5" x="854" y="630" textLength="451.4" 
clip-path="url(#breeze-ci-upgrade-l [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="654.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-26)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="654.4" textLength="207.4" 
clip-path="url(#breeze-ci-upgrade-line-26)">--k8s-schema-sync</text><text 
class="breeze-ci-upgrade-r1" x="231.8" y="654.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-26)">/</text><text 
class="breeze-ci-upgrade-r4" x="244" y="654.4" textLength="244" 
clip-path="url(#breeze-ci-up [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="678.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-27)">│</text><text 
class="breeze-ci-upgrade-r5" x="744.2" y="678.8" textLength="195.2" 
clip-path="url(#breeze-ci-upgrade-line-27)">k8s-schema-sync]</text><text 
class="breeze-ci-upgrade-r5" x="1451.8" y="678.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-27)">│</text><text 
class="breeze-ci-upgrade-r1" x="1464" y="678.8" textLength="12.2" 
clip-path="url(#breeze-ci [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="703.2" textLength="1464" 
clip-path="url(#breeze-ci-upgrade-line-28)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-ci-upgrade-r1" x="1464" y="703.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-28)">
+</text><text class="breeze-ci-upgrade-r5" x="0" y="727.6" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-29)">╭─</text><text 
class="breeze-ci-upgrade-r5" x="24.4" y="727.6" textLength="195.2" 
clip-path="url(#breeze-ci-upgrade-line-29)">&#160;Common&#160;options&#160;</text><text
 class="breeze-ci-upgrade-r5" x="219.6" y="727.6" textLength="1220" 
clip-path="url(#breeze-ci-upgrade-line-29)">───────────────────────────────────────────────────────────────────────────────────────────
 [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="752" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-30)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="752" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-30)">--answer&#160;</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="752" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-30)">-a</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="752" textLength="329.4" 
clip-path="url(#breeze-ci-upgrade- [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="776.4" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-31)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="776.4" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-31)">--verbose</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="776.4" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-31)">-v</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="776.4" textLength="585.6" 
clip-path="url(#breeze-ci-upgra [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="800.8" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-32)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="800.8" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-32)">--dry-run</text><text 
class="breeze-ci-upgrade-r7" x="158.6" y="800.8" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-32)">-D</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="800.8" textLength="719.8" 
clip-path="url(#breeze-ci-upgra [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="825.2" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-33)">│</text><text 
class="breeze-ci-upgrade-r4" x="24.4" y="825.2" textLength="109.8" 
clip-path="url(#breeze-ci-upgrade-line-33)">--help&#160;&#160;&#160;</text><text
 class="breeze-ci-upgrade-r7" x="158.6" y="825.2" textLength="24.4" 
clip-path="url(#breeze-ci-upgrade-line-33)">-h</text><text 
class="breeze-ci-upgrade-r1" x="207.4" y="825.2" textLength="329.4" 
clip-path="url(# [...]
+</text><text class="breeze-ci-upgrade-r5" x="0" y="849.6" textLength="1464" 
clip-path="url(#breeze-ci-upgrade-line-34)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-ci-upgrade-r1" x="1464" y="849.6" textLength="12.2" 
clip-path="url(#breeze-ci-upgrade-line-34)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_ci_upgrade.txt 
b/dev/breeze/doc/images/output_ci_upgrade.txt
index 890eea2fe0f..d825b5556de 100644
--- a/dev/breeze/doc/images/output_ci_upgrade.txt
+++ b/dev/breeze/doc/images/output_ci_upgrade.txt
@@ -1 +1 @@
-96a3cebcb6753bff242b0d524d734d00
+e8f12d48c804aac757f9a853dba754f7
diff --git a/dev/breeze/doc/images/output_testing_helm-tests.svg 
b/dev/breeze/doc/images/output_testing_helm-tests.svg
index de020c2ec91..b7e38b97f53 100644
--- a/dev/breeze/doc/images/output_testing_helm-tests.svg
+++ b/dev/breeze/doc/images/output_testing_helm-tests.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 660.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 708.8" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-testing-helm-tests-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="609.0" />
+      <rect x="0" y="0" width="1463.0" height="657.8" />
     </clipPath>
     <clipPath id="breeze-testing-helm-tests-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -117,9 +117,15 @@
 <clipPath id="breeze-testing-helm-tests-line-23">
     <rect x="0" y="562.7" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-testing-helm-tests-line-24">
+    <rect x="0" y="587.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-helm-tests-line-25">
+    <rect x="0" y="611.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="658" rx="8"/><text 
class="breeze-testing-helm-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;helm-tests</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="706.8" rx="8"/><text 
class="breeze-testing-helm-tests-title" fill="#c5c8c6" text-anchor="middle" 
x="740" y="27">Command:&#160;testing&#160;helm-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"/>
@@ -135,25 +141,27 @@
 </text><text class="breeze-testing-helm-tests-r1" x="12.2" y="93.2" 
textLength="256.2" 
clip-path="url(#breeze-testing-helm-tests-line-3)">Run&#160;Helm&#160;chart&#160;tests.</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="93.2" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-3)">
 </text><text class="breeze-testing-helm-tests-r1" x="1464" y="117.6" 
textLength="12.2" clip-path="url(#breeze-testing-helm-tests-line-4)">
 </text><text class="breeze-testing-helm-tests-r5" x="0" y="142" 
textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-5)">╭─</text><text 
class="breeze-testing-helm-tests-r5" x="24.4" y="142" textLength="378.2" 
clip-path="url(#breeze-testing-helm-tests-line-5)">&#160;Flags&#160;for&#160;helms-tests&#160;command&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="402.6" y="142" textLength="1037" 
clip-path="url(#breeze-testing-helm-tests-line-5)">─────────────────────────── 
[...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="166.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-6)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="166.4" textLength="170.8" 
clip-path="url(#breeze-testing-helm-tests-line-6)">--test-type&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="219.6" y="166.4" textLength="317.2" 
clip-path="url(#breeze-testing-helm-tests-line-6)">Type&#160;of&#160;helm&#160;tests&#160;to&#160;r
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="190.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-7)">│</text><text 
class="breeze-testing-helm-tests-r6" x="219.6" y="190.8" textLength="744.2" 
clip-path="url(#breeze-testing-helm-tests-line-7)">dagprocessor&#160;|&#160;other&#160;|&#160;redis&#160;|&#160;security&#160;|&#160;statsd&#160;|&#160;webserver)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="190.8" textLength="12.2" 
clip-path="url(# [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="215.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-8)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="215.2" textLength="170.8" 
clip-path="url(#breeze-testing-helm-tests-line-8)">--test-timeout</text><text 
class="breeze-testing-helm-tests-r1" x="219.6" y="215.2" textLength="1220" 
clip-path="url(#breeze-testing-helm-tests-line-8)">Test&#160;timeout&#160;in&#160;seconds.&#160;Set&#160;the&#160;p
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="239.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-9)">│</text><text 
class="breeze-testing-helm-tests-r5" x="219.6" y="239.6" textLength="158.6" 
clip-path="url(#breeze-testing-helm-tests-line-9)">[default:&#160;60]</text><text
 class="breeze-testing-helm-tests-r6" x="390.4" y="239.6" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-9)">(INTEGER&#160;RANGE&#160;x&gt;=0)</text><text
 class="breeze- [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="264" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-10)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="264" textLength="170.8" 
clip-path="url(#breeze-testing-helm-tests-line-10)">--use-xdist&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="219.6" y="264" textLength="329.4" 
clip-path="url(#breeze-testing-helm-tests-line-10)">Use&#160;xdist&#160;plugin&#160;for&#160;pytest</te
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="288.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-11)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="288.4" textLength="170.8" 
clip-path="url(#breeze-testing-helm-tests-line-11)">--parallelism&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="219.6" y="288.4" textLength="927.2" 
clip-path="url(#breeze-testing-helm-tests-line-11)">Maximum&#160;number&#160;of&#160;processes&#160;to&#160
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="312.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-12)">│</text><text 
class="breeze-testing-helm-tests-r6" x="219.6" y="312.8" textLength="170.8" 
clip-path="url(#breeze-testing-helm-tests-line-12)">RANGE&#160;1&lt;=x&lt;=8)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="312.8" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-12)">│</text><text 
class="breeze-testing-helm-tests-r [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="337.2" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-13)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="337.2" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-13)">
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="361.6" 
textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-14)">╭─</text><text 
class="breeze-testing-helm-tests-r5" x="24.4" y="361.6" textLength="451.4" 
clip-path="url(#breeze-testing-helm-tests-line-14)">&#160;Advanced&#160;flag&#160;for&#160;helm-test&#160;command&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="475.8" y="361.6" textLength="963.8" 
clip-path="url(#breeze-testing-helm-tests-line-14)">────── [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-15)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="386" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-15)">--github-repository</text><text
 class="breeze-testing-helm-tests-r7" x="280.6" y="386" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-15)">-g</text><text 
class="breeze-testing-helm-tests-r1" x="329.4"  [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="410.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-16)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="410.4" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-16)">--mount-sources&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="329.4" y="410.4" textLength="1110.2" 
clip-path="url(#breeze-testing-helm-tests-line-16)">Choose&#160;scope&#160;of&#160;loc
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-17)">│</text><text 
class="breeze-testing-helm-tests-r1" x="329.4" y="434.8" textLength="134.2" 
clip-path="url(#breeze-testing-helm-tests-line-17)">selected).&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="463.6" y="434.8" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-17)">[default:&#160;selected]</text><text
 class="breeze-testin [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-18)">│</text><text 
class="breeze-testing-helm-tests-r6" x="329.4" y="459.2" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-18)">providers-and-tests)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="459.2" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-18)">│</text><text 
class="breeze-testing-helm-tests-r1" x="1 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="483.6" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-19)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="483.6" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-19)">
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="508" 
textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-20)">╭─</text><text 
class="breeze-testing-helm-tests-r5" x="24.4" y="508" textLength="195.2" 
clip-path="url(#breeze-testing-helm-tests-line-20)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="219.6" y="508" textLength="1220" 
clip-path="url(#breeze-testing-helm-tests-line-20)">─────────────────────────────────────────────────
 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="532.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-21)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="532.4" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-21)">--verbose</text><text 
class="breeze-testing-helm-tests-r7" x="158.6" y="532.4" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-21)">-v</text><text 
class="breeze-testing-helm-tests-r1" x="207.4" y="5 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="556.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-22)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="556.8" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-22)">--dry-run</text><text 
class="breeze-testing-helm-tests-r7" x="158.6" y="556.8" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-22)">-D</text><text 
class="breeze-testing-helm-tests-r1" x="207.4" y="5 [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-23)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="581.2" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-23)">--help&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r7" x="158.6" y="581.2" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-23)">-h</text><text 
class="breeze-testing-helm-tests-r1" [...]
-</text><text class="breeze-testing-helm-tests-r5" x="0" y="605.6" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-24)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="605.6" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-24)">
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="166.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-6)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="166.4" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-6)">--test-type&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="292.8" y="166.4" textLength="317.2" 
clip-path="url(#breeze-testing-helm-tests-line-6)">Type&#160;of&# [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="190.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-7)">│</text><text 
class="breeze-testing-helm-tests-r6" x="292.8" y="190.8" textLength="744.2" 
clip-path="url(#breeze-testing-helm-tests-line-7)">dagprocessor&#160;|&#160;other&#160;|&#160;redis&#160;|&#160;security&#160;|&#160;statsd&#160;|&#160;webserver)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="190.8" textLength="12.2" 
clip-path="url(# [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="215.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-8)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="215.2" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-8)">--test-timeout&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="292.8" y="215.2" textLength="1146.8" 
clip-path="url(#breeze-testing-helm-tests-line-8)">Test&#160;timeout&#160;in&#1 
[...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="239.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-9)">│</text><text 
class="breeze-testing-helm-tests-r5" x="292.8" y="239.6" textLength="158.6" 
clip-path="url(#breeze-testing-helm-tests-line-9)">[default:&#160;60]</text><text
 class="breeze-testing-helm-tests-r6" x="463.6" y="239.6" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-9)">(INTEGER&#160;RANGE&#160;x&gt;=0)</text><text
 class="breeze- [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="264" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-10)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="264" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-10)">--kubernetes-version</text><text
 class="breeze-testing-helm-tests-r1" x="292.8" y="264" textLength="658.8" 
clip-path="url(#breeze-testing-helm-tests-line-10)">Kubernetes&#160;version&#160;to&#160;validate&#160;helm&#160;t
 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="288.4" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-11)">│</text><text 
class="breeze-testing-helm-tests-r6" x="292.8" y="288.4" textLength="414.8" 
clip-path="url(#breeze-testing-helm-tests-line-11)">1.32.8&#160;|&#160;1.33.4&#160;|&#160;1.34.0&#160;|&#160;1.35.0)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="288.4" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-11)">│</text>< [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="312.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-12)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="312.8" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-12)">--use-xdist&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="292.8" y="312.8" textLength="329.4" 
clip-path="url(#breeze-testing-helm-tests-line-12)">Use&#160;xd [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="337.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-13)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="337.2" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-13)">--parallelism&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="292.8" y="337.2" textLength="927.2" 
clip-path="url(#breeze-testing-helm-tests-line-13)">Maximum&#160;number&# [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-14)">│</text><text 
class="breeze-testing-helm-tests-r6" x="292.8" y="361.6" textLength="280.6" 
clip-path="url(#breeze-testing-helm-tests-line-14)">(INTEGER&#160;RANGE&#160;1&lt;=x&lt;=8)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="361.6" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-14)">│</text><text 
class="breeze-testin [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="386" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="386" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-15)">
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="410.4" 
textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-16)">╭─</text><text 
class="breeze-testing-helm-tests-r5" x="24.4" y="410.4" textLength="451.4" 
clip-path="url(#breeze-testing-helm-tests-line-16)">&#160;Advanced&#160;flag&#160;for&#160;helm-test&#160;command&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="475.8" y="410.4" textLength="963.8" 
clip-path="url(#breeze-testing-helm-tests-line-16)">────── [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-17)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="434.8" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-17)">--github-repository</text><text
 class="breeze-testing-helm-tests-r7" x="280.6" y="434.8" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-17)">-g</text><text 
class="breeze-testing-helm-tests-r1" x="3 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-18)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="459.2" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-18)">--mount-sources&#160;&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r1" x="329.4" y="459.2" textLength="1110.2" 
clip-path="url(#breeze-testing-helm-tests-line-18)">Choose&#160;scope&#160;of&#160;loc
 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="483.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-19)">│</text><text 
class="breeze-testing-helm-tests-r1" x="329.4" y="483.6" textLength="134.2" 
clip-path="url(#breeze-testing-helm-tests-line-19)">selected).&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="463.6" y="483.6" textLength="231.8" 
clip-path="url(#breeze-testing-helm-tests-line-19)">[default:&#160;selected]</text><text
 class="breeze-testin [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="508" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-20)">│</text><text 
class="breeze-testing-helm-tests-r6" x="329.4" y="508" textLength="244" 
clip-path="url(#breeze-testing-helm-tests-line-20)">providers-and-tests)</text><text
 class="breeze-testing-helm-tests-r5" x="1451.8" y="508" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-20)">│</text><text 
class="breeze-testing-helm-tests-r1" x="1464" y [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="532.4" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-21)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="532.4" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-21)">
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="556.8" 
textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-22)">╭─</text><text 
class="breeze-testing-helm-tests-r5" x="24.4" y="556.8" textLength="195.2" 
clip-path="url(#breeze-testing-helm-tests-line-22)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-helm-tests-r5" x="219.6" y="556.8" textLength="1220" 
clip-path="url(#breeze-testing-helm-tests-line-22)">───────────────────────────────────────────
 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="581.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-23)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="581.2" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-23)">--verbose</text><text 
class="breeze-testing-helm-tests-r7" x="158.6" y="581.2" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-23)">-v</text><text 
class="breeze-testing-helm-tests-r1" x="207.4" y="5 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="605.6" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-24)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="605.6" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-24)">--dry-run</text><text 
class="breeze-testing-helm-tests-r7" x="158.6" y="605.6" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-24)">-D</text><text 
class="breeze-testing-helm-tests-r1" x="207.4" y="6 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="630" 
textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-25)">│</text><text 
class="breeze-testing-helm-tests-r4" x="24.4" y="630" textLength="109.8" 
clip-path="url(#breeze-testing-helm-tests-line-25)">--help&#160;&#160;&#160;</text><text
 class="breeze-testing-helm-tests-r7" x="158.6" y="630" textLength="24.4" 
clip-path="url(#breeze-testing-helm-tests-line-25)">-h</text><text 
class="breeze-testing-helm-tests-r1" x="20 [...]
+</text><text class="breeze-testing-helm-tests-r5" x="0" y="654.4" 
textLength="1464" 
clip-path="url(#breeze-testing-helm-tests-line-26)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-helm-tests-r1" x="1464" y="654.4" textLength="12.2" 
clip-path="url(#breeze-testing-helm-tests-line-26)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_testing_helm-tests.txt 
b/dev/breeze/doc/images/output_testing_helm-tests.txt
index d57dd55b5c1..c816fbb2bb8 100644
--- a/dev/breeze/doc/images/output_testing_helm-tests.txt
+++ b/dev/breeze/doc/images/output_testing_helm-tests.txt
@@ -1 +1 @@
-769d6c7a6082bff2444e0a347a83d01c
+2a8a6d65107b81afe9bc7ffbec9dfb2e
diff --git a/dev/breeze/src/airflow_breeze/commands/ci_commands.py 
b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
index 95dfea5b107..fb375d60630 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_commands.py
@@ -431,6 +431,102 @@ def get_workflow_info(github_context: str, 
github_context_input: StringIO):
     wi.print_all_ga_outputs()
 
 
+def _check_k8s_schema_published(version: str) -> bool:
+    """Check if K8s schemas for a given version are published on 
airflow.apache.org."""
+    from urllib.error import HTTPError, URLError
+    from urllib.request import Request, urlopen
+
+    url = 
f"https://airflow.apache.org/k8s-schemas/v{version}-standalone-strict/configmap-v1.json";
+    req = Request(url, method="HEAD")
+    try:
+        resp = urlopen(req, timeout=15)
+        return resp.status == 200
+    except (HTTPError, URLError):
+        return False
+
+
+def _sync_k8s_schemas_to_airflow_site(airflow_site: Path, force: bool, 
command_env: dict[str, str]) -> None:
+    """Sync K8s schemas to airflow-site directory if needed."""
+    from airflow_breeze.global_constants import ALLOWED_KUBERNETES_VERSIONS
+
+    versions = [v.lstrip("v") for v in ALLOWED_KUBERNETES_VERSIONS]
+    missing: list[str] = []
+    for version in versions:
+        if not _check_k8s_schema_published(version):
+            missing.append(version)
+
+    if not missing and not force:
+        get_console().print("[success]All K8s schema versions are already 
published. Skipping sync.[/]")
+        return
+
+    if missing:
+        get_console().print(
+            f"[warning]K8s schemas missing for versions: {', '.join(f'v{v}' 
for v in missing)}[/]"
+        )
+    else:
+        get_console().print("[info]Force sync requested.[/]")
+
+    if not airflow_site.is_dir():
+        get_console().print(
+            f"[error]airflow-site directory not found at {airflow_site}. "
+            "Use --airflow-site to specify the path to the airflow-site 
checkout.[/]"
+        )
+        return
+
+    # Verify this is the airflow-site repo by checking git remote
+    remote_result = run_command(
+        ["git", "-C", str(airflow_site), "remote", "-v"],
+        capture_output=True,
+        text=True,
+        check=False,
+    )
+    if remote_result.returncode != 0 or "airflow-site" not in 
remote_result.stdout:
+        get_console().print(
+            f"[error]{airflow_site} does not appear to be a clone of the 
airflow-site repository.[/]"
+        )
+        return
+
+    static_dir = airflow_site / "landing-pages" / "site" / "static"
+    if not static_dir.is_dir():
+        get_console().print(
+            f"[error]Expected directory structure not found: {static_dir}\n"
+            "The airflow-site checkout should contain 
landing-pages/site/static/.[/]"
+        )
+        return
+
+    output_dir = static_dir / "k8s-schemas"
+
+    # Filter out versions already present in the local airflow-site checkout
+    versions_to_download = missing if (missing and not force) else versions
+    versions_to_download = [
+        v
+        for v in versions_to_download
+        if not (output_dir / f"v{v}-standalone-strict").is_dir()
+        or not any((output_dir / f"v{v}-standalone-strict").iterdir())
+    ]
+
+    if not versions_to_download:
+        get_console().print(
+            "[success]All required K8s schema versions already exist in 
airflow-site. Skipping download.[/]"
+        )
+        return
+
+    get_console().print(
+        f"[info]Downloading K8s schemas for versions "
+        f"{', '.join(f'v{v}' for v in versions_to_download)} to 
{output_dir}...[/]"
+    )
+    cmd = [
+        "uv",
+        "run",
+        str(AIRFLOW_ROOT_PATH / "scripts" / "ci" / "prek" / 
"download_k8s_schemas.py"),
+        "--output-dir",
+        str(output_dir),
+        "--versions",
+        *versions_to_download,
+    ]
+    run_command(cmd, check=False, env=command_env)
+
+
 @ci_group.command(
     name="upgrade",
     help="Perform important upgrade steps of the CI environment. And create a 
PR",
@@ -453,10 +549,64 @@ def get_workflow_info(github_context: str, 
github_context_input: StringIO):
     help="Automatically switch to the base branch if not already on it (if not 
specified, will ask)",
     is_flag=True,
 )
[email protected](
+    "--airflow-site",
+    default="../airflow-site",
+    show_default=True,
+    type=click.Path(file_okay=False, dir_okay=True, resolve_path=True, 
path_type=Path),
+    help="Path to airflow-site checkout for publishing K8s schemas",
+)
[email protected](
+    "--force-k8s-schema-sync",
+    is_flag=True,
+    default=False,
+    help="Force syncing K8s schemas to airflow-site even if all versions 
appear published",
+)
[email protected](
+    "--autoupdate/--no-autoupdate",
+    default=True,
+    show_default=True,
+    help="Run prek autoupdate to update hook revisions",
+)
[email protected](
+    "--pin-versions/--no-pin-versions",
+    default=True,
+    show_default=True,
+    help="Run pin-versions to pin CI dependency versions",
+)
[email protected](
+    "--update-chart-dependencies/--no-update-chart-dependencies",
+    default=True,
+    show_default=True,
+    help="Run update-chart-dependencies to update Helm chart dependencies",
+)
[email protected](
+    "--upgrade-important-versions/--no-upgrade-important-versions",
+    default=True,
+    show_default=True,
+    help="Run upgrade-important-versions to bump key dependency versions",
+)
[email protected](
+    "--k8s-schema-sync/--no-k8s-schema-sync",
+    default=True,
+    show_default=True,
+    help="Sync K8s JSON schemas to airflow-site",
+)
 @option_answer
 @option_verbose
 @option_dry_run
-def upgrade(target_branch: str, create_pr: bool | None, switch_to_base: bool | 
None):
+def upgrade(
+    target_branch: str,
+    create_pr: bool | None,
+    switch_to_base: bool | None,
+    airflow_site: Path,
+    force_k8s_schema_sync: bool,
+    autoupdate: bool,
+    pin_versions: bool,
+    update_chart_dependencies: bool,
+    upgrade_important_versions: bool,
+    k8s_schema_sync: bool,
+):
     # Validate target_branch pattern
     target_branch_pattern = re.compile(r"^(main|v\d+-\d+-test)$")
     if not target_branch_pattern.match(target_branch):
@@ -634,16 +784,37 @@ def upgrade(target_branch: str, create_pr: bool | None, 
switch_to_base: bool | N
         )
 
     # Define all upgrade commands to run (all run with check=False to continue 
on errors)
-    upgrade_commands = [
-        "prek autoupdate --cooldown-days 4 --freeze",
-        "prek --all-files --verbose --hook-stage manual pin-versions",
-        "prek --all-files --show-diff-on-failure --color always --verbose 
--hook-stage manual update-chart-dependencies",
-        "prek --all-files --show-diff-on-failure --color always --verbose 
--hook-stage manual upgrade-important-versions",
+    upgrade_commands: list[tuple[str, str]] = [
+        ("autoupdate", "prek autoupdate --cooldown-days 4 --freeze"),
+        ("pin-versions", "prek --all-files --verbose --hook-stage manual 
pin-versions"),
+        (
+            "update-chart-dependencies",
+            "prek --all-files --show-diff-on-failure --color always --verbose 
--hook-stage manual update-chart-dependencies",
+        ),
+        (
+            "upgrade-important-versions",
+            "prek --all-files --show-diff-on-failure --color always --verbose 
--hook-stage manual upgrade-important-versions",
+        ),
     ]
+    step_enabled = {
+        "autoupdate": autoupdate,
+        "pin-versions": pin_versions,
+        "update-chart-dependencies": update_chart_dependencies,
+        "upgrade-important-versions": upgrade_important_versions,
+    }
+
+    # Execute enabled upgrade commands with the environment containing GitHub 
token
+    for step_name, command in upgrade_commands:
+        if step_enabled[step_name]:
+            run_command(command.split(), check=False, env=command_env)
+        else:
+            get_console().print(f"[info]Skipping {step_name} (disabled).[/]")
 
-    # Execute all upgrade commands with the environment containing GitHub token
-    for command in upgrade_commands:
-        run_command(command.split(), check=False, env=command_env)
+    # Sync K8s schemas to airflow-site
+    if k8s_schema_sync:
+        _sync_k8s_schemas_to_airflow_site(airflow_site, force_k8s_schema_sync, 
command_env)
+    else:
+        get_console().print("[info]Skipping K8s schema sync (disabled).[/]")
 
     res = run_command(["git", "diff", "--exit-code"], check=False)
     if res.returncode == 0:
diff --git a/dev/breeze/src/airflow_breeze/commands/ci_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/ci_commands_config.py
index 2df31059727..e3bc9b89d4b 100644
--- a/dev/breeze/src/airflow_breeze/commands/ci_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/ci_commands_config.py
@@ -75,8 +75,20 @@ CI_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = 
{
                 "--target-branch",
                 "--create-pr",
                 "--switch-to-base",
+                "--airflow-site",
+                "--force-k8s-schema-sync",
             ],
-        }
+        },
+        {
+            "name": "Upgrade steps",
+            "options": [
+                "--autoupdate",
+                "--pin-versions",
+                "--update-chart-dependencies",
+                "--upgrade-important-versions",
+                "--k8s-schema-sync",
+            ],
+        },
     ],
     "breeze ci set-milestone": [
         {
diff --git 
a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py 
b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
index cc30b9c97e3..253cf846003 100644
--- a/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/release_management_commands.py
@@ -260,7 +260,7 @@ class VersionedFile(NamedTuple):
 
 
 AIRFLOW_PIP_VERSION = "26.0.1"
-AIRFLOW_UV_VERSION = "0.10.7"
+AIRFLOW_UV_VERSION = "0.10.8"
 AIRFLOW_USE_UV = False
 GITPYTHON_VERSION = "3.1.46"
 RICH_VERSION = "14.3.3"
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py 
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index c8cad6db6c3..55f1b0c49be 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -86,6 +86,7 @@ from airflow_breeze.commands.release_management_commands 
import option_distribut
 from airflow_breeze.global_constants import (
     ALL_TEST_SUITES,
     ALL_TEST_TYPE,
+    ALLOWED_KUBERNETES_VERSIONS,
     ALLOWED_TEST_TYPE_CHOICES,
     GroupOfTests,
     all_selective_core_test_types,
@@ -572,6 +573,16 @@ option_test_type_helm = click.option(
     show_default=True,
     type=BetterChoice(ALLOWED_TEST_TYPE_CHOICES[GroupOfTests.HELM]),
 )
+# Strip "v" prefix from ALLOWED_KUBERNETES_VERSIONS for helm tests (schemas 
use bare versions)
+_HELM_K8S_VERSIONS = [v.lstrip("v") for v in ALLOWED_KUBERNETES_VERSIONS]
+option_helm_kubernetes_version = click.option(
+    "--kubernetes-version",
+    help="Kubernetes version to validate helm templates against",
+    default=_HELM_K8S_VERSIONS[0],
+    envvar="HELM_TEST_KUBERNETES_VERSION",
+    show_default=True,
+    type=BetterChoice(_HELM_K8S_VERSIONS),
+)
 option_test_type_task_sdk_group = click.option(
     "--test-type",
     help="Type of test to run for task SDK",
@@ -1243,6 +1254,7 @@ def system_tests(
 @option_test_timeout
 @option_parallelism
 @option_test_type_helm
+@option_helm_kubernetes_version
 @option_use_xdist
 @option_verbose
 @option_dry_run
@@ -1253,6 +1265,7 @@ def helm_tests(
     github_repository: str,
     test_timeout: int,
     test_type: str,
+    kubernetes_version: str,
     parallelism: int,
     use_xdist: bool,
 ):
@@ -1263,6 +1276,7 @@ def helm_tests(
         test_type=test_type,
     )
     env = shell_params.env_variables_for_docker_commands
+    env["HELM_TEST_KUBERNETES_VERSION"] = kubernetes_version
     perform_environment_checks()
     fix_ownership_using_docker()
     cleanup_python_generated_files()
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 68a7150f7f3..ca3b0c11f61 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
@@ -265,6 +265,7 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
             "options": [
                 "--test-type",
                 "--test-timeout",
+                "--kubernetes-version",
                 "--use-xdist",
                 "--parallelism",
             ],
diff --git a/dev/breeze/src/airflow_breeze/global_constants.py 
b/dev/breeze/src/airflow_breeze/global_constants.py
index 390af7fcd77..386928e5265 100644
--- a/dev/breeze/src/airflow_breeze/global_constants.py
+++ b/dev/breeze/src/airflow_breeze/global_constants.py
@@ -219,7 +219,7 @@ if MYSQL_INNOVATION_RELEASE:
 ALLOWED_INSTALL_MYSQL_CLIENT_TYPES = ["mariadb"]
 
 PIP_VERSION = "26.0.1"
-UV_VERSION = "0.10.7"
+UV_VERSION = "0.10.8"
 
 # packages that providers docs
 REGULAR_DOC_PACKAGES = [
diff --git a/dev/breeze/src/airflow_breeze/utils/selective_checks.py 
b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
index 8470b1e0247..4a349c459ac 100644
--- a/dev/breeze/src/airflow_breeze/utils/selective_checks.py
+++ b/dev/breeze/src/airflow_breeze/utils/selective_checks.py
@@ -1432,6 +1432,16 @@ class SelectiveChecks:
     def helm_test_packages(self) -> str:
         return json.dumps(all_helm_test_packages())
 
+    @cached_property
+    def helm_test_kubernetes_versions(self) -> str:
+        default = CURRENT_KUBERNETES_VERSIONS[0]
+        if self.all_versions:
+            last = CURRENT_KUBERNETES_VERSIONS[-1]
+            versions = [default] if default == last else [default, last]
+        else:
+            versions = [default]
+        return json.dumps([v.lstrip("v") for v in versions])
+
     @cached_property
     def selected_providers_list_as_string(self) -> str | None:
         if self._default_branch != "main":
diff --git a/dev/breeze/tests/test_selective_checks.py 
b/dev/breeze/tests/test_selective_checks.py
index 3ac9f26f1cf..054c8346a8e 100644
--- a/dev/breeze/tests/test_selective_checks.py
+++ b/dev/breeze/tests/test_selective_checks.py
@@ -54,6 +54,15 @@ ALL_KUBERNETES_VERSIONS_AS_LIST = "[" + ", ".join([f"'{v}'" 
for v in ALLOWED_KUB
 ALL_PYTHON_VERSIONS_AS_STRING = " ".join(ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS)
 ALL_PYTHON_VERSIONS_AS_LIST = "[" + ", ".join([f"'{v}'" for v in 
ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS]) + "]"
 
+DEFAULT_HELM_K8S_VERSION = ALLOWED_KUBERNETES_VERSIONS[0].lstrip("v")
+LAST_HELM_K8S_VERSION = ALLOWED_KUBERNETES_VERSIONS[-1].lstrip("v")
+DEFAULT_HELM_K8S_VERSIONS_JSON = json.dumps([DEFAULT_HELM_K8S_VERSION])
+ALL_HELM_K8S_VERSIONS_JSON = json.dumps(
+    [DEFAULT_HELM_K8S_VERSION]
+    if DEFAULT_HELM_K8S_VERSION == LAST_HELM_K8S_VERSION
+    else [DEFAULT_HELM_K8S_VERSION, LAST_HELM_K8S_VERSION]
+)
+
 PYTHON_K8S_COMBO_LENGTH = max(len(ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS), 
len(ALLOWED_KUBERNETES_VERSIONS))
 PYTHON_VERSIONS_MAX = (ALLOWED_PYTHON_MAJOR_MINOR_VERSIONS * 
2)[:PYTHON_K8S_COMBO_LENGTH]
 KUBERNETES_VERSIONS_MAX = (ALLOWED_KUBERNETES_VERSIONS * 
2)[:PYTHON_K8S_COMBO_LENGTH]
@@ -3399,3 +3408,55 @@ dependencies = [
     # Should pass with the skip label
     result = selective_checks.common_compat_changed_without_next_version
     assert result is True
+
+
[email protected](
+    ("files", "pr_labels", "expected_outputs"),
+    [
+        pytest.param(
+            ("helm-tests/tests/helm_tests/random_helm_test.py",),
+            (),
+            {
+                "helm-test-kubernetes-versions": 
DEFAULT_HELM_K8S_VERSIONS_JSON,
+            },
+            id="Default K8s version when no all-versions label",
+        ),
+        pytest.param(
+            ("helm-tests/tests/helm_tests/random_helm_test.py",),
+            ("all versions",),
+            {
+                "helm-test-kubernetes-versions": ALL_HELM_K8S_VERSIONS_JSON,
+            },
+            id="First and last K8s versions when all-versions label is set",
+        ),
+        pytest.param(
+            ("INTHEWILD.md",),
+            ("full tests needed", "all versions"),
+            {
+                "helm-test-kubernetes-versions": ALL_HELM_K8S_VERSIONS_JSON,
+                "all-versions": "true",
+            },
+            id="First and last K8s versions when full tests needed with all 
versions",
+        ),
+        pytest.param(
+            ("INTHEWILD.md",),
+            ("full tests needed",),
+            {
+                "helm-test-kubernetes-versions": 
DEFAULT_HELM_K8S_VERSIONS_JSON,
+                "all-versions": "false",
+            },
+            id="Default K8s version when full tests needed but no all-versions 
label",
+        ),
+    ],
+)
+def test_helm_test_kubernetes_versions(
+    files: tuple[str, ...], pr_labels: tuple[str, ...], expected_outputs: 
dict[str, str]
+):
+    stderr = SelectiveChecks(
+        files=files,
+        commit_ref=NEUTRAL_COMMIT,
+        github_event=GithubEvents.PULL_REQUEST,
+        pr_labels=pr_labels,
+        default_branch="main",
+    )
+    assert_outputs_are_printed(expected_outputs, str(stderr))
diff --git a/dev/breeze/uv.lock b/dev/breeze/uv.lock
index 3bc2641740b..99f8b4ef812 100644
--- a/dev/breeze/uv.lock
+++ b/dev/breeze/uv.lock
@@ -260,30 +260,30 @@ wheels = [
 
 [[package]]
 name = "boto3"
-version = "1.42.59"
+version = "1.42.60"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "botocore" },
     { name = "jmespath" },
     { name = "s3transfer" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/b0/4e/499cb52aaee9468c346bcc1158965e24e72b4e2a20052725b680e0ac949b/boto3-1.42.59.tar.gz";,
 hash = 
"sha256:6c4a14a4eb37b58a9048901bdeefbe1c529638b73e8f55413319a25f010ca211", size 
= 112725, upload-time = "2026-02-27T20:25:33.228Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/b8/e0/071e00265d3d8127b28c27ba3918ba283f49b39943864a389ac3f5096ef3/boto3-1.42.60.tar.gz";,
 hash = 
"sha256:3d549d15c821dcc871a0821319049e7d493ae3317121eb01e4b1f5230c19d5d4", size 
= 112786, upload-time = "2026-03-03T21:21:07.199Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/17/c0/22d868b9408dc5a33935a72896ec8d638b2766c459668d1b37c3e5ac2066/boto3-1.42.59-py3-none-any.whl";,
 hash = 
"sha256:7a66e3e8e2087ea4403e135e9de592e6d63fc9a91080d8dac415bb74df873a72", size 
= 140557, upload-time = "2026-02-27T20:25:31.774Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ed/30/156ff2b5afb7dd03383b5f97d0e32535e9c0e783917380c476fe2fbc1874/boto3-1.42.60-py3-none-any.whl";,
 hash = 
"sha256:c0cc3d93cd76c99461f6e109e04bb020defe3ffcd04c6163c72836dff5591614", size 
= 140554, upload-time = "2026-03-03T21:21:05.194Z" },
 ]
 
 [[package]]
 name = "botocore"
-version = "1.42.59"
+version = "1.42.60"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
     { name = "jmespath" },
     { name = "python-dateutil" },
     { name = "urllib3" },
 ]
-sdist = { url = 
"https://files.pythonhosted.org/packages/45/ae/50fb33bdf1911c216d50f98d989dd032a506f054cf829ebd737c6fa7e3e6/botocore-1.42.59.tar.gz";,
 hash = 
"sha256:5314f19e1da8fc0ebc41bdb8bbe17c9a7397d87f4d887076ac8bdef972a34138", size 
= 14950271, upload-time = "2026-02-27T20:25:20.614Z" }
+sdist = { url = 
"https://files.pythonhosted.org/packages/82/d7/bfe8413cc7dc167e04ca9c68ea136251307960f662ec5889512615565b25/botocore-1.42.60.tar.gz";,
 hash = 
"sha256:de9278810fb2e92a9ffe3dc8ffa68f1066e6d2caf19da9460760743b39ca5215", size 
= 14950855, upload-time = "2026-03-03T21:20:51.529Z" }
 wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/59/df/9d52819e0d804ead073d53ab1823bc0f0cb172a250fba31107b0b43fbb04/botocore-1.42.59-py3-none-any.whl";,
 hash = 
"sha256:d2f2ff7ecc31e86ef46b5daee112cfbca052c13801285fb23af909f7bff5b657", size 
= 14619293, upload-time = "2026-02-27T20:25:17.455Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8d/63/5cf970a00e9ddcbb9e65ecc79276717a9555a77d3d0571bd962676e19c3b/botocore-1.42.60-py3-none-any.whl";,
 hash = 
"sha256:d8b4aab06cc134e21d294c068cb94e0eeb59bacd27c836fb6b882b61433df2f4", size 
= 14621726, upload-time = "2026-03-03T21:20:47.935Z" },
 ]
 
 [[package]]
@@ -2072,27 +2072,27 @@ wheels = [
 
 [[package]]
 name = "uv"
-version = "0.10.7"
-source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/7c/ec/b324a43b55fe59577505478a396cb1d2758487a2e2270c81ccfa4ac6c96d/uv-0.10.7.tar.gz";,
 hash = 
"sha256:7c3b0133c2d6bd725d5a35ec5e109ebf0d75389943abe826f3d9ea6d6667a375", size 
= 3922193, upload-time = "2026-02-27T12:33:58.525Z" }
-wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/f3/1b/decff24553325561850d70b75c737076e6fcbcfbf233011a27a33f06e4d9/uv-0.10.7-py3-none-linux_armv6l.whl";,
 hash = 
"sha256:6a0af6c7a90fd2053edfa2c8ee719078ea906a2d9f4798d3fb3c03378726209a", size 
= 22497542, upload-time = "2026-02-27T12:33:39.425Z" },
-    { url = 
"https://files.pythonhosted.org/packages/fc/b5/51152c87921bc2576fecb982df4a02ac9cfd7fc934e28114a1232b99eed4/uv-0.10.7-py3-none-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:3b7db0cab77232a7c8856062904fc3b9db22383f1dec7e97a9588fb6c8470f6a", size 
= 21558860, upload-time = "2026-02-27T12:34:03.362Z" },
-    { url = 
"https://files.pythonhosted.org/packages/5e/15/8365dc2ded350a4ee5fcbbf9b15195cb2b45855114f2a154b5effb6fa791/uv-0.10.7-py3-none-macosx_11_0_arm64.whl";,
 hash = 
"sha256:d872d2ff9c9dfba989b5f05f599715bc0f19b94cd0dbf8ae4ad22f8879a66c8c", size 
= 20212775, upload-time = "2026-02-27T12:33:55.365Z" },
-    { url = 
"https://files.pythonhosted.org/packages/53/a0/ccf25e897f3907b5a6fd899007ff9a80b5bbf151b3a75a375881005611fd/uv-0.10.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:d9b40d03693efda80a41e5d18ac997efdf1094b27fb75471c1a8f51a9ebeffb3", size 
= 22015584, upload-time = "2026-02-27T12:33:47.374Z" },
-    { url = 
"https://files.pythonhosted.org/packages/fa/3a/5099747954e7774768572d30917bb6bda6b8d465d7a3c49c9bbf7af2a812/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:e74fe4df9cf31fe84f20b84a0054874635077d31ce20e7de35ff0dd64d498d7b", size 
= 22100376, upload-time = "2026-02-27T12:34:06.169Z" },
-    { url = 
"https://files.pythonhosted.org/packages/0c/1a/75897fd966b871803cf78019fa31757ced0d54af5ffd7f57bce8b01d64f3/uv-0.10.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:9c76659fc8bb618dd35cd83b2f479c6f880555a16630a454a251045c4c118ea4", size 
= 22105202, upload-time = "2026-02-27T12:34:16.972Z" },
-    { url = 
"https://files.pythonhosted.org/packages/b5/1e/0b8caedd66ca911533e18fd051da79a213c792404138812c66043d529b9e/uv-0.10.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl";,
 hash = 
"sha256:1d160cceb9468024ca40dc57a180289dfd2024d98e42f2284b9ec44355723b0a", size 
= 23335601, upload-time = "2026-02-27T12:34:11.161Z" },
-    { url = 
"https://files.pythonhosted.org/packages/69/94/b741af277e39a92e0da07fe48c338eee1429c2607e7a192e41345208bb24/uv-0.10.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:c775975d891cb60cf10f00953e61e643fcb9a9139e94c9ef5c805fe36e90477f", size 
= 24152851, upload-time = "2026-02-27T12:33:33.904Z" },
-    { url = 
"https://files.pythonhosted.org/packages/27/b2/da351ccd02f0fb1aec5f992b886bea1374cce44276a78904348e2669dd78/uv-0.10.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:a709e75583231cc1f39567fb3d8d9b4077ff94a64046eb242726300144ed1a4a", size 
= 23276444, upload-time = "2026-02-27T12:33:36.891Z" },
-    { url = 
"https://files.pythonhosted.org/packages/71/a9/2735cc9dc39457c9cf64d1ce2ba5a9a8ecbb103d0fb64b052bf33ba3d669/uv-0.10.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:89de2504407dcf04aece914c6ca3b9d8e60cf9ff39a13031c1df1f7c040cea81", size 
= 23218464, upload-time = "2026-02-27T12:34:00.904Z" },
-    { url = 
"https://files.pythonhosted.org/packages/20/5f/5f204e9c3f04f5fc844d2f98d80a7de64b6b304af869644ab478d909f6ff/uv-0.10.7-py3-none-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:9945de1d11c4a5ad77e9c4f36f8b5f9e7c9c3c32999b8bc0e7e579145c3b641c", size 
= 22092562, upload-time = "2026-02-27T12:34:14.155Z" },
-    { url = 
"https://files.pythonhosted.org/packages/dd/a4/16bebf106e3289a29cc1e1482d551c49bd220983e9b4bc5960142389ad3f/uv-0.10.7-py3-none-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:dbe43527f478e2ffa420516aa465f82057763936bbea56f814fd054a9b7f961f", size 
= 22851312, upload-time = "2026-02-27T12:34:08.651Z" },
-    { url = 
"https://files.pythonhosted.org/packages/d1/7a/953b1da589225d98ca8668412f665c3192f6deed2a0f4bb782b0df18f611/uv-0.10.7-py3-none-musllinux_1_1_i686.whl";,
 hash = 
"sha256:c0783f327631141501bdc5f31dd2b4c748df7e7f5dc5cdbfc0fbb82da86cc9ca", size 
= 22543775, upload-time = "2026-02-27T12:33:30.935Z" },
-    { url = 
"https://files.pythonhosted.org/packages/8b/67/e133afdabf76e43989448be1c2ef607f13afc32aa1ee9f6897115dec8417/uv-0.10.7-py3-none-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:eba438899010522812d3497af586e6eedc94fa2b0ced028f51812f0c10aafb30", size 
= 23431187, upload-time = "2026-02-27T12:33:42.131Z" },
-    { url = 
"https://files.pythonhosted.org/packages/ba/40/6ffb58ec88a33d6cbe9a606966f9558807f37a50f7be7dc756824df2d04c/uv-0.10.7-py3-none-win32.whl";,
 hash = 
"sha256:b56d1818aafb2701d92e94f552126fe71d30a13f28712d99345ef5cafc53d874", size 
= 21524397, upload-time = "2026-02-27T12:33:44.579Z" },
-    { url = 
"https://files.pythonhosted.org/packages/e3/1f/74f4d625db838f716a555908d41777b6357bacc141ddef117a01855e5ef9/uv-0.10.7-py3-none-win_amd64.whl";,
 hash = 
"sha256:ad0d0ddd9f5407ad8699e3b20fe6c18406cd606336743e246b16914801cfd8b0", size 
= 23999929, upload-time = "2026-02-27T12:33:49.839Z" },
-    { url = 
"https://files.pythonhosted.org/packages/48/4e/20cbfbcb1a0f48c5c1ca94f6baa0fa00754aafda365da9160c15e3b9c277/uv-0.10.7-py3-none-win_arm64.whl";,
 hash = 
"sha256:edf732de80c1a9701180ef8c7a2fa926a995712e4a34ae8c025e090f797c2e0b", size 
= 22353084, upload-time = "2026-02-27T12:33:52.792Z" },
+version = "0.10.8"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/a3/e7/600a90d4662dbd8414c1f6b709c8c79075d37d2044f72b94acbfaf29baad/uv-0.10.8.tar.gz";,
 hash = 
"sha256:4b23242b5224c7eaea481ce6c6dbc210f0eafb447cf60211633980947cd23de4", size 
= 3936600, upload-time = "2026-03-03T21:35:22.386Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/a6/6c/8ef256575242d5f3869c5a445ffd4363b91a89acb34a3e043bec2ad5a1be/uv-0.10.8-py3-none-linux_armv6l.whl";,
 hash = 
"sha256:d214c82c7c14dd23f9aeb609d03070b8ea2b2f0cf249c9321cbbb5375a17e5df", size 
= 22461003, upload-time = "2026-03-03T21:35:20.093Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c9/fb/fd0656a92e6b9c4f92ddba7dcd76bd87469be500755125e06fea853dc212/uv-0.10.8-py3-none-macosx_10_12_x86_64.whl";,
 hash = 
"sha256:d1315c3901c5859aec2c5b4a17da4c5410d17f6890890f9f1a31f25aa0fa9ace", size 
= 21549446, upload-time = "2026-03-03T21:35:58.203Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/b9/1a4105df3afe7af99791f5b00fb037d85b2e3aaa1227e95878538d51ecf3/uv-0.10.8-py3-none-macosx_11_0_arm64.whl";,
 hash = 
"sha256:a253e5d2cae9e02654de31918b610dfc8f1f16a33f34046603757820bc45ee1b", size 
= 20222180, upload-time = "2026-03-03T21:35:46.984Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c5/72/6e98e0f8b3fe80cb881c36492dca6d932fbb05f956dfdccbdb8ebe4ceff4/uv-0.10.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl";,
 hash = 
"sha256:57a24e15fd9dd4a36bcec2ccbe4b26d2a172c109e954a8940f5e8a8b965dae74", size 
= 22064813, upload-time = "2026-03-03T21:35:17.108Z" },
+    { url = 
"https://files.pythonhosted.org/packages/71/b6/737da8577f4b1799f7024f6cd98fffcac77076a1b078b277cffc84946e96/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl";,
 hash = 
"sha256:675dc659195f9b9811ef5534eb3f16459fc88e109aefacbc91c07751b5b9715a", size 
= 22064861, upload-time = "2026-03-03T21:35:25.067Z" },
+    { url = 
"https://files.pythonhosted.org/packages/7e/21/464ee3cd81f44345953cb26dd49870811f7647f3074f7651775cadb2158b/uv-0.10.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl";,
 hash = 
"sha256:18d2968b0a50111c2fc6b782f7c63ded4f461c44efab537f552cf565f9aaae25", size 
= 22054515, upload-time = "2026-03-03T21:35:44.572Z" },
+    { url = 
"https://files.pythonhosted.org/packages/11/2c/1c592d7b843ffa999502116b0dc573732b40cb37061a4acc741dcdb181da/uv-0.10.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl";,
 hash = 
"sha256:8ed3c7ebb6f757cddedb56dec3d7c745e5ea7310b11e12ae1c28f1e8172e7bbf", size 
= 23433992, upload-time = "2026-03-03T21:35:36.886Z" },
+    { url = 
"https://files.pythonhosted.org/packages/f1/e2/2b716f0613746138294598668bbe65295a8da3d8fa104a756dec6284bf3c/uv-0.10.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl";,
 hash = 
"sha256:ffaf115501e33be0d4f13cb5b7c2b46b031d4c679a6109e24a7edfb719c44c6c", size 
= 24257250, upload-time = "2026-03-03T21:35:49.954Z" },
+    { url = 
"https://files.pythonhosted.org/packages/3e/4d/0165e82cd1117cd6f8a7d9a2122c23cc091f7cf738aa4a2a54579420a08f/uv-0.10.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl";,
 hash = 
"sha256:0209ee8cb573e113ff4a760360f28448f9ebcdcf9c91ca49e872821de5d2d054", size 
= 23338918, upload-time = "2026-03-03T21:35:33.795Z" },
+    { url = 
"https://files.pythonhosted.org/packages/20/74/652129a25145732482bb0020602507f52d9a5ca0e1a40ddd6deb27402333/uv-0.10.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl";,
 hash = 
"sha256:11dc790f732dc5fee61f0f6bd998fc2e9c200df1082245604ac091c32c23a523", size 
= 23259370, upload-time = "2026-03-03T21:35:39.478Z" },
+    { url = 
"https://files.pythonhosted.org/packages/19/c5/6e5923d6c9e3b50dc8542647bea692b7c227a9489f59ddff4fdfb20d8459/uv-0.10.8-py3-none-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:e26f8c35684face38db814d452dd1a2181152dbf7f7b2de1f547e6ba0c378d67", size 
= 22174747, upload-time = "2026-03-03T21:35:42.081Z" },
+    { url = 
"https://files.pythonhosted.org/packages/92/cd/eee9e1883888327d07f51e7595ed5952e0bca2dc79d1c03b8a6e4309553e/uv-0.10.8-py3-none-manylinux_2_31_riscv64.whl";,
 hash = 
"sha256:385add107d40c43dc00ca8c1a21ecf43101f846f8339eb7026bf6c9f6df7760d", size 
= 22893359, upload-time = "2026-03-03T21:35:30.802Z" },
+    { url = 
"https://files.pythonhosted.org/packages/bf/36/407a22917e55ce5cc2e7af956e3b9d91648a96558858acef84e3c50d5ca8/uv-0.10.8-py3-none-musllinux_1_1_i686.whl";,
 hash = 
"sha256:24e8eb28c4f05acb38e60fefe2a2b15f4283a3849ce580bf2a62aca0a13123b3", size 
= 22637451, upload-time = "2026-03-03T21:35:55.677Z" },
+    { url = 
"https://files.pythonhosted.org/packages/21/d5/dabef9914e1ff27ad95e4b1daf59cd97c80e26a44c04c2870bcca7c83fc0/uv-0.10.8-py3-none-musllinux_1_1_x86_64.whl";,
 hash = 
"sha256:73a8c1a1fceac73cd983dcc0a64f4f94f5fd1e5428681a5a76132574264504fb", size 
= 23480991, upload-time = "2026-03-03T21:35:52.809Z" },
+    { url = 
"https://files.pythonhosted.org/packages/2f/c0/1a4a45a9246f087e9446d0d804a436f6ee0befeaef731b04d1b2802d9d8f/uv-0.10.8-py3-none-win32.whl";,
 hash = 
"sha256:9f344fdb34938ce35e9211a1b866adfa0c7f043967652ed1431917514aeec062", size 
= 21579030, upload-time = "2026-03-03T21:35:28.176Z" },
+    { url = 
"https://files.pythonhosted.org/packages/a4/2b/b29510efa1e6f409db105dbdafbd942ca3a2b638bef682ff2e5b9f6e4021/uv-0.10.8-py3-none-win_amd64.whl";,
 hash = 
"sha256:1e63015284ed28c2112717256c328513215fb966a57c5870788eac2e8f949f28", size 
= 23944828, upload-time = "2026-03-03T21:36:00.763Z" },
+    { url = 
"https://files.pythonhosted.org/packages/3f/9e/b5a11b0523171c0103c4fed54da76685a765ad4d3215e8220facfd24aed9/uv-0.10.8-py3-none-win_arm64.whl";,
 hash = 
"sha256:a80284f46b6f2e0b3d03eb7c2d43e17139a4ec313e8b9f56a71efafc996804cb", size 
= 22322224, upload-time = "2026-03-03T21:35:14.148Z" },
 ]
 
 [[package]]
diff --git a/helm-tests/tests/chart_utils/helm_template_generator.py 
b/helm-tests/tests/chart_utils/helm_template_generator.py
index f8ea40e2b8d..f4f0d0576e4 100644
--- a/helm-tests/tests/chart_utils/helm_template_generator.py
+++ b/helm-tests/tests/chart_utils/helm_template_generator.py
@@ -16,31 +16,47 @@
 # under the License.
 from __future__ import annotations
 
+import ast
 import json
 import os
 import subprocess
-from datetime import datetime, timezone
 from functools import cache
 from pathlib import Path
 from tempfile import NamedTemporaryFile
 from typing import Any
+from urllib.error import HTTPError
+from urllib.request import urlopen
 
 import jmespath
 import jsonschema
-import requests
 import yaml
 from kubernetes.client.api_client import ApiClient
-from requests import Response
-from rich.console import Console
 
 api_client = ApiClient()
 
-CHART_DIR = Path(__file__).resolve().parents[3] / "chart"
+AIRFLOW_ROOT = Path(__file__).resolve().parents[3]
+CHART_DIR = AIRFLOW_ROOT / "chart"
 
-DEFAULT_KUBERNETES_VERSION = "1.30.13"
-BASE_URL_SPEC = (
-    f"https://api.github.com/repos/yannh/kubernetes-json-schema/contents/";
-    f"v{DEFAULT_KUBERNETES_VERSION}-standalone-strict"
+SCHEMA_URL_TEMPLATE = (
+    
"https://airflow.apache.org/k8s-schemas/v{kubernetes_version}-standalone-strict/{filename}";
+)
+
+
+def _read_default_kubernetes_version() -> str:
+    """Read the first ALLOWED_KUBERNETES_VERSIONS entry from 
global_constants.py."""
+    gc_path = AIRFLOW_ROOT / "dev" / "breeze" / "src" / "airflow_breeze" / 
"global_constants.py"
+    tree = ast.parse(gc_path.read_text())
+    for node in tree.body:
+        if isinstance(node, ast.Assign):
+            for target in node.targets:
+                if isinstance(target, ast.Name) and target.id == 
"ALLOWED_KUBERNETES_VERSIONS":
+                    versions: list[str] = ast.literal_eval(node.value)
+                    return versions[0].lstrip("v")
+    raise RuntimeError("ALLOWED_KUBERNETES_VERSIONS not found in 
global_constants.py")
+
+
+DEFAULT_KUBERNETES_VERSION = os.environ.get(
+    "HELM_TEST_KUBERNETES_VERSION", _read_default_kubernetes_version()
 )
 
 MY_DIR = Path(__file__).parent.resolve()
@@ -51,50 +67,6 @@ crd_lookup = {
 }
 
 
-GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")
-
-console = Console(width=400, color_system="standard")
-
-
-def log_github_rate_limit_error(response: Response) -> None:
-    """
-    Logs info about GitHub rate limit errors (primary or secondary).
-    """
-    if response.status_code not in (403, 429):
-        return
-
-    remaining = response.headers.get("x-rateLimit-remaining")
-    reset = response.headers.get("x-rateLimit-reset")
-    retry_after = response.headers.get("retry-after")
-
-    try:
-        message = response.json().get("message", "")
-    except Exception:
-        message = response.text or ""
-
-    remaining_int = int(remaining) if remaining and remaining.isdigit() else 
None
-
-    if reset and reset.isdigit():
-        reset_dt = datetime.fromtimestamp(int(reset), tz=timezone.utc)
-        reset_time = reset_dt.strftime("%Y-%m-%d %H:%M:%S UTC")
-    else:
-        reset_time = "unknown"
-
-    if remaining_int == 0:
-        print(f"Primary rate limit exceeded. No requests remaining. Reset at 
{reset_time}.")
-        return
-
-    # Message for secondary looks like: "You have exceeded a secondary rate 
limit"
-    if "secondary rate limit" in message.lower():
-        if retry_after and retry_after.isdigit():
-            print(f"Secondary rate limit exceeded. Retry after {retry_after} 
seconds.")
-        else:
-            print(f"Secondary rate limit exceeded. Please wait until 
{reset_time} or at least 60 seconds.")
-        return
-
-    print(f"Rate limit error. Status: {response.status_code}, Message: 
{message}")
-
-
 @cache
 def get_schema_k8s(api_version, kind, kubernetes_version):
     api_version = api_version.lower()
@@ -103,27 +75,21 @@ def get_schema_k8s(api_version, kind, kubernetes_version):
     if "/" in api_version:
         ext, _, api_version = api_version.partition("/")
         ext = ext.split(".")[0]
-        url = f"{BASE_URL_SPEC}/{kind}-{ext}-{api_version}.json"
+        filename = f"{kind}-{ext}-{api_version}.json"
     else:
-        url = f"{BASE_URL_SPEC}/{kind}-{api_version}.json"
+        filename = f"{kind}-{api_version}.json"
 
-    headers = {
-        "Accept": "application/vnd.github.v3.raw",
-    }
-    if GITHUB_TOKEN:
-        headers["Authorization"] = f"Bearer {GITHUB_TOKEN}"
-        headers["X-GitHub-Api-Version"] = "2022-11-28"
-    else:
-        console.print("[bright_blue] No GITHUB_TOKEN found. Using 
unauthenticated requests.")
-
-    response = requests.get(url, headers=headers)
-    log_github_rate_limit_error(response)
-    response.raise_for_status()
-    schema = json.loads(
-        response.text.replace(
-            "kubernetesjsonschema.dev", 
"raw.githubusercontent.com/yannh/kubernetes-json-schema/master"
-        )
-    )
+    url = SCHEMA_URL_TEMPLATE.format(kubernetes_version=kubernetes_version, 
filename=filename)
+    try:
+        resp = urlopen(url, timeout=30)
+        schema = json.loads(resp.read())
+    except HTTPError as e:
+        if e.code == 404:
+            raise FileNotFoundError(
+                f"K8s JSON schema not found at {url}\n"
+                f"Ensure schemas for K8s v{kubernetes_version} are published 
to airflow-site."
+            ) from e
+        raise
     return schema
 
 
diff --git a/pyproject.toml b/pyproject.toml
index e633cba7f44..c4c320ba023 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -538,7 +538,7 @@ packages = []
     "apache-airflow-providers-amazon[s3fs]",
 ]
 "uv" = [
-    "uv>=0.10.7",
+    "uv>=0.10.8",
 ]
 
 [project.urls]
diff --git a/scripts/ci/prek/check_k8s_schemas_published.py 
b/scripts/ci/prek/check_k8s_schemas_published.py
new file mode 100755
index 00000000000..53648fb4aa5
--- /dev/null
+++ b/scripts/ci/prek/check_k8s_schemas_published.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+# 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.
+"""Pre-commit hook that verifies K8s JSON schemas are published on 
airflow.apache.org.
+
+Triggered when ``global_constants.py`` changes.  For each version in
+``ALLOWED_KUBERNETES_VERSIONS``, sends a HEAD request to
+``https://airflow.apache.org/k8s-schemas/v{version}-standalone-strict/configmap-v1.json``.
+If any version returns non-200 the hook fails with instructions.
+"""
+
+from __future__ import annotations
+
+import sys
+from pathlib import Path
+from urllib.error import HTTPError, URLError
+from urllib.request import Request, urlopen
+
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from common_prek_utils import console, read_allowed_kubernetes_versions
+
+PROBE_URL_TEMPLATE = 
"https://airflow.apache.org/k8s-schemas/v{version}-standalone-strict/configmap-v1.json";
+
+
+def _print(msg: str) -> None:
+    if console:
+        console.print(msg)
+    else:
+        print(msg, file=sys.stderr)
+
+
+def main() -> int:
+    versions = read_allowed_kubernetes_versions()
+    missing: list[str] = []
+
+    for version in versions:
+        url = PROBE_URL_TEMPLATE.format(version=version)
+        req = Request(url, method="HEAD")
+        try:
+            resp = urlopen(req, timeout=15)
+            if resp.status == 200:
+                _print(f"  v{version}: published")
+            else:
+                _print(f"  v{version}: HTTP {resp.status}")
+                missing.append(version)
+        except HTTPError as e:
+            _print(f"  v{version}: HTTP {e.code}")
+            missing.append(version)
+        except URLError as e:
+            _print(f"  v{version}: {e.reason}")
+            missing.append(version)
+
+    if missing:
+        _print(
+            "\nK8s schemas are NOT published for the following versions:\n"
+            + "\n".join(f"  - v{v}" for v in missing)
+            + "\n\nTo fix this:\n"
+            "  1. Check out the airflow-site repository.\n"
+            "  2. Run: breeze ci upgrade --airflow-site 
<path-to-airflow-site>\n"
+            "  3. Commit and push changes in both repos.\n"
+        )
+        return 1
+
+    _print("All K8s schema versions are published.")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/scripts/ci/prek/common_prek_utils.py 
b/scripts/ci/prek/common_prek_utils.py
index c26c4888c6e..5a9024e00ac 100644
--- a/scripts/ci/prek/common_prek_utils.py
+++ b/scripts/ci/prek/common_prek_utils.py
@@ -113,6 +113,26 @@ def read_airflow_version() -> str:
     raise RuntimeError("Couldn't find __version__ in AST")
 
 
+GLOBAL_CONSTANTS_PATH = (
+    AIRFLOW_ROOT_PATH / "dev" / "breeze" / "src" / "airflow_breeze" / 
"global_constants.py"
+)
+
+
+def read_allowed_kubernetes_versions() -> list[str]:
+    """Parse ALLOWED_KUBERNETES_VERSIONS from global_constants.py (single 
source of truth).
+
+    Returns versions without the ``v`` prefix, e.g. ``["1.30.13", "1.31.12", 
...]``.
+    """
+    tree = ast.parse(GLOBAL_CONSTANTS_PATH.read_text())
+    for node in tree.body:
+        if isinstance(node, ast.Assign):
+            for target in node.targets:
+                if isinstance(target, ast.Name) and target.id == 
"ALLOWED_KUBERNETES_VERSIONS":
+                    versions: list[str] = ast.literal_eval(node.value)
+                    return [v.lstrip("v") for v in versions]
+    raise RuntimeError("ALLOWED_KUBERNETES_VERSIONS not found in 
global_constants.py")
+
+
 def pre_process_files(files: list[str]) -> list[str]:
     """Pre-process files passed to mypy.
 
diff --git a/scripts/ci/prek/download_k8s_schemas.py 
b/scripts/ci/prek/download_k8s_schemas.py
new file mode 100755
index 00000000000..60069ba9e65
--- /dev/null
+++ b/scripts/ci/prek/download_k8s_schemas.py
@@ -0,0 +1,281 @@
+#!/usr/bin/env python
+# 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.
+# /// script
+# requires-python = ">=3.10,<3.11"
+# dependencies = [
+#   "pyyaml>=6.0.3",
+#   "requests>=2.31.0",
+#   "rich>=13.6.0",
+# ]
+# ///
+"""Download K8s JSON schemas used by helm chart tests.
+
+Runs ``helm template`` with multiple value combinations to discover all
+rendered (apiVersion, kind) pairs, then downloads the matching
+standalone-strict JSON schemas from the yannh/kubernetes-json-schema
+GitHub repository for every supported Kubernetes version and stores them
+in a target directory (typically airflow-site/k8s-schemas for publishing
+to https://airflow.apache.org/k8s-schemas/).
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import subprocess
+import sys
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+import requests
+import yaml
+
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from common_prek_utils import AIRFLOW_ROOT_PATH, console, 
read_allowed_kubernetes_versions
+
+KUBERNETES_VERSIONS = read_allowed_kubernetes_versions()
+DEFAULT_KUBERNETES_VERSION = KUBERNETES_VERSIONS[0]
+CHART_DIR = AIRFLOW_ROOT_PATH / "chart"
+BASE_URL_TEMPLATE = (
+    
"https://api.github.com/repos/yannh/kubernetes-json-schema/contents/v{version}-standalone-strict";
+)
+
+# Value combinations that exercise conditional templates (executors, KEDA,
+# flower, persistence, pgbouncer, HPA, ingress, network policies,
+# priority classes, CronJobs, PDBs, ClusterRoles, etc.)
+VALUE_SETS: list[dict] = [
+    {},
+    {"executor": "CeleryExecutor", "flower": {"enabled": True}},
+    {"executor": "CeleryExecutor"},
+    {"executor": "KubernetesExecutor"},
+    {"executor": "CeleryKubernetesExecutor"},
+    {"executor": "LocalExecutor"},
+    {"executor": "LocalKubernetesExecutor"},
+    {
+        "executor": "CeleryExecutor",
+        "workers": {"keda": {"enabled": True}},
+    },
+    {"pgbouncer": {"enabled": True}},
+    {
+        "dags": {"persistence": {"enabled": True}},
+        "logs": {"persistence": {"enabled": True}},
+    },
+    {"redis": {"enabled": True}},
+    {"statsd": {"enabled": True}},
+    {
+        "webserver": {"defaultUser": {"enabled": True}},
+        "cleanup": {"enabled": True},
+        "databaseCleanup": {"enabled": True},
+    },
+    {
+        "ingress": {"web": {"enabled": True}, "flower": {"enabled": True}},
+        "networkPolicies": {"enabled": True},
+        "flower": {"enabled": True},
+        "executor": "CeleryExecutor",
+    },
+    {
+        "workers": {"hpa": {"enabled": True}},
+        "webserver": {
+            "hpa": {"enabled": True},
+            "podDisruptionBudget": {"enabled": True},
+        },
+        "scheduler": {"podDisruptionBudget": {"enabled": True}},
+    },
+    {
+        "multiNamespaceMode": True,
+        "limits": [{"type": "Container", "max": {"cpu": "2"}}],
+        "quotas": {"pods": "10"},
+    },
+    {
+        "priorityClasses": [
+            {"name": "high", "preemptionPolicy": "PreemptLowerPriority", 
"value": 1000},
+        ],
+    },
+]
+
+# Additional (apiVersion, kind) pairs that are needed but not produced by
+# ``helm template`` on the main chart (e.g. the pod-template-file is only
+# copied into templates/ during tests).
+EXTRA_PAIRS: set[tuple[str, str]] = {
+    ("v1", "Pod"),
+}
+
+
+def schema_filename(api_version: str, kind: str) -> str:
+    """Compute the schema filename using the same logic as 
``get_schema_k8s``."""
+    api_version = api_version.lower()
+    kind = kind.lower()
+    if "/" in api_version:
+        ext, _, ver = api_version.partition("/")
+        ext = ext.split(".")[0]
+        return f"{kind}-{ext}-{ver}.json"
+    return f"{kind}-{api_version}.json"
+
+
+def discover_pairs() -> set[tuple[str, str]]:
+    """Run helm template with each value set and collect (apiVersion, kind) 
pairs."""
+    pairs: set[tuple[str, str]] = set()
+    for values in VALUE_SETS:
+        with NamedTemporaryFile(mode="w", suffix=".yaml") as tmp:
+            yaml.dump(values, tmp)
+            tmp.flush()
+            result = subprocess.run(
+                [
+                    "helm",
+                    "template",
+                    "release-name",
+                    str(CHART_DIR),
+                    "--values",
+                    tmp.name,
+                    "--kube-version",
+                    DEFAULT_KUBERNETES_VERSION,
+                ],
+                capture_output=True,
+                check=False,
+            )
+            if result.returncode != 0:
+                console.print(
+                    f"[yellow]helm template failed for values {values}: 
{result.stderr.decode()[:200]}[/]"
+                )
+                continue
+            for obj in yaml.safe_load_all(result.stdout):
+                if obj and "apiVersion" in obj and "kind" in obj:
+                    pairs.add((obj["apiVersion"], obj["kind"]))
+    pairs.update(EXTRA_PAIRS)
+    return pairs
+
+
+def download_schema(base_url: str, filename: str, token: str | None, retries: 
int = 3) -> str | None:
+    """Download a single schema file from GitHub. Returns content or None."""
+    import time
+
+    url = f"{base_url}/{filename}"
+    headers = {"Accept": "application/vnd.github.v3.raw"}
+    if token:
+        headers["Authorization"] = f"Bearer {token}"
+        headers["X-GitHub-Api-Version"] = "2022-11-28"
+    for attempt in range(retries):
+        resp = requests.get(url, headers=headers)
+        if resp.status_code == 404:
+            console.print(f"[yellow]  Schema not found (404): {filename}[/]")
+            return None
+        if resp.status_code >= 500 and attempt < retries - 1:
+            wait = 2**attempt
+            console.print(f"[yellow]  Server error {resp.status_code}, 
retrying in {wait}s...[/]")
+            time.sleep(wait)
+            continue
+        resp.raise_for_status()
+        # Replace references to kubernetesjsonschema.dev with the raw GitHub 
URL
+        return resp.text.replace(
+            "kubernetesjsonschema.dev",
+            "raw.githubusercontent.com/yannh/kubernetes-json-schema/master",
+        )
+    return None
+
+
+def download_schemas_for_version(
+    version: str,
+    filenames: dict[str, tuple[str, str]],
+    token: str | None,
+    output_dir: Path,
+) -> tuple[int, int]:
+    """Download all schema files for a given K8s version. Returns (downloaded, 
skipped)."""
+    schema_dir = output_dir / f"v{version}-standalone-strict"
+    base_url = BASE_URL_TEMPLATE.format(version=version)
+    console.print(f"[bold]  Downloading {len(filenames)} schemas for 
v{version} to {schema_dir}[/]")
+    schema_dir.mkdir(parents=True, exist_ok=True)
+
+    downloaded = 0
+    skipped = 0
+    for fname in sorted(filenames):
+        api_version, kind = filenames[fname]
+        target = schema_dir / fname
+        console.print(f"    {api_version}/{kind} -> {fname}")
+        content = download_schema(base_url, fname, token)
+        if content is None:
+            skipped += 1
+            continue
+        # Validate it's proper JSON
+        json.loads(content)
+        target.write_text(content)
+        downloaded += 1
+
+    return downloaded, skipped
+
+
+def main() -> None:
+    import os
+
+    parser = argparse.ArgumentParser(description="Download K8s JSON schemas 
for helm chart tests.")
+    parser.add_argument(
+        "--output-dir",
+        type=Path,
+        required=True,
+        help="Directory to write schemas to (e.g. airflow-site/k8s-schemas).",
+    )
+    parser.add_argument(
+        "--versions",
+        nargs="+",
+        default=None,
+        help="Specific K8s versions to download (default: all 
ALLOWED_KUBERNETES_VERSIONS).",
+    )
+    args = parser.parse_args()
+
+    output_dir: Path = args.output_dir
+    versions: list[str] = args.versions if args.versions else 
KUBERNETES_VERSIONS
+
+    token = os.environ.get("GITHUB_TOKEN")
+    if not token:
+        # Try gh CLI
+        try:
+            result = subprocess.run(["gh", "auth", "token"], 
capture_output=True, text=True, check=False)
+            if result.returncode == 0 and result.stdout.strip():
+                token = result.stdout.strip()
+        except FileNotFoundError:
+            pass
+
+    if token:
+        console.print("[green]Using GitHub token for authenticated 
requests.[/]")
+    else:
+        console.print("[yellow]No GitHub token found. Using unauthenticated 
requests (60 req/hr limit).[/]")
+
+    console.print("[bold]Discovering (apiVersion, kind) pairs via helm 
template...[/]")
+    pairs = discover_pairs()
+    console.print(f"[green]Found {len(pairs)} unique (apiVersion, kind) 
pairs.[/]")
+
+    # Compute filenames
+    filenames: dict[str, tuple[str, str]] = {}
+    for api_version, kind in sorted(pairs):
+        fname = schema_filename(api_version, kind)
+        filenames[fname] = (api_version, kind)
+
+    total_downloaded = 0
+    total_skipped = 0
+    for version in versions:
+        downloaded, skipped = download_schemas_for_version(version, filenames, 
token, output_dir)
+        total_downloaded += downloaded
+        total_skipped += skipped
+
+    console.print(
+        f"[green]Done. Downloaded {total_downloaded} schemas across "
+        f"{len(versions)} versions, skipped {total_skipped}.[/]"
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/tools/setup_breeze b/scripts/tools/setup_breeze
index a277a9c2d6c..7f2d54ccea7 100755
--- a/scripts/tools/setup_breeze
+++ b/scripts/tools/setup_breeze
@@ -27,7 +27,7 @@ COLOR_YELLOW=$'\e[33m'
 COLOR_BLUE=$'\e[34m'
 COLOR_RESET=$'\e[0m'
 
-UV_VERSION="0.10.7"
+UV_VERSION="0.10.8"
 
 function manual_instructions() {
     echo

Reply via email to