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

bugraoz pushed a commit to branch backport-80911e8-v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit d0b77568e09e71bcde9b3e50f8ce778eb43d45e3
Author: Bugra Ozturk <[email protected]>
AuthorDate: Mon Oct 20 10:44:07 2025 +0200

    [v3-1-test] Implement integration test for airflowctl with 3.1 (#56124)
    
    * Implement initial integration test for airflowctl with 3.1
    
    * password can be passed without interaction, update integration tests
    
    * Add AIRFLOW_CLI_DEBUG_MODE for enhanced CLI debugging and update 
integration tests to skip keyring
    
    * Warn user while running each command if debug mode enabled and explicitly 
state it shouldn't be used unless debugging or integration tests
    
    * Move python-on-whales to devel-common, use shared docker-composer file, 
update documentation mistakes
    
    * remove shared python-on-whales from airflow-ctl-tests/
    
    Co-authored-by: Jarek Potiuk <[email protected]>
    
    * Decouple docker compose logic from test method to pytest_sessionstart in 
conftest
    
    * Move python_on_whale import to file level
    
    * Reorder dependencies in pyproject.toml for consistency
    
    * Add workspace to main pyproject.toml, remove unused variable, move 
console to singleton __init__.py
    
    * Add workspace to main pyproject.toml, remove unused variable, move 
console to singleton __init__.py
    
    ---------
    (cherry picked from commit 80911e833903315bb2afd4caa6069aad65103dc5)
    
    Co-authored-by: Bugra Ozturk <[email protected]>
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .dockerignore                                      |   1 +
 .github/workflows/additional-prod-image-tests.yml  |  41 ++++
 Dockerfile                                         |   1 +
 Dockerfile.ci                                      |   1 +
 .../pyproject.toml                                 |   7 +-
 .../tests/airflowctl_tests/__init__.py             |  24 ++
 .../tests/airflowctl_tests/conftest.py             | 253 +++++++++++++++++++++
 .../tests/airflowctl_tests/constants.py            |  32 +++
 .../airflowctl_tests/test_airflowctl_commands.py   |  73 ++++++
 airflow-ctl/docs/cli-and-env-variables-ref.rst     |   7 +
 airflow-ctl/src/airflowctl/api/client.py           |  25 +-
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |   9 +-
 .../airflow_ctl/ctl/commands/test_auth_command.py  |   6 +-
 airflow-e2e-tests/pyproject.toml                   |   0
 dev/breeze/doc/05_test_commands.rst                |  21 +-
 .../output_setup_check-all-params-in-groups.svg    |  35 +--
 ...utput_testing_airflow-ctl-integration-tests.svg | 144 ++++++++++++
 ...utput_testing_airflow-ctl-integration-tests.txt |   1 +
 .../airflow_breeze/commands/testing_commands.py    |  56 +++++
 .../commands/testing_commands_config.py            |  15 +-
 dev/breeze/src/airflow_breeze/global_constants.py  |   2 +
 dev/breeze/src/airflow_breeze/utils/run_tests.py   |   5 +
 devel-common/pyproject.toml                        |   1 +
 docker-tests/pyproject.toml                        |   1 -
 kubernetes-tests/pyproject.toml                    |   1 -
 pyproject.toml                                     |   4 +
 .../docker/install_airflow_when_building_images.sh |   1 +
 task-sdk-tests/pyproject.toml                      |   1 -
 28 files changed, 735 insertions(+), 33 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index edce6e9e78a..c7fb2e60c7b 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -47,6 +47,7 @@
 !helm-tests
 !kubernetes-tests
 !task-sdk-tests
+!airflow-ctl-tests
 !shared/
 
 # Add scripts so that we can use them inside the container
diff --git a/.github/workflows/additional-prod-image-tests.yml 
b/.github/workflows/additional-prod-image-tests.yml
index 714761bceb3..f87ace8b917 100644
--- a/.github/workflows/additional-prod-image-tests.yml
+++ b/.github/workflows/additional-prod-image-tests.yml
@@ -191,3 +191,44 @@ jobs:
         id: breeze
       - name: "Run Task SDK integration tests"
         run: breeze testing task-sdk-integration-tests
+
+  test-e2e-integration-tests-basic:
+    name: "Test e2e integration tests with PROD image"
+    uses: ./.github/workflows/airflow-e2e-tests.yml
+    with:
+      workflow-name: "Regular e2e test"
+      runners: ${{ inputs.runners }}
+      platform: ${{ inputs.platform }}
+      default-python-version: "${{ inputs.default-python-version }}"
+      use-uv: ${{ inputs.use-uv }}
+
+  airflow-ctl-integration-tests:
+    timeout-minutes: 60
+    name: "Airflow CTL integration tests with PROD image"
+    runs-on: ${{ fromJSON(inputs.runners) }}
+    env:
+      PYTHON_MAJOR_MINOR_VERSION: "${{ inputs.default-python-version }}"
+      GITHUB_REPOSITORY: ${{ github.repository }}
+      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      GITHUB_USERNAME: ${{ github.actor }}
+      VERBOSE: "true"
+    steps:
+      - name: "Cleanup repo"
+        shell: bash
+        run: docker run -v "${GITHUB_WORKSPACE}:/workspace" -u 0:0 bash -c "rm 
-rf /workspace/*"
+      - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # 
v4.2.2
+        with:
+          fetch-depth: 2
+          persist-credentials: false
+      - name: "Prepare breeze & PROD image: ${{ env.PYTHON_MAJOR_MINOR_VERSION 
}}"
+        uses: ./.github/actions/prepare_breeze_and_image
+        with:
+          platform: ${{ inputs.platform }}
+          image-type: "prod"
+          python: ${{ env.PYTHON_MAJOR_MINOR_VERSION }}
+          use-uv: ${{ inputs.use-uv }}
+          make-mnt-writeable-and-cleanup: true
+        id: breeze
+      - name: "Run airflowctl integration tests"
+        run: breeze testing airflow-ctl-integration-tests
diff --git a/Dockerfile b/Dockerfile
index 0f7f5007b3a..8fab2a4ae01 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1116,6 +1116,7 @@ function install_from_sources() {
               --editable ./airflow-core --editable ./task-sdk --editable 
./airflow-ctl \
               --editable ./kubernetes-tests --editable ./docker-tests 
--editable ./helm-tests \
               --editable ./task-sdk-tests \
+              --editable ./airflow-ctl-tests \
               --editable ./devel-common[all] --editable ./dev \
               --group dev --group docs --group docs-gen --group leveldb"
         local -a projects_with_devel_dependencies
diff --git a/Dockerfile.ci b/Dockerfile.ci
index 74c7692031d..744fe240a10 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -870,6 +870,7 @@ function install_from_sources() {
               --editable ./airflow-core --editable ./task-sdk --editable 
./airflow-ctl \
               --editable ./kubernetes-tests --editable ./docker-tests 
--editable ./helm-tests \
               --editable ./task-sdk-tests \
+              --editable ./airflow-ctl-tests \
               --editable ./devel-common[all] --editable ./dev \
               --group dev --group docs --group docs-gen --group leveldb"
         local -a projects_with_devel_dependencies
diff --git a/task-sdk-tests/pyproject.toml b/airflow-ctl-tests/pyproject.toml
similarity index 92%
copy from task-sdk-tests/pyproject.toml
copy to airflow-ctl-tests/pyproject.toml
index 5e6ac78373c..d401975ad73 100644
--- a/task-sdk-tests/pyproject.toml
+++ b/airflow-ctl-tests/pyproject.toml
@@ -21,8 +21,8 @@ requires = [ "hatchling==1.27.0" ]
 build-backend = "hatchling.build"
 
 [project]
-name = "apache-airflow-task-sdk-tests"
-description = "Task SDK tests for Apache Airflow"
+name = "apache-airflow-ctl-tests"
+description = "Airflow CTL tests for Apache Airflow"
 classifiers = [
     "Private :: Do Not Upload",
 ]
@@ -36,9 +36,8 @@ maintainers = [
 version = "0.0.1"
 
 dependencies = [
-    "apache-airflow-core",
+    "apache-airflow-ctl",
     "apache-airflow-devel-common",
-    "python-on-whales>=0.70.0",
 ]
 
 [tool.pytest.ini_options]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/__init__.py 
b/airflow-ctl-tests/tests/airflowctl_tests/__init__.py
new file mode 100644
index 00000000000..973b8fdce34
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/__init__.py
@@ -0,0 +1,24 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from rich.console import Console
+
+console = Console(width=400, color_system="standard")
+
+
+__all__ = ["console"]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py 
b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
new file mode 100644
index 00000000000..720403e3c64
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -0,0 +1,253 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+import subprocess
+import sys
+
+import pytest
+from python_on_whales import DockerClient, docker
+
+from airflowctl_tests import console
+from airflowctl_tests.constants import (
+    AIRFLOW_ROOT_PATH,
+    DOCKER_COMPOSE_FILE_PATH,
+    DOCKER_IMAGE,
+)
+
+docker_client = None
+
+
+# Pytest hook to run at the start of the session
+def pytest_sessionstart(session):
+    """Install airflowctl at the very start of the pytest session."""
+    airflow_ctl_version = os.environ.get("AIRFLOW_CTL_VERSION", "1.0.0")
+    console.print(f"[yellow]Installing 
apache-airflow-ctl=={airflow_ctl_version} via pytest_sessionstart...")
+
+    airflow_ctl_path = AIRFLOW_ROOT_PATH / "airflow-ctl"
+    console.print(f"[blue]Installing from: {airflow_ctl_path}")
+
+    # Install directly to current UV environment
+    console.print("[blue]Installing to current UV environment...")
+    console.print(f"[blue]Current Python: {sys.executable}")
+
+    try:
+        cmd = ["uv", "pip", "install", str(airflow_ctl_path)]
+        console.print(f"[cyan]Running command: {' '.join(cmd)}")
+        subprocess.check_call(cmd)
+        console.print("[green]airflowctl installed successfully to UV 
environment via pytest_sessionstart!")
+    except (subprocess.CalledProcessError, FileNotFoundError) as e:
+        console.print(f"[yellow]UV installation failed: {e}")
+        raise
+
+    console.print("[yellow]Verifying airflowctl installation via 
pytest_sessionstart...")
+    try:
+        result = subprocess.run(
+            [
+                sys.executable,
+                "-c",
+                "import airflowctl.api.client; print('✅ airflowctl import 
successful via pytest_sessionstart')",
+            ],
+            capture_output=True,
+            text=True,
+            check=True,
+        )
+        console.print(f"[green]{result.stdout.strip()}")
+    except subprocess.CalledProcessError as e:
+        console.print("[red]❌ airflowctl import verification failed via 
pytest_sessionstart:")
+        console.print(f"[red]Return code: {e.returncode}")
+        console.print(f"[red]Stdout: {e.stdout}")
+        console.print(f"[red]Stderr: {e.stderr}")
+        raise
+
+    docker_compose_up(session.config._tmp_path_factory)
+
+
+def print_diagnostics(compose, compose_version, docker_version):
+    """Print diagnostic information when test fails."""
+    console.print("[red]=== DIAGNOSTIC INFORMATION ===[/]")
+    console.print(f"Docker version: {docker_version}")
+    console.print(f"Docker Compose version: {compose_version}")
+    console.print("\n[yellow]Container Status:[/]")
+    try:
+        containers = compose.compose.ps()
+        for container in containers:
+            console.print(f"  {container.name}: {container.state}")
+    except Exception as e:
+        console.print(f"  Error getting container status: {e}")
+
+    console.print("\n[yellow]Container Logs:[/]")
+    try:
+        logs = compose.compose.logs()
+        console.print(logs)
+    except Exception as e:
+        console.print(f"  Error getting logs: {e}")
+
+
+def debug_environment():
+    """Debug the Python environment setup in CI."""
+    import os
+    import subprocess
+    import sys
+    from pathlib import Path
+
+    console.print("[yellow]===== CI ENVIRONMENT DEBUG =====")
+    console.print(f"[blue]Python executable: {sys.executable}")
+    console.print(f"[blue]Python version: {sys.version}")
+    console.print(f"[blue]Working directory: {os.getcwd()}")
+    console.print(f"[blue]VIRTUAL_ENV: {os.environ.get('VIRTUAL_ENV', 'Not 
set')}")
+    console.print(f"[blue]PYTHONPATH: {os.environ.get('PYTHONPATH', 'Not 
set')}")
+
+    console.print(f"[blue]Python executable exists: 
{Path(sys.executable).exists()}")
+    if Path(sys.executable).is_symlink():
+        console.print(f"[blue]Python executable is symlink to: 
{Path(sys.executable).readlink()}")
+
+    try:
+        uv_python = subprocess.check_output(["uv", "python", "find"], 
text=True).strip()
+        console.print(f"[cyan]UV Python: {uv_python}")
+        console.print(f"[green]Match: {uv_python == sys.executable}")
+
+        console.print(f"[cyan]UV Python exists: {Path(uv_python).exists()}")
+        if Path(uv_python).is_symlink():
+            console.print(f"[cyan]UV Python is symlink to: 
{Path(uv_python).readlink()}")
+    except Exception as e:
+        console.print(f"[red]UV Python error: {e}")
+
+    # Check what's installed in current environment
+    try:
+        import airflowctl
+
+        console.print(f"[green]✅ airflow already available: 
{airflowctl.__file__}")
+    except ImportError:
+        console.print("[red]❌ airflowctl not available in current environment")
+
+    console.print("[yellow]================================")
+
+
+def docker_compose_up(tmp_path_factory):
+    """Fixture to spin up Docker Compose environment for the test session."""
+    from shutil import copyfile
+
+    global docker_client
+
+    tmp_dir = tmp_path_factory.mktemp("airflow-ctl-test")
+    console.print(f"[yellow]Tests are run in {tmp_dir}")
+
+    # Copy docker-compose.yaml to temp directory
+    tmp_docker_compose_file = tmp_dir / "docker-compose.yaml"
+    copyfile(DOCKER_COMPOSE_FILE_PATH, tmp_docker_compose_file)
+
+    dot_env_file = tmp_dir / ".env"
+    dot_env_file.write_text(
+        f"AIRFLOW_UID={os.getuid()}\n"
+        # To enable debug mode for airflowctl CLI
+        "AIRFLOW_CTL_CLI_DEBUG_MODE=true\n"
+        # To enable config operations to work
+        "AIRFLOW__API__EXPOSE_CONFIG=true\n"
+    )
+
+    # Set environment variables for the test
+    os.environ["AIRFLOW_IMAGE_NAME"] = DOCKER_IMAGE
+    os.environ["AIRFLOW_CTL_VERSION"] = os.environ.get("AIRFLOW_CTL_VERSION", 
"1.0.0")
+    os.environ["ENV_FILE_PATH"] = str(tmp_dir / ".env")
+
+    # Initialize Docker client
+    docker_client = DockerClient(compose_files=[str(tmp_docker_compose_file)])
+
+    try:
+        console.print(f"[blue]Spinning up airflow environment using 
{DOCKER_IMAGE}")
+        docker_client.compose.up(detach=True, wait=True)
+        console.print("[green]Docker compose started for airflowctl test\n")
+    except Exception:
+        print_diagnostics(docker_client.compose, 
docker_client.compose.version(), docker.version())
+        debug_environment()
+        docker_compose_down()
+        raise
+
+
+def docker_compose_down():
+    """Tear down Docker Compose environment."""
+    global docker_client
+    if docker_client:
+        docker_client.compose.down(remove_orphans=True, volumes=True, 
quiet=True)
+
+
+def pytest_sessionfinish(session, exitstatus):
+    """Tear down test environment at the end of the pytest session."""
+    if not os.environ.get("SKIP_DOCKER_COMPOSE_DELETION"):
+        docker_compose_down()
+
+
+# Fixtures for tests
[email protected]
+def login_command():
+    # Passing password via command line is insecure but acceptable for testing 
purposes
+    # Please do not do this in production, it enables possibility of exposing 
your credentials
+    return "auth login --username airflow --password airflow"
+
+
[email protected]
+def login_output():
+    return "Login successful! Welcome to airflowctl!"
+
+
[email protected]
+def date_param():
+    import random
+    from datetime import datetime, timedelta
+
+    from dateutil.relativedelta import relativedelta
+
+    # original datetime string
+    dt_str = "2025-10-25T00:02:00+00:00"
+
+    # parse to datetime object
+    dt = datetime.fromisoformat(dt_str)
+
+    # boundaries
+    start = dt - relativedelta(months=1)
+    end = dt + relativedelta(months=1)
+
+    # pick random time between start and end
+    delta = end - start
+    random_seconds = random.randint(0, int(delta.total_seconds()))
+    random_dt = start + timedelta(seconds=random_seconds)
+    return random_dt.isoformat()
+
+
[email protected]
+def test_commands(login_command, date_param):
+    # Define test commands to run with actual running API server
+    return [
+        login_command,
+        "backfill list",
+        "config get --section core --option executor",
+        "connections create --connection-id=test_con --conn-type=mysql 
--password=TEST_PASS -o json",
+        "connections list",
+        "connections list -o yaml",
+        "connections list -o tabledags list",
+        f"dagrun trigger --dag-id=example_bash_operator 
--logical-date={date_param} --run-after={date_param}",
+        "dagrun list --dag-id example_bash_operator --state success --limit=1",
+        "jobs list",
+        "pools create --name=test_pool --slots=5",
+        "pools list",
+        "providers list",
+        "variables create --key=test_key --value=test_value",
+        "variables list",
+        "version --remote",
+    ]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/constants.py 
b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
new file mode 100644
index 00000000000..549a9ad8c74
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+AIRFLOW_ROOT_PATH = Path(__file__).resolve().parents[3]
+
+DEFAULT_PYTHON_MAJOR_MINOR_VERSION = "3.10"
+DEFAULT_DOCKER_IMAGE = 
f"ghcr.io/apache/airflow/main/prod/python{DEFAULT_PYTHON_MAJOR_MINOR_VERSION}:latest"
+DOCKER_IMAGE = os.environ.get("DOCKER_IMAGE") or DEFAULT_DOCKER_IMAGE
+
+DOCKER_COMPOSE_HOST_PORT = os.environ.get("HOST_PORT", "localhost:8080")
+
+DOCKER_COMPOSE_FILE_PATH = (
+    AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "howto" / "docker-compose" / 
"docker-compose.yaml"
+)
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
new file mode 100644
index 00000000000..f41b2c1424f
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -0,0 +1,73 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import os
+from subprocess import PIPE, STDOUT, Popen
+
+from airflowctl_tests import console
+
+
+def test_airflowctl_commands(login_command, login_output, test_commands):
+    """Test airflowctl commands using docker-compose environment."""
+    host_envs = os.environ.copy()
+    host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
+    # Testing commands of airflowctl
+    for command in test_commands:
+        command_from_config = f"airflowctl {command}"
+        # We need to run auth login first for all commands except login itself
+        if command != login_command:
+            run_command = f"airflowctl {login_command} && 
{command_from_config}"
+        else:
+            run_command = command_from_config
+        console.print(f"[yellow]Running command: {command}")
+
+        # Give some time for the command to execute and output to be ready
+        proc = Popen(run_command.encode(), stdout=PIPE, stderr=STDOUT, 
stdin=PIPE, shell=True, env=host_envs)
+        stdout_result, stderr_result = proc.communicate(timeout=60)
+
+        # CLI command gave errors
+        if stderr_result:
+            console.print(
+                f"[red]Errors while executing command 
'{command_from_config}':\n{stderr_result.decode()}"
+            )
+
+        # Decode the output
+        stdout_result = stdout_result.decode()
+        # We need to trim auth login output if the command is not login itself 
and clean backspaces
+        if command != login_command:
+            if login_output not in stdout_result:
+                console.print(
+                    f"[red]❌ Login output not found before command output for 
'{command_from_config}'"
+                )
+                console.print(f"[red]Full output:\n{stdout_result}\n")
+                raise AssertionError("Login output not found before command 
output")
+            stdout_result = stdout_result.split(f"{login_output}\n")[1].strip()
+        else:
+            stdout_result = stdout_result.strip()
+
+        # This is a common error message that is thrown by client when 
something is wrong
+        # Please ensure it is aligning with 
airflowctl.api.client.get_json_error
+        airflowctl_client_server_response_error = "Server error"
+        if airflowctl_client_server_response_error in stdout_result:
+            console.print(f"[red]❌ Output contained unexpected text for 
command '{command_from_config}'")
+            console.print(f"[red]Did not expect to 
find:\n{airflowctl_client_server_response_error}\n")
+            console.print(f"[red]But got:\n{stdout_result}\n")
+            raise AssertionError(f"Output contained unexpected 
text\nOutput:\n{stdout_result}")
+        console.print(f"[green]✅ Output did not contain unexpected text for 
command '{command_from_config}'")
+        console.print(f"[cyan]Result:\n{stdout_result}\n")
+        proc.kill()
diff --git a/airflow-ctl/docs/cli-and-env-variables-ref.rst 
b/airflow-ctl/docs/cli-and-env-variables-ref.rst
index aebe2c56579..2d63d47d5be 100644
--- a/airflow-ctl/docs/cli-and-env-variables-ref.rst
+++ b/airflow-ctl/docs/cli-and-env-variables-ref.rst
@@ -53,3 +53,10 @@ Environment Variables
     required if you have multiple environments set up and want to
     specify which one to use. If not set, the default environment
     will be used which is production.
+
+.. envvar:: AIRFLOW_CLI_DEBUG_MODE
+
+    This variable can be used to enable debug mode for the CLI.
+    It disables some features such as keyring integration and save credentials 
to file.
+    It is only meant to use if either you are developing airflowctl or running 
API integration tests.
+    Please do not use this variable unless you know what you are doing.
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index 4f2ee2fbd8d..09947833baf 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -94,6 +94,8 @@ def get_json_error(response: httpx.Response):
     """Raise a ServerResponseError if we can extract error info from the 
error."""
     err = ServerResponseError.from_response(response)
     if err:
+        # This part is used in integration tests to verify the error message
+        # If you are updating here don't forget to update the airflow-ctl-tests
         log.warning("Server error ", extra=dict(err.response.json()))
         raise err
 
@@ -133,8 +135,15 @@ class Credentials:
         os.makedirs(default_config_dir, exist_ok=True)
         with open(os.path.join(default_config_dir, 
self.input_cli_config_file), "w") as f:
             json.dump({"api_url": self.api_url}, f)
+
         try:
-            keyring.set_password("airflowctl", 
f"api_token-{self.api_environment}", self.api_token)
+            if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true":
+                with open(
+                    os.path.join(default_config_dir, 
f"debug_creds_{self.input_cli_config_file}"), "w"
+                ) as f:
+                    json.dump({f"api_token_{self.api_environment}": 
self.api_token}, f)
+            else:
+                keyring.set_password("airflowctl", 
f"api_token_{self.api_environment}", self.api_token)
         except NoKeyringError as e:
             log.error(e)
         except TypeError as e:
@@ -145,12 +154,20 @@ class Credentials:
     def load(self) -> Credentials:
         """Load the credentials from keyring and URL from disk file."""
         default_config_dir = os.environ.get("AIRFLOW_HOME", 
os.path.expanduser("~/airflow"))
-        credential_path = os.path.join(default_config_dir, 
self.input_cli_config_file)
+        config_path = os.path.join(default_config_dir, 
self.input_cli_config_file)
         try:
-            with open(credential_path) as f:
+            with open(config_path) as f:
                 credentials = json.load(f)
                 self.api_url = credentials["api_url"]
-                self.api_token = keyring.get_password("airflowctl", 
f"api_token-{self.api_environment}")
+                if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true":
+                    debug_creds_path = os.path.join(
+                        default_config_dir, 
f"debug_creds_{self.input_cli_config_file}"
+                    )
+                    with open(debug_creds_path) as df:
+                        debug_credentials = json.load(df)
+                        self.api_token = 
debug_credentials.get(f"api_token_{self.api_environment}")
+                else:
+                    self.api_token = keyring.get_password("airflowctl", 
f"api_token_{self.api_environment}")
         except FileNotFoundError:
             if self.client_kind == ClientKind.AUTH:
                 # Saving the URL set from the Auth Commands if Kind is AUTH
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 085fe5abec4..fb6a7dfde0d 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -66,6 +66,12 @@ def lazy_load_command(import_path: str) -> Callable:
 def safe_call_command(function: Callable, args: Iterable[Arg]) -> None:
     import sys
 
+    if os.getenv("AIRFLOW_CLI_DEBUG_MODE") == "true":
+        rich.print(
+            "[yellow]Debug mode is enabled. Please be aware that your 
credentials are not secure.\n"
+            "Please unset AIRFLOW_CLI_DEBUG_MODE or set it to false.[/yellow]"
+        )
+
     try:
         function(args)
     except AirflowCtlCredentialNotFoundException as e:
@@ -187,7 +193,8 @@ class Password(argparse.Action):
     """Custom action to prompt for password input."""
 
     def __call__(self, parser, namespace, values, option_string=None):
-        values = getpass.getpass()
+        if values is None:
+            values = getpass.getpass()
         setattr(namespace, self.dest, values)
 
 
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
index c5285710d58..0ad428ee5ba 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_auth_command.py
@@ -67,7 +67,7 @@ class TestCliAuthCommands:
                 assert json.load(f) == {"api_url": "http://localhost:8080"}
 
             mock_keyring.set_password.assert_called_once_with(
-                "airflowctl", "api_token-TEST_AUTH_LOGIN", "TEST_TOKEN"
+                "airflowctl", "api_token_TEST_AUTH_LOGIN", "TEST_TOKEN"
             )
 
     # Test auth login with username and password
@@ -102,7 +102,7 @@ class TestCliAuthCommands:
             )
             mock_keyring.set_password.assert_has_calls(
                 [
-                    mock.call("airflowctl", "api_token-production", ""),
-                    mock.call("airflowctl", "api_token-production", 
"TEST_TOKEN"),
+                    mock.call("airflowctl", "api_token_production", ""),
+                    mock.call("airflowctl", "api_token_production", 
"TEST_TOKEN"),
                 ]
             )
diff --git a/airflow-e2e-tests/pyproject.toml b/airflow-e2e-tests/pyproject.toml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/dev/breeze/doc/05_test_commands.rst 
b/dev/breeze/doc/05_test_commands.rst
index adb20969803..6d46779f749 100644
--- a/dev/breeze/doc/05_test_commands.rst
+++ b/dev/breeze/doc/05_test_commands.rst
@@ -197,6 +197,25 @@ Here is the detailed set of options for the ``breeze 
testing airflow-ctl-tests``
   :width: 100%
   :alt: Breeze testing airflow-ctl-tests
 
+Running airflowctl integration tests
+..................................
+
+You can use Breeze to run the airflowctl integration tests. Those tests are 
run using Production image by default
+and the tests are running with the docker-compose we have for airflowctl tests.
+
+.. image:: ./images/output_testing_airflow-ctl-integration-tests.svg
+  :target: 
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_testing_airflow-ctl-integration-tests.svg
+  :width: 100%
+  :alt: Breeze testing airflow-ctl-integration-tests
+
+You can also iterate over those tests with pytest command but unlike regular 
unit tests, they need to be run in
+a local venv. You can build the prod image with breeze and that will be used 
by default if present to run the tests.
+
+You can override the ``DOCKER_IMAGE`` environment variable to point to the 
image to test using the
+``breeze testing airflow-ctl-integration-tests`` command.
+
+The airflowctl tests are in ``airflow-ctl-tests/`` folder in the main repo.
+
 Running integration core tests
 ...............................
 
@@ -323,7 +342,7 @@ Running task-sdk integration tests
 You can use Breeze to run the task sdk integration tests. Those tests are run 
using Production image by default
 and the tests are running with the docker-compose we have for task-sdk tests.
 
-.. image:: ./images/output_testing_docker-compose-tests.svg
+.. image:: ./images/output_testing_task-sdk-integration-tests.svg
   :target: 
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_testing_task-sdk-integration-tests.svg
   :width: 100%
   :alt: Breeze testing task-sdk-integration-tests
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg 
b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index 53d05594312..f1af776fa4a 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1123.6" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 1148.0" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-setup-check-all-params-in-groups-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1072.6" />
+      <rect x="0" y="0" width="1463.0" height="1097.0" />
     </clipPath>
     <clipPath id="breeze-setup-check-all-params-in-groups-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -174,17 +174,20 @@
 <clipPath id="breeze-setup-check-all-params-in-groups-line-42">
     <rect x="0" y="1026.3" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-setup-check-all-params-in-groups-line-43">
+    <rect x="0" y="1050.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="1121.6" rx="8"/><text 
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;setup&#160;check-all-params-in-groups</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1146" rx="8"/><text 
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;setup&#160;check-all-params-in-groups</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
-        
+
     <g transform="translate(9, 41)" 
clip-path="url(#breeze-setup-check-all-params-in-groups-clip-terminal)">
-    
+
     <g class="breeze-setup-check-all-params-in-groups-matrix">
     <text class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="20" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-0)">
 </text><text class="breeze-setup-check-all-params-in-groups-r2" x="12.2" 
y="44.4" textLength="73.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-1)">Usage:</text><text
 class="breeze-setup-check-all-params-in-groups-r3" x="97.6" y="44.4" 
textLength="475.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-1)">breeze&#160;setup&#160;check-all-params-in-groups</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="585.6" y="44.4" 
textLength="12.2" clip- [...]
@@ -219,17 +222,17 @@
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="752" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">sbom:export-dependency-information&#160;|&#160;sbom:generate-providers-requirements&#160;|&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="776.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="776.4" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">sbom:update-sbom-information&#160;|&#160;setup&#160;|&#160;setup:autocomplete&#160;|&#160;setup:check-all-params-in-groups&#160;|&#160;&#160;&#160;&#160;&#160;
 [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="800.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="800.8" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">setup:config&#160;|&#160;setup:regenerate-command-images&#160;|&#160;setup:self-upgrade&#160;|&#160;setup:synchronize-local-mounts&#160;|&#160;</text><text
 cla [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="825.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="825.2" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;testing:airflow-ctl-tests&#160;|&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="849.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="849.6" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">testing:core-integration-tests&#160;|&#160;testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests</text><text
 class="breeze [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="874" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">|&#160;testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;&#160;&#160;&#160;</text><te
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="898.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="898.4" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;workflow-run&#160;|&#160;&#160;&#160;&#160;<
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="922.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="922.8" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">workflow-run:publish-docs)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="947.2" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="947.2" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="971.6" textLength="24.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">╭─</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="971.6" 
textLength="195.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">&#160;Common&#160;options&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="971.6" 
textLength="1220" clip-path="url(#breeze-se [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="996" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">--verbose</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="996" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params-in-group [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1020.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1020.4" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">--dry-run</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1020.4" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1044.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1044.8" 
textLength="73.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--help</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params-in- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1069.2" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1069.2" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="825.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="825.2" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;testing:airflow-ctl-integration-tests&#160;|&#160;&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="874" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests&#160;|&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="898.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="898.4" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;&#160;&#160;&#160;&#160;&#160;<
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="922.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="922.8" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;workflow-run&#160;|&#160;&#160;&#160;&#160;<
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="947.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="183" y="947.2" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">workflow-run:publish-docs)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="971.6" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="971.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996" 
textLength="24.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">╭─</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="996" 
textLength="195.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">&#160;Common&#160;options&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="996" 
textLength="1220" clip-path="url(#breeze-setup-ch [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1020.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1020.4" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">--verbose</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1020.4" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1044.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1044.8" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--dry-run</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1069.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1069.2" 
textLength="73.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">--help</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1069.2" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params-in- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1093.6" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1093.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">
 </text>
     </g>
     </g>
diff --git 
a/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.svg 
b/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.svg
new file mode 100644
index 00000000000..e6630729158
--- /dev/null
+++ b/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.svg
@@ -0,0 +1,144 @@
+<svg class="rich-terminal" viewBox="0 0 1482 562.4" 
xmlns="http://www.w3.org/2000/svg";>
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff";)
 format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff";)
 format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .breeze-testing-airflow-ctl-integration-tests-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .breeze-testing-airflow-ctl-integration-tests-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .breeze-testing-airflow-ctl-integration-tests-r1 { fill: #c5c8c6 }
+.breeze-testing-airflow-ctl-integration-tests-r2 { fill: #d0b344 }
+.breeze-testing-airflow-ctl-integration-tests-r3 { fill: #c5c8c6;font-weight: 
bold }
+.breeze-testing-airflow-ctl-integration-tests-r4 { fill: #68a0b3;font-weight: 
bold }
+.breeze-testing-airflow-ctl-integration-tests-r5 { fill: #868887 }
+.breeze-testing-airflow-ctl-integration-tests-r6 { fill: #98a84b;font-weight: 
bold }
+.breeze-testing-airflow-ctl-integration-tests-r7 { fill: #8d7b39 }
+    </style>
+
+    <defs>
+    <clipPath id="breeze-testing-airflow-ctl-integration-tests-clip-terminal">
+      <rect x="0" y="0" width="1463.0" height="511.4" />
+    </clipPath>
+    <clipPath id="breeze-testing-airflow-ctl-integration-tests-line-0">
+    <rect x="0" y="1.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-1">
+    <rect x="0" y="25.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-2">
+    <rect x="0" y="50.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-3">
+    <rect x="0" y="74.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-4">
+    <rect x="0" y="99.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-5">
+    <rect x="0" y="123.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-6">
+    <rect x="0" y="147.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-7">
+    <rect x="0" y="172.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-8">
+    <rect x="0" y="196.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-9">
+    <rect x="0" y="221.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-11">
+    <rect x="0" y="269.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-12">
+    <rect x="0" y="294.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-13">
+    <rect x="0" y="318.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-14">
+    <rect x="0" y="343.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-15">
+    <rect x="0" y="367.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-16">
+    <rect x="0" y="391.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-17">
+    <rect x="0" y="416.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-18">
+    <rect x="0" y="440.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-testing-airflow-ctl-integration-tests-line-19">
+    <rect x="0" y="465.1" 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="560.4" rx="8"/><text 
class="breeze-testing-airflow-ctl-integration-tests-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;testing&#160;airflow-ctl-integration-tests</text>
+            <g transform="translate(26,22)">
+            <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+            <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+            <circle cx="44" cy="0" r="7" fill="#28c840"/>
+            </g>
+        
+    <g transform="translate(9, 41)" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-clip-terminal)">
+    
+    <g class="breeze-testing-airflow-ctl-integration-tests-matrix">
+    <text class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" 
y="20" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-0)">
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r2" x="12.2" 
y="44.4" textLength="73.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-1)">Usage:</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r3" x="97.6" y="44.4" 
textLength="536.8" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-1)">breeze&#160;testing&#160;airflow-ctl-integration-tests</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="646.6" y= [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" 
y="68.8" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-2)">
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r1" x="12.2" 
y="93.2" textLength="402.6" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-3)">Run&#160;airflowctl&#160;integration&#160;tests.</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" y="93.2" 
textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-3)">
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" 
y="117.6" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-4)">
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="142" textLength="24.4" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-5)">╭─</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="24.4" y="142" 
textLength="329.4" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-5)">&#160;Docker-compose&#160;tests&#160;flag&#160;</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="353.8" y="142" 
textLength [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="166.4" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-6)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="166.4" 
textLength="146.4" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-6)">--image-name</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="414.8" y="166.4" 
textLength="24.4" clip-path="url(#breeze [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="190.8" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-7)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="190.8" 
textLength="97.6" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-7)">--python</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="414.8" y="190.8" 
textLength="24.4" clip-path="url(#breeze-test [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="215.2" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-8)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r7" x="463.6" y="215.2" 
textLength="732" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-8)">(&gt;3.10&lt;&#160;|&#160;3.11&#160;|&#160;3.12&#160;|&#160;3.13)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160
 [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="239.6" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-9)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="463.6" y="239.6" 
textLength="732" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-9)">[default:&#160;3.10]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#
 [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="264" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-10)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="264" 
textLength="366" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-10)">--skip-docker-compose-deletion</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="463.6" y="264" 
textLength="671" clip-path=" [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="288.4" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-11)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="288.4" 
textLength="305" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-11)">--include-success-outputs</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="463.6" y="288.4" 
textLength="841.8" clip-pat [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="312.8" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-12)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="312.8" 
textLength="231.8" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-12)">--github-repository</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="414.8" y="312.8" 
textLength="24.4" clip-path="ur [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="337.2" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-13)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="463.6" y="337.2" 
textLength="585.6" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-13)">[default:&#160;apache/airflow]&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="361.6" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-14)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="361.6" 
textLength="256.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-14)">--airflow-ctl-version</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="463.6" y="361.6" 
textLength="353.8" clip-path= [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="386" textLength="1464" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-15)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" y="386" 
textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-15)">
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="410.4" textLength="24.4" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-16)">╭─</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="24.4" y="410.4" 
textLength="195.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-16)">&#160;Common&#160;options&#160;</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r5" x="219.6" y="410.4" 
textLength="1220"  [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="434.8" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-17)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="434.8" 
textLength="109.8" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-17)">--verbose</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="158.6" y="434.8" 
textLength="24.4" clip-path="url(#breeze- [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="459.2" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-18)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="459.2" 
textLength="109.8" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-18)">--dry-run</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="158.6" y="459.2" 
textLength="24.4" clip-path="url(#breeze- [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="483.6" textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-19)">│</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r4" x="24.4" y="483.6" 
textLength="73.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-19)">--help</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r6" x="158.6" y="483.6" 
textLength="24.4" clip-path="url(#breeze-test [...]
+</text><text class="breeze-testing-airflow-ctl-integration-tests-r5" x="0" 
y="508" textLength="1464" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-20)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-testing-airflow-ctl-integration-tests-r1" x="1464" y="508" 
textLength="12.2" 
clip-path="url(#breeze-testing-airflow-ctl-integration-tests-line-20)">
+</text>
+    </g>
+    </g>
+</svg>
diff --git 
a/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.txt 
b/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.txt
new file mode 100644
index 00000000000..9d92f133d1c
--- /dev/null
+++ b/dev/breeze/doc/images/output_testing_airflow-ctl-integration-tests.txt
@@ -0,0 +1 @@
+fdff2a26a3d0f8aee8b54ad4309e88d4
diff --git a/dev/breeze/src/airflow_breeze/commands/testing_commands.py 
b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
index c0334b1fa88..ff078f60d20 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands.py
@@ -831,6 +831,62 @@ def task_sdk_integration_tests(
     sys.exit(return_code)
 
 
+@group_for_testing.command(
+    name="airflow-ctl-integration-tests",
+    context_settings=dict(
+        ignore_unknown_options=True,
+        allow_extra_args=True,
+    ),
+)
+@option_python
+@option_image_name
+@option_skip_docker_compose_deletion
+@option_github_repository
+@option_include_success_outputs
+@option_verbose
+@option_dry_run
[email protected](
+    "--airflow-ctl-version",
+    help="Version of airflowctl to test",
+    default="1.0.0",
+    show_default=True,
+    envvar="AIRFLOW_CTL_VERSION",
+)
[email protected]("extra_pytest_args", nargs=-1, type=click.Path(path_type=str))
+def airflowctl_integration_tests(
+    python: str,
+    image_name: str | None,
+    skip_docker_compose_deletion: bool,
+    github_repository: str,
+    include_success_outputs: bool,
+    airflow_ctl_version: str,
+    extra_pytest_args: tuple,
+):
+    """Run airflowctl integration tests."""
+    # Export the AIRFLOW_CTL_VERSION environment variable for the test
+    import os
+
+    perform_environment_checks()
+
+    os.environ["AIRFLOW_CTL_VERSION"] = airflow_ctl_version
+    image_name = image_name or os.environ.get("DOCKER_IMAGE")
+
+    if image_name is None:
+        build_params = BuildProdParams(python=python, 
github_repository=github_repository)
+        image_name = build_params.airflow_image_name
+
+    get_console().print(f"[info]Running airflowctl integration tests with PROD 
image: {image_name}[/]")
+    get_console().print(f"[info]Using airflowctl version: 
{airflow_ctl_version}[/]")
+    return_code, info = run_docker_compose_tests(
+        image_name=image_name,
+        include_success_outputs=include_success_outputs,
+        extra_pytest_args=extra_pytest_args,
+        skip_docker_compose_deletion=skip_docker_compose_deletion,
+        test_type="airflow-ctl-integration",
+    )
+    sys.exit(return_code)
+
+
 @group_for_testing.command(
     name="airflow-ctl-tests",
     help="Run airflow-ctl tests - all airflowctl tests are non-DB bound 
tests.",
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 9a9e706fdc3..ff0f8012d91 100644
--- a/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/testing_commands_config.py
@@ -152,7 +152,7 @@ TESTING_COMMANDS: list[dict[str, str | list[str]]] = [
     },
     {
         "name": "airflowctl Tests",
-        "commands": ["airflow-ctl-tests"],
+        "commands": ["airflow-ctl-tests", "airflow-ctl-integration-tests"],
     },
     {
         "name": "Other Tests",
@@ -206,6 +206,19 @@ TESTING_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
             ],
         },
     ],
+    "breeze testing airflow-ctl-integration-tests": [
+        {
+            "name": "Docker-compose tests flag",
+            "options": [
+                "--image-name",
+                "--python",
+                "--skip-docker-compose-deletion",
+                "--include-success-outputs",
+                "--github-repository",
+                "--airflow-ctl-version",
+            ],
+        }
+    ],
     "breeze testing core-integration-tests": [
         TEST_OPTIONS_DB,
         TEST_ENVIRONMENT_DB,
diff --git a/dev/breeze/src/airflow_breeze/global_constants.py 
b/dev/breeze/src/airflow_breeze/global_constants.py
index 523412ed4d8..8b765ef0b86 100644
--- a/dev/breeze/src/airflow_breeze/global_constants.py
+++ b/dev/breeze/src/airflow_breeze/global_constants.py
@@ -275,6 +275,7 @@ class GroupOfTests(Enum):
     TASK_SDK = "task-sdk"
     TASK_SDK_INTEGRATION = "task-sdk-integration"
     CTL = "airflow-ctl"
+    CTL_INTEGRATION = "airflow-ctl-integration"
     HELM = "helm"
     INTEGRATION_CORE = "integration-core"
     INTEGRATION_PROVIDERS = "integration-providers"
@@ -312,6 +313,7 @@ ALLOWED_TEST_TYPE_CHOICES: dict[GroupOfTests, list[str]] = {
     GroupOfTests.TASK_SDK_INTEGRATION: [ALL_TEST_TYPE],
     GroupOfTests.HELM: [ALL_TEST_TYPE, *all_helm_test_packages()],
     GroupOfTests.CTL: [ALL_TEST_TYPE],
+    GroupOfTests.CTL_INTEGRATION: [ALL_TEST_TYPE],
 }
 
 
diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py 
b/dev/breeze/src/airflow_breeze/utils/run_tests.py
index b38dfe4561f..9b67c7dea70 100644
--- a/dev/breeze/src/airflow_breeze/utils/run_tests.py
+++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py
@@ -47,6 +47,8 @@ TASK_SDK_TESTS_ROOT_PATH = AIRFLOW_ROOT_PATH / 
"task-sdk-tests"
 TASK_SDK_TESTS_TESTS_MODULE_PATH = TASK_SDK_TESTS_ROOT_PATH / "tests" / 
"task_sdk_tests"
 TASK_SDK_TESTS_REQUIREMENTS = TASK_SDK_TESTS_ROOT_PATH / "requirements.txt"
 
+AIRFLOW_CTL_TESTS_ROOT_PATH = AIRFLOW_ROOT_PATH / "airflow-ctl-tests"
+
 IGNORE_DB_INIT_FOR_TEST_GROUPS = [
     GroupOfTests.HELM,
     GroupOfTests.PYTHON_API_CLIENT,
@@ -111,6 +113,9 @@ def run_docker_compose_tests(
     if test_type == "task-sdk-integration":
         test_path = Path("tests") / "task_sdk_tests" / 
"test_task_sdk_health.py"
         cwd = TASK_SDK_TESTS_ROOT_PATH.as_posix()
+    elif test_type == "airflow-ctl-integration":
+        test_path = Path("tests") / "airflowctl_tests" / 
"test_airflowctl_commands.py"
+        cwd = AIRFLOW_CTL_TESTS_ROOT_PATH.as_posix()
     else:
         test_path = Path("tests") / "docker_tests" / 
"test_docker_compose_quick_start.py"
         cwd = DOCKER_TESTS_ROOT_PATH.as_posix()
diff --git a/devel-common/pyproject.toml b/devel-common/pyproject.toml
index d297bedbbcd..7f4ea1f4318 100644
--- a/devel-common/pyproject.toml
+++ b/devel-common/pyproject.toml
@@ -40,6 +40,7 @@ dependencies = [
     "time-machine>=2.15.0",
     "wheel>=0.42.0",
     "yamllint>=1.33.0",
+    "python-on-whales>=0.70.0",
     "apache-airflow-devel-common[basic]"
 ]
 
diff --git a/docker-tests/pyproject.toml b/docker-tests/pyproject.toml
index 7a8363750ed..25c20506506 100644
--- a/docker-tests/pyproject.toml
+++ b/docker-tests/pyproject.toml
@@ -37,7 +37,6 @@ version = "0.0.1"
 
 dependencies = [
     "apache-airflow-devel-common",
-    "python-on-whales>=0.70.0",
 ]
 
 [tool.pytest.ini_options]
diff --git a/kubernetes-tests/pyproject.toml b/kubernetes-tests/pyproject.toml
index 6be8235f4d1..2ccb1824be1 100644
--- a/kubernetes-tests/pyproject.toml
+++ b/kubernetes-tests/pyproject.toml
@@ -41,7 +41,6 @@ dependencies = [
     "apache-airflow-task-sdk",
     # Requests 3 if it will be released, will be heavily breaking.
     "requests>=2.32.0,<3",
-    "python-on-whales>=0.70.0",
 ]
 
 [tool.pytest.ini_options]
diff --git a/pyproject.toml b/pyproject.toml
index 62995a5b42a..4c15515f57b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1007,6 +1007,7 @@ mypy_path = [
     "$MYPY_CONFIG_FILE_DIR/task-sdk/tests",
     "$MYPY_CONFIG_FILE_DIR/airflow-ctl/src",
     "$MYPY_CONFIG_FILE_DIR/airflow-ctl/tests",
+    "$MYPY_CONFIG_FILE_DIR/airflow-ctl-tests/tests",
     "$MYPY_CONFIG_FILE_DIR/dev",
     "$MYPY_CONFIG_FILE_DIR/devel-common/src",
     "$MYPY_CONFIG_FILE_DIR/helm-tests/tests",
@@ -1263,6 +1264,7 @@ dev = [
     "apache-airflow-kubernetes-tests",
     "apache-airflow-task-sdk",
     "apache-airflow-ctl",
+    "apache-airflow-ctl-tests",
     "apache-airflow-shared-logging",
     "apache-airflow-shared-secrets-masker",
     "apache-airflow-shared-timezones",
@@ -1304,6 +1306,7 @@ apache-airflow-breeze = {workspace = true}
 apache-airflow-dev = {workspace = true}
 apache-airflow-core = {workspace = true}
 apache-airflow-ctl = {workspace = true}
+apache-airflow-ctl-tests = {workspace = true}
 apache-airflow-task-sdk = { workspace = true }
 apache-airflow-devel-common = { workspace = true }
 apache-airflow-docker-tests = { workspace = true }
@@ -1421,6 +1424,7 @@ members = [
     "airflow-core",
     "dev/breeze",
     "airflow-ctl",
+    "airflow-ctl-tests",
     "dev",
     "devel-common",
     "docker-tests",
diff --git a/scripts/docker/install_airflow_when_building_images.sh 
b/scripts/docker/install_airflow_when_building_images.sh
index ca45faee1f1..c93c0387a63 100644
--- a/scripts/docker/install_airflow_when_building_images.sh
+++ b/scripts/docker/install_airflow_when_building_images.sh
@@ -81,6 +81,7 @@ function install_from_sources() {
               --editable ./airflow-core --editable ./task-sdk --editable 
./airflow-ctl \
               --editable ./kubernetes-tests --editable ./docker-tests 
--editable ./helm-tests \
               --editable ./task-sdk-tests \
+              --editable ./airflow-ctl-tests \
               --editable ./devel-common[all] --editable ./dev \
               --group dev --group docs --group docs-gen --group leveldb"
         local -a projects_with_devel_dependencies
diff --git a/task-sdk-tests/pyproject.toml b/task-sdk-tests/pyproject.toml
index 5e6ac78373c..1de482b3c87 100644
--- a/task-sdk-tests/pyproject.toml
+++ b/task-sdk-tests/pyproject.toml
@@ -38,7 +38,6 @@ version = "0.0.1"
 dependencies = [
     "apache-airflow-core",
     "apache-airflow-devel-common",
-    "python-on-whales>=0.70.0",
 ]
 
 [tool.pytest.ini_options]

Reply via email to