You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ep...@apache.org on 2026/01/16 08:59:57 UTC
(airflow) 15/43: Sync airflowctl to v3-1-test (#60350)
This is an automated email from the ASF dual-hosted git repository.
ephraimanierobi pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
commit 9eb17092ef3d5bae9b60b8c648f8e355f7745097
Author: Bugra Ozturk <bu...@users.noreply.github.com>
AuthorDate: Sat Jan 10 14:54:10 2026 +0100
Sync airflowctl to v3-1-test (#60350)
---
airflow-ctl-tests/pyproject.toml | 6 +-
.../tests/airflowctl_tests/conftest.py | 111 ++++-------
.../tests/airflowctl_tests/constants.py | 7 +-
.../airflowctl_tests/fixtures/pools_export.json | 38 ++++
.../fixtures/test_connections.json | 11 +
.../airflowctl_tests/fixtures/test_pools.json | 8 +
.../airflowctl_tests/fixtures/test_variables.json | 7 +
.../fixtures/variables_export.json | 8 +
.../airflowctl_tests/test_airflowctl_commands.py | 222 ++++++++++++++++-----
airflow-ctl/.pre-commit-config.yaml | 2 +-
airflow-ctl/NOTICE | 2 +-
airflow-ctl/RELEASE_NOTES.rst | 4 +-
airflow-ctl/docs/conf.py | 12 +-
airflow-ctl/docs/images/command_hashes.txt | 4 +-
airflow-ctl/docs/images/output_connections.svg | 141 ++++++-------
airflow-ctl/docs/images/output_variables.svg | 114 +++++------
.../docs/installation/installing-from-pypi.rst | 5 +-
.../docs/installation/installing-from-sources.rst | 22 +-
airflow-ctl/newsfragments/59850.significant.rst | 30 +++
airflow-ctl/pyproject.toml | 20 +-
airflow-ctl/src/airflowctl/__init__.py | 2 +-
airflow-ctl/src/airflowctl/ctl/cli_config.py | 30 +--
.../airflowctl/ctl/commands/variable_command.py | 22 --
airflow-ctl/tests/airflow_ctl/api/test_client.py | 2 +-
.../tests/airflow_ctl/api/test_operations.py | 8 +-
.../airflow_ctl/ctl/commands/test_dag_command.py | 8 +-
.../ctl/commands/test_variable_command.py | 20 --
.../ci/prek/check_airflowctl_command_coverage.py | 150 ++++++++++++++
.../in_container/run_capture_airflowctl_help.py | 6 +-
29 files changed, 640 insertions(+), 382 deletions(-)
diff --git a/airflow-ctl-tests/pyproject.toml b/airflow-ctl-tests/pyproject.toml
index 24c7f561ed1..2cb3a8109b2 100644
--- a/airflow-ctl-tests/pyproject.toml
+++ b/airflow-ctl-tests/pyproject.toml
@@ -26,7 +26,7 @@ description = "Airflow CTL tests for Apache Airflow"
classifiers = [
"Private :: Do Not Upload",
]
-requires-python = ">=3.10,!=3.13"
+requires-python = ">=3.10,!=3.14"
authors = [
{ name = "Apache Software Foundation", email = "dev@airflow.apache.org" },
]
@@ -69,3 +69,7 @@ exclude = ["*"]
[tool.hatch.build.targets.wheel]
bypass-selection = true
+
+[tool.ruff.lint.per-file-ignores]
+
+"!test/*" = ["S101"]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
index 13dacf5d8c5..a34b4804d65 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -20,7 +20,6 @@ import os
import subprocess
import sys
-import pytest
from python_on_whales import DockerClient, docker
from airflowctl_tests import console
@@ -30,14 +29,18 @@ from airflowctl_tests.constants import (
DOCKER_IMAGE,
)
-docker_client = None
+
+class _CtlTestState:
+ docker_client: DockerClient | None = 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_version = os.environ.get("AIRFLOW_CTL_VERSION", "0.1.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}")
@@ -50,12 +53,16 @@ def pytest_sessionstart(session):
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!")
+ 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...")
+ console.print(
+ "[yellow]Verifying airflowctl installation via pytest_sessionstart..."
+ )
try:
result = subprocess.run(
[
@@ -69,7 +76,9 @@ def pytest_sessionstart(session):
)
console.print(f"[green]{result.stdout.strip()}")
except subprocess.CalledProcessError as e:
- console.print("[red]❌ airflowctl import verification failed via pytest_sessionstart:")
+ 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}")
@@ -115,7 +124,9 @@ def debug_environment():
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()}")
+ 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()
@@ -124,7 +135,9 @@ def debug_environment():
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()}")
+ 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}")
@@ -143,8 +156,6 @@ 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}")
@@ -167,14 +178,20 @@ def docker_compose_up(tmp_path_factory):
os.environ["ENV_FILE_PATH"] = str(tmp_dir / ".env")
# Initialize Docker client
- docker_client = DockerClient(compose_files=[str(tmp_docker_compose_file)])
+ _CtlTestState.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)
+ _CtlTestState.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())
+ print_diagnostics(
+ _CtlTestState.docker_client.compose,
+ _CtlTestState.docker_client.compose.version(),
+ docker.version(),
+ )
debug_environment()
docker_compose_down()
raise
@@ -182,71 +199,13 @@ def docker_compose_up(tmp_path_factory):
def docker_compose_down():
"""Tear down Docker Compose environment."""
- if docker_client:
- docker_client.compose.down(remove_orphans=True, volumes=True, quiet=True)
+ if _CtlTestState.docker_client:
+ _CtlTestState.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
-@pytest.fixture
-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"
-
-
-@pytest.fixture
-def login_output():
- return "Login successful! Welcome to airflowctl!"
-
-
-@pytest.fixture
-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()
-
-
-@pytest.fixture
-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
index 549a9ad8c74..3f40c99d84b 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/constants.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
@@ -28,5 +28,10 @@ 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"
+ AIRFLOW_ROOT_PATH
+ / "airflow-core"
+ / "docs"
+ / "howto"
+ / "docker-compose"
+ / "docker-compose.yaml"
)
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json
new file mode 100644
index 00000000000..4752ad7801f
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json
@@ -0,0 +1,38 @@
+[
+ {
+ "deferred_slots": 0,
+ "description": "Default pool",
+ "include_deferred": false,
+ "name": "default_pool",
+ "occupied_slots": 0,
+ "open_slots": 128,
+ "queued_slots": 0,
+ "running_slots": 0,
+ "scheduled_slots": 0,
+ "slots": 128
+ },
+ {
+ "deferred_slots": 0,
+ "description": null,
+ "include_deferred": false,
+ "name": "test_pool",
+ "occupied_slots": 0,
+ "open_slots": 10,
+ "queued_slots": 0,
+ "running_slots": 0,
+ "scheduled_slots": 0,
+ "slots": 10
+ },
+ {
+ "deferred_slots": 0,
+ "description": "Test pool for import",
+ "include_deferred": false,
+ "name": "test_import_pool",
+ "occupied_slots": 0,
+ "open_slots": 10,
+ "queued_slots": 0,
+ "running_slots": 0,
+ "scheduled_slots": 0,
+ "slots": 10
+ }
+]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json
new file mode 100644
index 00000000000..0141f399ec2
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json
@@ -0,0 +1,11 @@
+{
+ "test_import_conn": {
+ "conn_type": "postgres",
+ "host": "localhost",
+ "login": "testuser",
+ "password": "testpass",
+ "port": 5432,
+ "extra": "{}",
+ "description": "Test connection for import"
+ }
+}
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json
new file mode 100644
index 00000000000..691e1d62d56
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json
@@ -0,0 +1,8 @@
+[
+ {
+ "name": "test_import_pool",
+ "slots": 10,
+ "description": "Test pool for import",
+ "include_deferred": false
+ }
+]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json
new file mode 100644
index 00000000000..05de7d2fc8b
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json
@@ -0,0 +1,7 @@
+{
+ "test_import_var": "import_value",
+ "test_import_var_with_desc": {
+ "value": "import_value_with_description",
+ "description": "Test variable with description"
+ }
+}
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json
new file mode 100644
index 00000000000..f8aed8ce379
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json
@@ -0,0 +1,8 @@
+{
+ "test_import_var": "import_value",
+ "test_import_var_with_desc": {
+ "description": "Test variable with description",
+ "value": "import_value_with_description"
+ },
+ "test_key": "updated_value"
+}
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index d17e646cb06..9ba407b1aa8 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -19,59 +19,181 @@ from __future__ import annotations
import os
from subprocess import PIPE, STDOUT, Popen
+import pytest
+
from airflowctl_tests import console
-def test_airflowctl_commands(login_command, login_output, test_commands):
+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()
+
+
+LOGIN_COMMAND = "auth login --username airflow --password airflow"
+LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
+ONE_DATE_PARAM = date_param()
+TEST_COMMANDS = [
+ # 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
+ LOGIN_COMMAND,
+ # Assets commands
+ "assets list",
+ "assets get --asset-id=1",
+ "assets create-event --asset-id=1",
+ # Backfill commands
+ "backfill list",
+ # Config commands
+ "config get --section core --option executor",
+ "config list",
+ "config lint",
+ # Connections commands
+ "connections create --connection-id=test_con --conn-type=mysql --password=TEST_PASS -o json",
+ "connections list",
+ "connections list -o yaml",
+ "connections list -o table",
+ "connections get --conn-id=test_con",
+ "connections get --conn-id=test_con -o json",
+ "connections update --connection-id=test_con --conn-type=postgres",
+ "connections import tests/airflowctl_tests/fixtures/test_connections.json",
+ "connections delete --conn-id=test_con",
+ "connections delete --conn-id=test_import_conn",
+ # DAGs commands
+ "dags list",
+ "dags get --dag-id=example_bash_operator",
+ "dags get-details --dag-id=example_bash_operator",
+ "dags get-stats --dag-ids=example_bash_operator",
+ "dags get-version --dag-id=example_bash_operator --version-number=1",
+ "dags list-import-errors",
+ "dags list-version --dag-id=example_bash_operator",
+ "dags list-warning",
+ # Order of trigger and pause/unpause is important for test stability because state checked
+ f"dags trigger --dag-id=example_bash_operator --logical-date={ONE_DATE_PARAM} --run-after={ONE_DATE_PARAM}",
+ "dags pause example_bash_operator",
+ "dags unpause example_bash_operator",
+ # DAG Run commands
+ f'dagrun get --dag-id=example_bash_operator --dag-run-id="manual__{ONE_DATE_PARAM}"',
+ "dags update --dag-id=example_bash_operator --no-is-paused",
+ # DAG Run commands
+ "dagrun list --dag-id example_bash_operator --state success --limit=1",
+ # Jobs commands
+ "jobs list",
+ # Pools commands
+ "pools create --name=test_pool --slots=5",
+ "pools list",
+ "pools get --pool-name=test_pool",
+ "pools get --pool-name=test_pool -o yaml",
+ "pools update --pool=test_pool --slots=10",
+ "pools import tests/airflowctl_tests/fixtures/test_pools.json",
+ "pools delete --pool=test_pool",
+ "pools delete --pool=test_import_pool",
+ # Providers commands
+ "providers list",
+ # Variables commands
+ "variables create --key=test_key --value=test_value",
+ "variables list",
+ "variables get --variable-key=test_key",
+ "variables get --variable-key=test_key -o table",
+ "variables update --key=test_key --value=updated_value",
+ "variables import tests/airflowctl_tests/fixtures/test_variables.json",
+ "variables delete --variable-key=test_key",
+ "variables delete --variable-key=test_import_var",
+ "variables delete --variable-key=test_import_var_with_desc",
+ # Version command
+ "version --remote",
+]
+
+
+@pytest.mark.flaky(reruns=3, reruns_delay=1)
+@pytest.mark.parametrize(
+ "command",
+ TEST_COMMANDS,
+ ids=[" ".join(command.split(" ", 2)[:2]) for command in TEST_COMMANDS],
+)
+def test_airflowctl_commands(command: str):
"""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"
- airflowctl_command_error = "command error: argument GROUP_OR_COMMAND: invalid choice"
- if (
- airflowctl_client_server_response_error in stdout_result
- or airflowctl_command_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()
+
+ 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_bytes, stderr_result = proc.communicate(timeout=60)
+
+ # CLI command gave errors
+ assert not stderr_result, (
+ f"Errors while executing command '{command_from_config}':\n{stderr_result.decode()}"
+ )
+
+ # Decode the output
+ stdout_result = stdout_bytes.decode()
+ # We need to trim auth login output if the command is not login itself and clean backspaces
+ if command != LOGIN_COMMAND:
+ assert LOGIN_OUTPUT in stdout_result, (
+ f"❌ Login output not found before command output for '{command_from_config}'",
+ f"\nFull output:\n{stdout_result}",
+ )
+ stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
+ else:
+ stdout_result = stdout_result.strip()
+
+ # Check for non-zero exit code
+ assert proc.returncode == 0, (
+ f"❌ Command '{command_from_config}' exited with code {proc.returncode}",
+ f"\nOutput:\n{stdout_result}",
+ )
+
+ # Error patterns to detect failures that might otherwise slip through
+ # Please ensure it is aligning with airflowctl.api.client.get_json_error
+ error_patterns = [
+ "Server error",
+ "command error",
+ "unrecognized arguments",
+ "invalid choice",
+ "Traceback (most recent call last):",
+ ]
+ matched_error = next(
+ (error for error in error_patterns if error in stdout_result), None
+ )
+ assert not matched_error, (
+ f"❌ Output contained unexpected text for command '{command_from_config}'",
+ f"\nMatched error pattern: {matched_error}",
+ f"\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/.pre-commit-config.yaml b/airflow-ctl/.pre-commit-config.yaml
index 8926b9e7336..00eada7f645 100644
--- a/airflow-ctl/.pre-commit-config.yaml
+++ b/airflow-ctl/.pre-commit-config.yaml
@@ -16,7 +16,7 @@
# under the License.
---
default_stages: [pre-commit, pre-push]
-minimum_prek_version: '0.2.0'
+minimum_prek_version: '0.0.28'
default_language_version:
python: python3
node: 22.19.0
diff --git a/airflow-ctl/NOTICE b/airflow-ctl/NOTICE
index e02aab0589f..a51bd9390d0 100644
--- a/airflow-ctl/NOTICE
+++ b/airflow-ctl/NOTICE
@@ -1,5 +1,5 @@
Apache Airflow
-Copyright 2016-2025 The Apache Software Foundation
+Copyright 2016-2026 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst
index 70d19556a9f..58f1ecbeacb 100644
--- a/airflow-ctl/RELEASE_NOTES.rst
+++ b/airflow-ctl/RELEASE_NOTES.rst
@@ -15,10 +15,10 @@
specific language governing permissions and limitations
under the License.
-airflowctl 0.1.0 (2025-11-05)
+airflowctl 0.1.0 (2025-11-03)
-----------------------------
-Release of airflowctl, a command-line tool. There are lots of great features to use from start.
+Beta Release of airflowctl, a command-line tool. There are lots of great features to use from start.
Please check the documentation for quick start and usage instructions.
Please visit quick start guide: :doc:`/start`
diff --git a/airflow-ctl/docs/conf.py b/airflow-ctl/docs/conf.py
index 5a19b2adea3..08f867ee43f 100644
--- a/airflow-ctl/docs/conf.py
+++ b/airflow-ctl/docs/conf.py
@@ -207,18 +207,12 @@ jinja_contexts = {
"config_ctx": {"configs": configs, "deprecated_options": deprecated_options},
"quick_start_ctx": {"doc_root_url": f"https://airflow.apache.org/docs/apache-airflow/{PACKAGE_VERSION}/"},
"official_download_page": {
- "base_url": f"https://downloads.apache.org/airflow/airflow-ctl/{PACKAGE_VERSION}",
- "closer_lua_url": f"https://www.apache.org/dyn/closer.lua/airflow/airflow-ctl/{PACKAGE_VERSION}",
- "airflowctl_version": PACKAGE_VERSION,
+ "base_url": f"https://downloads.apache.org/airflow/{PACKAGE_VERSION}",
+ "closer_lua_url": f"https://www.apache.org/dyn/closer.lua/airflow/{PACKAGE_VERSION}",
+ "airflow_version": PACKAGE_VERSION,
},
}
-# Use for generate rst_epilog and other post-generation substitutions
-global_substitutions = {
- "version": PACKAGE_VERSION,
- "experimental": "This is an :ref:`experimental feature <experimental>`.",
-}
-
# -- Options for sphinx.ext.autodoc --------------------------------------------
# See: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
diff --git a/airflow-ctl/docs/images/command_hashes.txt b/airflow-ctl/docs/images/command_hashes.txt
index 6792a0234ef..c0e3b5995fa 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -3,12 +3,12 @@ assets:b3ae2b933e54528bf486ff28e887804d
auth:f396d4bce90215599dde6ad0a8f30f29
backfill:bbce9859a2d1ce054ad22db92dea8c05
config:cb175bedf29e8a2c2c6a2ebd13d770a7
-connections:a16225e1c7d28488d0da612752669b4b
+connections:e34b6b93f64714986139958c1f370428
dags:287a128a71c97d2b537e09a5c7c73c09
dagrun:f47ed2a89ed0f8c71f79dba53a3a3882
jobs:7f8680afff230eb9940bc7fca727bd52
pools:03fc7d948cbecf16ff8d640eb8f0ce43
providers:1c0afb2dff31d93ab2934b032a2250ab
-variables:0b04188937b3c364204ef4cc9a541c62
+variables:0354f8f4b0dde1c3771ed1568692c6ae
version:d4a7a6229b3a204f114283b62eac789b
auth login:5277c653ff6dce51f37472dc0bda9775
diff --git a/airflow-ctl/docs/images/output_connections.svg b/airflow-ctl/docs/images/output_connections.svg
index 8283756115f..7dc48cce16a 100644
--- a/airflow-ctl/docs/images/output_connections.svg
+++ b/airflow-ctl/docs/images/output_connections.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 562.4" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 489.2" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,123 +19,110 @@
font-weight: 700;
}
- .terminal-3087056112-matrix {
+ .terminal-1162206820-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-3087056112-title {
+ .terminal-1162206820-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-3087056112-r1 { fill: #ff8700 }
-.terminal-3087056112-r2 { fill: #c5c8c6 }
-.terminal-3087056112-r3 { fill: #808080 }
-.terminal-3087056112-r4 { fill: #68a0b3 }
-.terminal-3087056112-r5 { fill: #c5c8c6;font-weight: bold }
+ .terminal-1162206820-r1 { fill: #ff8700 }
+.terminal-1162206820-r2 { fill: #c5c8c6 }
+.terminal-1162206820-r3 { fill: #808080 }
+.terminal-1162206820-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-3087056112-clip-terminal">
- <rect x="0" y="0" width="792.0" height="511.4" />
+ <clipPath id="terminal-1162206820-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="438.2" />
</clipPath>
- <clipPath id="terminal-3087056112-line-0">
- <rect x="0" y="1.5" width="793" height="24.65"/>
+ <clipPath id="terminal-1162206820-line-0">
+ <rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-1">
- <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-1">
+ <rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-2">
- <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-2">
+ <rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-3">
- <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-3">
+ <rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-4">
- <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-4">
+ <rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-5">
- <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-5">
+ <rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-6">
- <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-6">
+ <rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-7">
- <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-7">
+ <rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-8">
- <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-8">
+ <rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-9">
- <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-9">
+ <rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-10">
- <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-10">
+ <rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-11">
- <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-11">
+ <rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-12">
- <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-12">
+ <rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-13">
- <rect x="0" y="318.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-13">
+ <rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-14">
- <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-14">
+ <rect x="0" y="343.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-15">
- <rect x="0" y="367.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-15">
+ <rect x="0" y="367.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-3087056112-line-16">
- <rect x="0" y="391.9" width="793" height="24.65"/>
- </clipPath>
-<clipPath id="terminal-3087056112-line-17">
- <rect x="0" y="416.3" width="793" height="24.65"/>
- </clipPath>
-<clipPath id="terminal-3087056112-line-18">
- <rect x="0" y="440.7" width="793" height="24.65"/>
- </clipPath>
-<clipPath id="terminal-3087056112-line-19">
- <rect x="0" y="465.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-16">
+ <rect x="0" y="391.9" width="915" height="24.65"/>
</clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="809" height="560.4" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="931" height="487.2" rx="8"/>
<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(#terminal-3087056112-clip-terminal)">
+ <g transform="translate(9, 41)" clip-path="url(#terminal-1162206820-clip-terminal)">
- <g class="terminal-3087056112-matrix">
- <text class="terminal-3087056112-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-3087056112-line-0)">Usage:</text><text class="terminal-3087056112-r3" x="85.4" y="20" textLength="268.4" clip-path="url(#terminal-3087056112-line-0)">airflowctl connections</text><text class="terminal-3087056112-r2" x="353.8" y="20" textLength="24.4" clip-path="url(#terminal-3087056112-line-0)"> [</text><text class="terminal-3087056112-r4" x="378.2" y="20" textLength="24.4" clip-pat [...]
-</text><text class="terminal-3087056112-r2" x="793" y="44.4" textLength="12.2" clip-path="url(#terminal-3087056112-line-1)">
-</text><text class="terminal-3087056112-r2" x="0" y="68.8" textLength="366" clip-path="url(#terminal-3087056112-line-2)">Perform Connections operations</text><text class="terminal-3087056112-r2" x="793" y="68.8" textLength="12.2" clip-path="url(#terminal-3087056112-line-2)">
-</text><text class="terminal-3087056112-r2" x="793" y="93.2" textLength="12.2" clip-path="url(#terminal-3087056112-line-3)">
-</text><text class="terminal-3087056112-r1" x="0" y="117.6" textLength="256.2" clip-path="url(#terminal-3087056112-line-4)">Positional Arguments:</text><text class="terminal-3087056112-r2" x="793" y="117.6" textLength="12.2" clip-path="url(#terminal-3087056112-line-4)">
-</text><text class="terminal-3087056112-r4" x="24.4" y="142" textLength="85.4" clip-path="url(#terminal-3087056112-line-5)">COMMAND</text><text class="terminal-3087056112-r2" x="793" y="142" textLength="12.2" clip-path="url(#terminal-3087056112-line-5)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="166.4" textLength="73.2" clip-path="url(#terminal-3087056112-line-6)">create</text><text class="terminal-3087056112-r2" x="231.8" y="166.4" textLength="292.8" clip-path="url(#terminal-3087056112-line-6)">Perform create operation</text><text class="terminal-3087056112-r2" x="793" y="166.4" textLength="12.2" clip-path="url(#terminal-3087056112-line-6)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="190.8" textLength="183" clip-path="url(#terminal-3087056112-line-7)">create-defaults</text><text class="terminal-3087056112-r2" x="793" y="190.8" textLength="12.2" clip-path="url(#terminal-3087056112-line-7)">
-</text><text class="terminal-3087056112-r2" x="231.8" y="215.2" textLength="402.6" clip-path="url(#terminal-3087056112-line-8)">Perform create_defaults operation</text><text class="terminal-3087056112-r2" x="793" y="215.2" textLength="12.2" clip-path="url(#terminal-3087056112-line-8)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="239.6" textLength="73.2" clip-path="url(#terminal-3087056112-line-9)">delete</text><text class="terminal-3087056112-r2" x="231.8" y="239.6" textLength="292.8" clip-path="url(#terminal-3087056112-line-9)">Perform delete operation</text><text class="terminal-3087056112-r2" x="793" y="239.6" textLength="12.2" clip-path="url(#terminal-3087056112-line-9)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="264" textLength="36.6" clip-path="url(#terminal-3087056112-line-10)">get</text><text class="terminal-3087056112-r2" x="231.8" y="264" textLength="256.2" clip-path="url(#terminal-3087056112-line-10)">Perform get operation</text><text class="terminal-3087056112-r2" x="793" y="264" textLength="12.2" clip-path="url(#terminal-3087056112-line-10)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="288.4" textLength="73.2" clip-path="url(#terminal-3087056112-line-11)">import</text><text class="terminal-3087056112-r2" x="231.8" y="288.4" textLength="549" clip-path="url(#terminal-3087056112-line-11)">Import connections from a file. This feature </text><text class="terminal-3087056112-r2" x="793" y="288.4" textLength="12.2" clip-path="url(#terminal-3087056112-line-11)">
-</text><text class="terminal-3087056112-r2" x="0" y="312.8" textLength="390.4" clip-path="url(#terminal-3087056112-line-12)">is compatible with airflow CLI `</text><text class="terminal-3087056112-r5" x="390.4" y="312.8" textLength="329.4" clip-path="url(#terminal-3087056112-line-12)">airflow connections export </text><text class="terminal-3087056112-r2" x="793" y="312.8" textLength="12.2" clip-path="url(#terminal-3087056112-line-12)">
-</text><text class="terminal-3087056112-r5" x="0" y="337.2" textLength="73.2" clip-path="url(#terminal-3087056112-line-13)">a.json</text><text class="terminal-3087056112-r2" x="73.2" y="337.2" textLength="329.4" clip-path="url(#terminal-3087056112-line-13)">` command. Export it from `</text><text class="terminal-3087056112-r5" x="402.6" y="337.2" textLength="134.2" clip-path="url(#terminal-3087056112-line-13)">airflow CLI</text><text class="terminal-30870561 [...]
-</text><text class="terminal-3087056112-r2" x="0" y="361.6" textLength="317.2" clip-path="url(#terminal-3087056112-line-14)">securely via this command.</text><text class="terminal-3087056112-r2" x="793" y="361.6" textLength="12.2" clip-path="url(#terminal-3087056112-line-14)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="386" textLength="48.8" clip-path="url(#terminal-3087056112-line-15)">list</text><text class="terminal-3087056112-r2" x="231.8" y="386" textLength="268.4" clip-path="url(#terminal-3087056112-line-15)">Perform list operation</text><text class="terminal-3087056112-r2" x="793" y="386" textLength="12.2" clip-path="url(#terminal-3087056112-line-15)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="410.4" textLength="48.8" clip-path="url(#terminal-3087056112-line-16)">test</text><text class="terminal-3087056112-r2" x="231.8" y="410.4" textLength="268.4" clip-path="url(#terminal-3087056112-line-16)">Perform test operation</text><text class="terminal-3087056112-r2" x="793" y="410.4" textLength="12.2" clip-path="url(#terminal-3087056112-line-16)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="434.8" textLength="73.2" clip-path="url(#terminal-3087056112-line-17)">update</text><text class="terminal-3087056112-r2" x="231.8" y="434.8" textLength="292.8" clip-path="url(#terminal-3087056112-line-17)">Perform update operation</text><text class="terminal-3087056112-r2" x="793" y="434.8" textLength="12.2" clip-path="url(#terminal-3087056112-line-17)">
-</text><text class="terminal-3087056112-r2" x="793" y="459.2" textLength="12.2" clip-path="url(#terminal-3087056112-line-18)">
-</text><text class="terminal-3087056112-r1" x="0" y="483.6" textLength="97.6" clip-path="url(#terminal-3087056112-line-19)">Options:</text><text class="terminal-3087056112-r2" x="793" y="483.6" textLength="12.2" clip-path="url(#terminal-3087056112-line-19)">
-</text><text class="terminal-3087056112-r4" x="24.4" y="508" textLength="24.4" clip-path="url(#terminal-3087056112-line-20)">-h</text><text class="terminal-3087056112-r2" x="48.8" y="508" textLength="24.4" clip-path="url(#terminal-3087056112-line-20)">, </text><text class="terminal-3087056112-r4" x="73.2" y="508" textLength="73.2" clip-path="url(#terminal-3087056112-line-20)">--help</text><text class="terminal-3087056112-r2" x="231.8" y="508" textLength="378.2" clip-path="url(#termi [...]
+ <g class="terminal-1162206820-matrix">
+ <text class="terminal-1162206820-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-1162206820-line-0)">Usage:</text><text class="terminal-1162206820-r3" x="85.4" y="20" textLength="268.4" clip-path="url(#terminal-1162206820-line-0)">airflowctl connections</text><text class="terminal-1162206820-r2" x="353.8" y="20" textLength="24.4" clip-path="url(#terminal-1162206820-line-0)"> [</text><text class="terminal-1162206820-r4" x="378.2" y="20" textLength="24.4" clip-pat [...]
+</text><text class="terminal-1162206820-r2" x="915" y="44.4" textLength="12.2" clip-path="url(#terminal-1162206820-line-1)">
+</text><text class="terminal-1162206820-r2" x="0" y="68.8" textLength="366" clip-path="url(#terminal-1162206820-line-2)">Perform Connections operations</text><text class="terminal-1162206820-r2" x="915" y="68.8" textLength="12.2" clip-path="url(#terminal-1162206820-line-2)">
+</text><text class="terminal-1162206820-r2" x="915" y="93.2" textLength="12.2" clip-path="url(#terminal-1162206820-line-3)">
+</text><text class="terminal-1162206820-r1" x="0" y="117.6" textLength="256.2" clip-path="url(#terminal-1162206820-line-4)">Positional Arguments:</text><text class="terminal-1162206820-r2" x="915" y="117.6" textLength="12.2" clip-path="url(#terminal-1162206820-line-4)">
+</text><text class="terminal-1162206820-r4" x="24.4" y="142" textLength="85.4" clip-path="url(#terminal-1162206820-line-5)">COMMAND</text><text class="terminal-1162206820-r2" x="915" y="142" textLength="12.2" clip-path="url(#terminal-1162206820-line-5)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="166.4" textLength="73.2" clip-path="url(#terminal-1162206820-line-6)">create</text><text class="terminal-1162206820-r2" x="231.8" y="166.4" textLength="292.8" clip-path="url(#terminal-1162206820-line-6)">Perform create operation</text><text class="terminal-1162206820-r2" x="915" y="166.4" textLength="12.2" clip-path="url(#terminal-1162206820-line-6)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="190.8" textLength="183" clip-path="url(#terminal-1162206820-line-7)">create-defaults</text><text class="terminal-1162206820-r2" x="915" y="190.8" textLength="12.2" clip-path="url(#terminal-1162206820-line-7)">
+</text><text class="terminal-1162206820-r2" x="231.8" y="215.2" textLength="402.6" clip-path="url(#terminal-1162206820-line-8)">Perform create_defaults operation</text><text class="terminal-1162206820-r2" x="915" y="215.2" textLength="12.2" clip-path="url(#terminal-1162206820-line-8)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="239.6" textLength="73.2" clip-path="url(#terminal-1162206820-line-9)">delete</text><text class="terminal-1162206820-r2" x="231.8" y="239.6" textLength="292.8" clip-path="url(#terminal-1162206820-line-9)">Perform delete operation</text><text class="terminal-1162206820-r2" x="915" y="239.6" textLength="12.2" clip-path="url(#terminal-1162206820-line-9)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="264" textLength="36.6" clip-path="url(#terminal-1162206820-line-10)">get</text><text class="terminal-1162206820-r2" x="231.8" y="264" textLength="256.2" clip-path="url(#terminal-1162206820-line-10)">Perform get operation</text><text class="terminal-1162206820-r2" x="915" y="264" textLength="12.2" clip-path="url(#terminal-1162206820-line-10)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="288.4" textLength="73.2" clip-path="url(#terminal-1162206820-line-11)">import</text><text class="terminal-1162206820-r2" x="231.8" y="288.4" textLength="671" clip-path="url(#terminal-1162206820-line-11)">Import connections from a file exported with local CLI.</text><text class="terminal-1162206820-r2" x="915" y="288.4" textLength="12.2" clip-path="url(#terminal-1162206820-line-11)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="312.8" textLength="48.8" clip-path="url(#terminal-1162206820-line-12)">list</text><text class="terminal-1162206820-r2" x="231.8" y="312.8" textLength="268.4" clip-path="url(#terminal-1162206820-line-12)">Perform list operation</text><text class="terminal-1162206820-r2" x="915" y="312.8" textLength="12.2" clip-path="url(#terminal-1162206820-line-12)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="337.2" textLength="48.8" clip-path="url(#terminal-1162206820-line-13)">test</text><text class="terminal-1162206820-r2" x="231.8" y="337.2" textLength="268.4" clip-path="url(#terminal-1162206820-line-13)">Perform test operation</text><text class="terminal-1162206820-r2" x="915" y="337.2" textLength="12.2" clip-path="url(#terminal-1162206820-line-13)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="361.6" textLength="73.2" clip-path="url(#terminal-1162206820-line-14)">update</text><text class="terminal-1162206820-r2" x="231.8" y="361.6" textLength="292.8" clip-path="url(#terminal-1162206820-line-14)">Perform update operation</text><text class="terminal-1162206820-r2" x="915" y="361.6" textLength="12.2" clip-path="url(#terminal-1162206820-line-14)">
+</text><text class="terminal-1162206820-r2" x="915" y="386" textLength="12.2" clip-path="url(#terminal-1162206820-line-15)">
+</text><text class="terminal-1162206820-r1" x="0" y="410.4" textLength="97.6" clip-path="url(#terminal-1162206820-line-16)">Options:</text><text class="terminal-1162206820-r2" x="915" y="410.4" textLength="12.2" clip-path="url(#terminal-1162206820-line-16)">
+</text><text class="terminal-1162206820-r4" x="24.4" y="434.8" textLength="24.4" clip-path="url(#terminal-1162206820-line-17)">-h</text><text class="terminal-1162206820-r2" x="48.8" y="434.8" textLength="24.4" clip-path="url(#terminal-1162206820-line-17)">, </text><text class="terminal-1162206820-r4" x="73.2" y="434.8" textLength="73.2" clip-path="url(#terminal-1162206820-line-17)">--help</text><text class="terminal-1162206820-r2" x="231.8" y="434.8" textLength="378.2" clip-path="ur [...]
</text>
</g>
</g>
diff --git a/airflow-ctl/docs/images/output_variables.svg b/airflow-ctl/docs/images/output_variables.svg
index 31599fa47c9..78c24d31074 100644
--- a/airflow-ctl/docs/images/output_variables.svg
+++ b/airflow-ctl/docs/images/output_variables.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 440.4" xmlns="http://www.w3.org/2000/svg">
+<svg class="rich-terminal" viewBox="0 0 933 416.0" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@@ -19,102 +19,98 @@
font-weight: 700;
}
- .terminal-4094392876-matrix {
+ .terminal-938427142-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
- .terminal-4094392876-title {
+ .terminal-938427142-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
- .terminal-4094392876-r1 { fill: #ff8700 }
-.terminal-4094392876-r2 { fill: #c5c8c6 }
-.terminal-4094392876-r3 { fill: #808080 }
-.terminal-4094392876-r4 { fill: #68a0b3 }
+ .terminal-938427142-r1 { fill: #ff8700 }
+.terminal-938427142-r2 { fill: #c5c8c6 }
+.terminal-938427142-r3 { fill: #808080 }
+.terminal-938427142-r4 { fill: #68a0b3 }
</style>
<defs>
- <clipPath id="terminal-4094392876-clip-terminal">
- <rect x="0" y="0" width="792.0" height="389.4" />
+ <clipPath id="terminal-938427142-clip-terminal">
+ <rect x="0" y="0" width="914.0" height="365.0" />
</clipPath>
- <clipPath id="terminal-4094392876-line-0">
- <rect x="0" y="1.5" width="793" height="24.65"/>
+ <clipPath id="terminal-938427142-line-0">
+ <rect x="0" y="1.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-1">
- <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-1">
+ <rect x="0" y="25.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-2">
- <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-2">
+ <rect x="0" y="50.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-3">
- <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-3">
+ <rect x="0" y="74.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-4">
- <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-4">
+ <rect x="0" y="99.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-5">
- <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-5">
+ <rect x="0" y="123.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-6">
- <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-6">
+ <rect x="0" y="147.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-7">
- <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-7">
+ <rect x="0" y="172.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-8">
- <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-8">
+ <rect x="0" y="196.7" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-9">
- <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-9">
+ <rect x="0" y="221.1" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-10">
- <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-10">
+ <rect x="0" y="245.5" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-11">
- <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-11">
+ <rect x="0" y="269.9" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-12">
- <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-12">
+ <rect x="0" y="294.3" width="915" height="24.65"/>
</clipPath>
-<clipPath id="terminal-4094392876-line-13">
- <rect x="0" y="318.7" width="793" height="24.65"/>
- </clipPath>
-<clipPath id="terminal-4094392876-line-14">
- <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-13">
+ <rect x="0" y="318.7" width="915" height="24.65"/>
</clipPath>
</defs>
- <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="809" height="438.4" rx="8"/>
+ <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="931" height="414" rx="8"/>
<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(#terminal-4094392876-clip-terminal)">
+ <g transform="translate(9, 41)" clip-path="url(#terminal-938427142-clip-terminal)">
- <g class="terminal-4094392876-matrix">
- <text class="terminal-4094392876-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-4094392876-line-0)">Usage:</text><text class="terminal-4094392876-r3" x="85.4" y="20" textLength="244" clip-path="url(#terminal-4094392876-line-0)">airflowctl variables</text><text class="terminal-4094392876-r2" x="329.4" y="20" textLength="24.4" clip-path="url(#terminal-4094392876-line-0)"> [</text><text class="terminal-4094392876-r4" x="353.8" y="20" textLength="24.4" clip-path="u [...]
-</text><text class="terminal-4094392876-r2" x="793" y="44.4" textLength="12.2" clip-path="url(#terminal-4094392876-line-1)">
-</text><text class="terminal-4094392876-r2" x="0" y="68.8" textLength="341.6" clip-path="url(#terminal-4094392876-line-2)">Perform Variables operations</text><text class="terminal-4094392876-r2" x="793" y="68.8" textLength="12.2" clip-path="url(#terminal-4094392876-line-2)">
-</text><text class="terminal-4094392876-r2" x="793" y="93.2" textLength="12.2" clip-path="url(#terminal-4094392876-line-3)">
-</text><text class="terminal-4094392876-r1" x="0" y="117.6" textLength="256.2" clip-path="url(#terminal-4094392876-line-4)">Positional Arguments:</text><text class="terminal-4094392876-r2" x="793" y="117.6" textLength="12.2" clip-path="url(#terminal-4094392876-line-4)">
-</text><text class="terminal-4094392876-r4" x="24.4" y="142" textLength="85.4" clip-path="url(#terminal-4094392876-line-5)">COMMAND</text><text class="terminal-4094392876-r2" x="793" y="142" textLength="12.2" clip-path="url(#terminal-4094392876-line-5)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="166.4" textLength="73.2" clip-path="url(#terminal-4094392876-line-6)">create</text><text class="terminal-4094392876-r2" x="170.8" y="166.4" textLength="292.8" clip-path="url(#terminal-4094392876-line-6)">Perform create operation</text><text class="terminal-4094392876-r2" x="793" y="166.4" textLength="12.2" clip-path="url(#terminal-4094392876-line-6)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="190.8" textLength="73.2" clip-path="url(#terminal-4094392876-line-7)">delete</text><text class="terminal-4094392876-r2" x="170.8" y="190.8" textLength="292.8" clip-path="url(#terminal-4094392876-line-7)">Perform delete operation</text><text class="terminal-4094392876-r2" x="793" y="190.8" textLength="12.2" clip-path="url(#terminal-4094392876-line-7)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="215.2" textLength="73.2" clip-path="url(#terminal-4094392876-line-8)">export</text><text class="terminal-4094392876-r2" x="170.8" y="215.2" textLength="244" clip-path="url(#terminal-4094392876-line-8)">Export all variables</text><text class="terminal-4094392876-r2" x="793" y="215.2" textLength="12.2" clip-path="url(#terminal-4094392876-line-8)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="239.6" textLength="36.6" clip-path="url(#terminal-4094392876-line-9)">get</text><text class="terminal-4094392876-r2" x="170.8" y="239.6" textLength="256.2" clip-path="url(#terminal-4094392876-line-9)">Perform get operation</text><text class="terminal-4094392876-r2" x="793" y="239.6" textLength="12.2" clip-path="url(#terminal-4094392876-line-9)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="264" textLength="73.2" clip-path="url(#terminal-4094392876-line-10)">import</text><text class="terminal-4094392876-r2" x="170.8" y="264" textLength="195.2" clip-path="url(#terminal-4094392876-line-10)">Import variables</text><text class="terminal-4094392876-r2" x="793" y="264" textLength="12.2" clip-path="url(#terminal-4094392876-line-10)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="288.4" textLength="48.8" clip-path="url(#terminal-4094392876-line-11)">list</text><text class="terminal-4094392876-r2" x="170.8" y="288.4" textLength="268.4" clip-path="url(#terminal-4094392876-line-11)">Perform list operation</text><text class="terminal-4094392876-r2" x="793" y="288.4" textLength="12.2" clip-path="url(#terminal-4094392876-line-11)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="312.8" textLength="73.2" clip-path="url(#terminal-4094392876-line-12)">update</text><text class="terminal-4094392876-r2" x="170.8" y="312.8" textLength="292.8" clip-path="url(#terminal-4094392876-line-12)">Perform update operation</text><text class="terminal-4094392876-r2" x="793" y="312.8" textLength="12.2" clip-path="url(#terminal-4094392876-line-12)">
-</text><text class="terminal-4094392876-r2" x="793" y="337.2" textLength="12.2" clip-path="url(#terminal-4094392876-line-13)">
-</text><text class="terminal-4094392876-r1" x="0" y="361.6" textLength="97.6" clip-path="url(#terminal-4094392876-line-14)">Options:</text><text class="terminal-4094392876-r2" x="793" y="361.6" textLength="12.2" clip-path="url(#terminal-4094392876-line-14)">
-</text><text class="terminal-4094392876-r4" x="24.4" y="386" textLength="24.4" clip-path="url(#terminal-4094392876-line-15)">-h</text><text class="terminal-4094392876-r2" x="48.8" y="386" textLength="24.4" clip-path="url(#terminal-4094392876-line-15)">, </text><text class="terminal-4094392876-r4" x="73.2" y="386" textLength="73.2" clip-path="url(#terminal-4094392876-line-15)">--help</text><text class="terminal-4094392876-r2" x="170.8" y="386" textLength="378.2" clip-path="url(#termi [...]
+ <g class="terminal-938427142-matrix">
+ <text class="terminal-938427142-r1" x="0" y="20" textLength="73.2" clip-path="url(#terminal-938427142-line-0)">Usage:</text><text class="terminal-938427142-r3" x="85.4" y="20" textLength="244" clip-path="url(#terminal-938427142-line-0)">airflowctl variables</text><text class="terminal-938427142-r2" x="329.4" y="20" textLength="24.4" clip-path="url(#terminal-938427142-line-0)"> [</text><text class="terminal-938427142-r4" x="353.8" y="20" textLength="24.4" clip-path="url(#ter [...]
+</text><text class="terminal-938427142-r2" x="915" y="44.4" textLength="12.2" clip-path="url(#terminal-938427142-line-1)">
+</text><text class="terminal-938427142-r2" x="0" y="68.8" textLength="341.6" clip-path="url(#terminal-938427142-line-2)">Perform Variables operations</text><text class="terminal-938427142-r2" x="915" y="68.8" textLength="12.2" clip-path="url(#terminal-938427142-line-2)">
+</text><text class="terminal-938427142-r2" x="915" y="93.2" textLength="12.2" clip-path="url(#terminal-938427142-line-3)">
+</text><text class="terminal-938427142-r1" x="0" y="117.6" textLength="256.2" clip-path="url(#terminal-938427142-line-4)">Positional Arguments:</text><text class="terminal-938427142-r2" x="915" y="117.6" textLength="12.2" clip-path="url(#terminal-938427142-line-4)">
+</text><text class="terminal-938427142-r4" x="24.4" y="142" textLength="85.4" clip-path="url(#terminal-938427142-line-5)">COMMAND</text><text class="terminal-938427142-r2" x="915" y="142" textLength="12.2" clip-path="url(#terminal-938427142-line-5)">
+</text><text class="terminal-938427142-r4" x="48.8" y="166.4" textLength="73.2" clip-path="url(#terminal-938427142-line-6)">create</text><text class="terminal-938427142-r2" x="170.8" y="166.4" textLength="292.8" clip-path="url(#terminal-938427142-line-6)">Perform create operation</text><text class="terminal-938427142-r2" x="915" y="166.4" textLength="12.2" clip-path="url(#terminal-938427142-line-6)">
+</text><text class="terminal-938427142-r4" x="48.8" y="190.8" textLength="73.2" clip-path="url(#terminal-938427142-line-7)">delete</text><text class="terminal-938427142-r2" x="170.8" y="190.8" textLength="292.8" clip-path="url(#terminal-938427142-line-7)">Perform delete operation</text><text class="terminal-938427142-r2" x="915" y="190.8" textLength="12.2" clip-path="url(#terminal-938427142-line-7)">
+</text><text class="terminal-938427142-r4" x="48.8" y="215.2" textLength="36.6" clip-path="url(#terminal-938427142-line-8)">get</text><text class="terminal-938427142-r2" x="170.8" y="215.2" textLength="256.2" clip-path="url(#terminal-938427142-line-8)">Perform get operation</text><text class="terminal-938427142-r2" x="915" y="215.2" textLength="12.2" clip-path="url(#terminal-938427142-line-8)">
+</text><text class="terminal-938427142-r4" x="48.8" y="239.6" textLength="73.2" clip-path="url(#terminal-938427142-line-9)">import</text><text class="terminal-938427142-r2" x="170.8" y="239.6" textLength="646.6" clip-path="url(#terminal-938427142-line-9)">Import variables from a file exported with local CLI.</text><text class="terminal-938427142-r2" x="915" y="239.6" textLength="12.2" clip-path="url(#terminal-938427142-line-9)">
+</text><text class="terminal-938427142-r4" x="48.8" y="264" textLength="48.8" clip-path="url(#terminal-938427142-line-10)">list</text><text class="terminal-938427142-r2" x="170.8" y="264" textLength="268.4" clip-path="url(#terminal-938427142-line-10)">Perform list operation</text><text class="terminal-938427142-r2" x="915" y="264" textLength="12.2" clip-path="url(#terminal-938427142-line-10)">
+</text><text class="terminal-938427142-r4" x="48.8" y="288.4" textLength="73.2" clip-path="url(#terminal-938427142-line-11)">update</text><text class="terminal-938427142-r2" x="170.8" y="288.4" textLength="292.8" clip-path="url(#terminal-938427142-line-11)">Perform update operation</text><text class="terminal-938427142-r2" x="915" y="288.4" textLength="12.2" clip-path="url(#terminal-938427142-line-11)">
+</text><text class="terminal-938427142-r2" x="915" y="312.8" textLength="12.2" clip-path="url(#terminal-938427142-line-12)">
+</text><text class="terminal-938427142-r1" x="0" y="337.2" textLength="97.6" clip-path="url(#terminal-938427142-line-13)">Options:</text><text class="terminal-938427142-r2" x="915" y="337.2" textLength="12.2" clip-path="url(#terminal-938427142-line-13)">
+</text><text class="terminal-938427142-r4" x="24.4" y="361.6" textLength="24.4" clip-path="url(#terminal-938427142-line-14)">-h</text><text class="terminal-938427142-r2" x="48.8" y="361.6" textLength="24.4" clip-path="url(#terminal-938427142-line-14)">, </text><text class="terminal-938427142-r4" x="73.2" y="361.6" textLength="73.2" clip-path="url(#terminal-938427142-line-14)">--help</text><text class="terminal-938427142-r2" x="170.8" y="361.6" textLength="378.2" clip-path="url(#term [...]
</text>
</g>
</g>
diff --git a/airflow-ctl/docs/installation/installing-from-pypi.rst b/airflow-ctl/docs/installation/installing-from-pypi.rst
index 9ec3a5e73ab..cecd66c354e 100644
--- a/airflow-ctl/docs/installation/installing-from-pypi.rst
+++ b/airflow-ctl/docs/installation/installing-from-pypi.rst
@@ -24,7 +24,7 @@ PyPI <https://pypi.org/project/apache-airflow-ctl/>`__.
Installation tools
''''''''''''''''''
-Only ``pip`` and ``uv`` installation is currently officially supported.
+Only ``pip`` installation is currently officially supported.
.. note::
@@ -33,8 +33,7 @@ Only ``pip`` and ``uv`` installation is currently officially supported.
``pip`` - especially when it comes to constraint vs. requirements management.
Installing via ``Poetry`` or ``pip-tools`` is not currently supported. If you wish to install airflow
using those tools you should use the constraints and convert them to appropriate
- format and workflow that your tool requires. Uv follows ``pip`` approach
- with ``uv pip`` so it should work similarly.
+ format and workflow that your tool requires.
There are known issues with ``bazel`` that might lead to circular dependencies when using it to install
Airflow. Please switch to ``pip`` if you encounter such problems. ``Bazel`` community works on fixing
diff --git a/airflow-ctl/docs/installation/installing-from-sources.rst b/airflow-ctl/docs/installation/installing-from-sources.rst
index 2d2f9a421f5..ad1e616873d 100644
--- a/airflow-ctl/docs/installation/installing-from-sources.rst
+++ b/airflow-ctl/docs/installation/installing-from-sources.rst
@@ -23,8 +23,9 @@ Released packages
.. jinja:: official_download_page
- This page describes downloading and verifying Airflow Ctl version ``|version|`` using officially released packages.
- You can also install ``airflowctl`` - as most Python packages - via :doc:`PyPI <installing-from-pypi>`.
+ This page describes downloading and verifying Airflow® version
+ ``{{ airflow_version }}`` using officially released packages.
+ You can also install ``Apache airflowctl`` - as most Python packages - via :doc:`PyPI <installing-from-pypi>`.
You can choose different version of Airflow by selecting a different version from the drop-down at
the top-left of the page.
@@ -33,13 +34,22 @@ can use if you want to verify the origin of the packages and want to verify chec
the packages. The packages are available via the
`Official Apache Software Foundations Downloads <https://dlcdn.apache.org/>`_
-The {{ airflowctl_version }} downloads of Airflow Ctl are available at:
+As of version 2.8 Airflow follows PEP 517/518 and uses ``pyproject.toml`` file to define build dependencies
+and build process and it requires relatively modern versions of packaging tools to get airflow built from
+local sources or ``sdist`` packages, as PEP 517 compliant build hooks are used to determine dynamic build
+dependencies. In case of ``pip`` it means that at least version 22.1.0 is needed (released at the beginning of
+2022) to build or install Airflow from sources. This does not affect the ability of installing Airflow from
+released wheel packages.
+
+The |version| downloads of airflowctl are available at:
.. jinja:: official_download_page
- * `Sources package for airflow-ctl: <{{ closer_lua_url }}/apache_airflow_ctl-{{ airflowctl_version }}-source.tar.gz>`__ (`asc <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-source.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-source.tar.gz.sha512>`__)
- * `Sdist package for airflow-ctl distributions <{{ closer_lua_url }}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
- * `Whl package for airflow-ctl distribution <{{ closer_lua_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.sha512>`__)
+ * `Sources package for airflow <{{ closer_lua_url }}/apache-airflow-ctl-{{ airflowctl_version }}-source.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow-ctl-{{ airflowctl_version }}-source.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow-ctl-{{ airflowctl_version }}-source.tar.gz.sha512>`__)
+ * `Sdist package for airflow meta package <{{ closer_lua_url }}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
+ * `Whl package for airflow meta package <{{ closer_lua_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.sha512>`__)
+ * `Sdist package for airflow core package <{{ closer_lua_url }}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url }}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ base_url }}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
+ * `Whl package for airflow core package <{{ closer_lua_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl.sha512>`__)
If you want to install from the source code, you can download from the sources link above, it will contain
a ``INSTALL`` file containing details on how you can build and install airflowctl.
diff --git a/airflow-ctl/newsfragments/59850.significant.rst b/airflow-ctl/newsfragments/59850.significant.rst
new file mode 100644
index 00000000000..dfb10ae852c
--- /dev/null
+++ b/airflow-ctl/newsfragments/59850.significant.rst
@@ -0,0 +1,30 @@
+Remove ``airflowctl variables export`` command
+
+The ``airflowctl variables export`` command has been removed from the remote CLI. Variable export functionality is only available through the local Airflow CLI.
+
+**What changed:**
+
+- Removed ``airflowctl variables export`` command
+
+**Migration:**
+
+Use the local Airflow CLI command for exporting variables:
+
+.. code-block:: bash
+
+ # Old (no longer available)
+ airflowctl variables export variables.json
+
+ # New (use local CLI)
+ airflow variables export variables.json
+
+* Types of change
+
+ * [ ] Dag changes
+ * [ ] Config changes
+ * [ ] API changes
+ * [x] CLI changes
+ * [ ] Behaviour changes
+ * [ ] Plugin changes
+ * [ ] Dependency changes
+ * [ ] Code interface changes
diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml
index 8c2fe12a7c5..bbfeffcd72d 100644
--- a/airflow-ctl/pyproject.toml
+++ b/airflow-ctl/pyproject.toml
@@ -20,8 +20,6 @@ name = "apache-airflow-ctl"
dynamic = ["version"]
description = "Apache Airflow command line tool for communicating with an Apache Airflow, using the API."
readme = { file = "README.md", content-type = "text/markdown" }
-license = "Apache-2.0"
-license-files = ["LICENSE", "NOTICE"]
# We do not want to upper-bind Python version, as we do not know if we will support Python 3.14+
# out-of-the box. Airflow-ctl is a small tool that does not have many dependencies and does not use
# sophisticated features of Python, so it should work with Python 3.14+ once all it's dependencies are
@@ -46,20 +44,6 @@ classifiers = [
"Framework :: Apache Airflow",
]
-[project.urls]
-"Bug Tracker" = "https://github.com/apache/airflow/issues"
-Documentation = "https://airflow.apache.org/docs/apache-airflow-ctl/stable/index.html"
-Downloads = "https://archive.apache.org/dist/airflow/airflow-ctl/"
-Homepage = "https://airflow.apache.org/"
-"Release Notes" = "https://airflow.apache.org/docs/apache-airflow-ctl/stable/changelog.html"
-"Slack Chat" = "https://s.apache.org/airflow-slack"
-"Source Code" = "https://github.com/apache/airflow"
-LinkedIn = "https://www.linkedin.com/company/apache-airflow/"
-Mastodon = "https://fosstodon.org/@airflow"
-Bluesky = "https://bsky.app/profile/apache-airflow.bsky.social"
-YouTube = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/"
-
-
[project.optional-dependencies]
"dev" = [
"keyrings.alt>=5.0.2",
@@ -163,7 +147,7 @@ input = "../airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-ge
output = "src/airflowctl/api/datamodels/generated.py"
## pytest settings ##
-[tool.pytest]
+[tool.pytest.ini_options]
addopts = [
"--tb=short",
"-rasl",
@@ -196,7 +180,7 @@ testpaths = [
]
asyncio_default_fixture_loop_scope = "function"
-pythonpath = ["tests"]
+pythonpath = "tests"
# Keep temporary directories (created by `tmp_path`) for 2 recent runs only failed tests.
tmp_path_retention_count = "2"
diff --git a/airflow-ctl/src/airflowctl/__init__.py b/airflow-ctl/src/airflowctl/__init__.py
index 32a64efabc5..f3fd163907d 100644
--- a/airflow-ctl/src/airflowctl/__init__.py
+++ b/airflow-ctl/src/airflowctl/__init__.py
@@ -19,4 +19,4 @@ from __future__ import annotations
__path__ = __import__("pkgutil").extend_path(__path__, __name__)
-__version__ = "0.1.0"
+__version__ = "0.1.1"
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index da548530eb0..a578396acd4 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -74,13 +74,11 @@ def safe_call_command(function: Callable, args: Iterable[Arg]) -> None:
try:
function(args)
- except AirflowCtlCredentialNotFoundException as e:
- rich.print(f"command failed due to {e}")
- sys.exit(1)
- except AirflowCtlConnectionException as e:
- rich.print(f"command failed due to {e}")
- sys.exit(1)
- except AirflowCtlNotFoundException as e:
+ except (
+ AirflowCtlCredentialNotFoundException,
+ AirflowCtlConnectionException,
+ AirflowCtlNotFoundException,
+ ) as e:
rich.print(f"command failed due to {e}")
sys.exit(1)
except (httpx.RemoteProtocolError, httpx.ReadError) as e:
@@ -202,8 +200,7 @@ class Password(argparse.Action):
ARG_FILE = Arg(
flags=("file",),
metavar="FILEPATH",
- help="File path to read from or write to. "
- "For import commands, it is a file to read from. For export commands, it is a file to write to.",
+ help="File path to read from for import commands.",
)
ARG_OUTPUT = Arg(
(
@@ -254,9 +251,8 @@ ARG_AUTH_PASSWORD = Arg(
# Dag Commands Args
ARG_DAG_ID = Arg(
- flags=("--dag-id",),
+ flags=("dag_id",),
type=str,
- dest="dag_id",
help="The DAG ID of the DAG to pause or unpause",
)
@@ -804,9 +800,7 @@ CONFIG_COMMANDS = (
CONNECTION_COMMANDS = (
ActionCommand(
name="import",
- help="Import connections from a file. "
- "This feature is compatible with airflow CLI `airflow connections export a.json` command. "
- "Export it from `airflow CLI` and import it securely via this command.",
+ help="Import connections from a file exported with local CLI.",
func=lazy_load_command("airflowctl.ctl.commands.connection_command.import_"),
args=(Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON file"),),
),
@@ -854,16 +848,10 @@ POOL_COMMANDS = (
VARIABLE_COMMANDS = (
ActionCommand(
name="import",
- help="Import variables",
+ help="Import variables from a file exported with local CLI.",
func=lazy_load_command("airflowctl.ctl.commands.variable_command.import_"),
args=(ARG_FILE, ARG_VARIABLE_ACTION_ON_EXISTING_KEY),
),
- ActionCommand(
- name="export",
- help="Export all variables",
- func=lazy_load_command("airflowctl.ctl.commands.variable_command.export"),
- args=(ARG_FILE,),
- ),
)
core_commands: list[CLICommand] = [
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py b/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
index d5ffed31d8c..466be2ccac7 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
@@ -19,7 +19,6 @@ from __future__ import annotations
import json
import os
import sys
-from pathlib import Path
import rich
@@ -79,24 +78,3 @@ def import_(args, api_client=NEW_API_CLIENT) -> list[str]:
rich.print(success_message.format(success=result.create.success))
return result.create.success
-
-
-@provide_api_client(kind=ClientKind.CLI)
-def export(args, api_client=NEW_API_CLIENT) -> None:
- """Export all the variables to the file."""
- success_message = "[green]Export successful! {total_entries} variable(s) to {file}[/green]"
- var_dict = {}
- variables = api_client.variables.list()
-
- for variable in variables.variables:
- if variable.description:
- var_dict[variable.key] = {
- "value": variable.value,
- "description": variable.description,
- }
- else:
- var_dict[variable.key] = variable.value
-
- with open(Path(args.file), "w") as var_file:
- json.dump(var_dict, var_file, sort_keys=True, indent=4)
- rich.print(success_message.format(total_entries=variables.total_entries, file=args.file))
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index e2adcadff5e..39ffc436706 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -87,7 +87,7 @@ class TestClient:
assert err.value.args == ("Client error message: {'detail': 'Not found'}",)
@pytest.mark.parametrize(
- "base_url, client_kind, expected_base_url",
+ ("base_url", "client_kind", "expected_base_url"),
[
("http://localhost:8080", ClientKind.CLI, "http://localhost:8080/api/v2/"),
("http://localhost:8080", ClientKind.AUTH, "http://localhost:8080/auth/"),
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 1e7391b99d2..0f92aedb3a3 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -128,7 +128,7 @@ class TestBaseOperations:
client.connections.get("1")
@pytest.mark.parametrize(
- "total_entries, limit, expected_response",
+ ("total_entries", "limit", "expected_response"),
[
(1, 50, (HelloCollectionResponse(hellos=[HelloResponse(name="hello")], total_entries=1))),
(
@@ -1066,7 +1066,7 @@ class TestPoolsOperations:
pools=[pool_response],
total_entries=1,
)
- pool_bulk_aresponse = BulkResponse(
+ pool_bulk_response = BulkResponse(
create=BulkActionResponse(success=[pool_name], errors=[]),
update=None,
delete=None,
@@ -1102,11 +1102,11 @@ class TestPoolsOperations:
def test_bulk(self):
def handle_request(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/v2/pools"
- return httpx.Response(200, json=json.loads(self.pool_bulk_aresponse.model_dump_json()))
+ return httpx.Response(200, json=json.loads(self.pool_bulk_response.model_dump_json()))
client = make_api_client(transport=httpx.MockTransport(handle_request))
response = client.pools.bulk(pools=self.pools_bulk_body)
- assert response == self.pool_bulk_aresponse
+ assert response == self.pool_bulk_response
def test_delete(self):
def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
index 98fb318024b..fbc6ee1240b 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
@@ -93,7 +93,7 @@ class TestDagCommands:
)
assert self.dag_response_paused.is_paused is False
dag_response_dict = dag_command.pause(
- self.parser.parse_args(["dags", "pause", "--dag-id", self.dag_id]),
+ self.parser.parse_args(["dags", "pause", self.dag_id]),
api_client=api_client,
)
assert dag_response_dict["is_paused"] is False
@@ -107,7 +107,7 @@ class TestDagCommands:
)
with pytest.raises(SystemExit):
dag_command.pause(
- self.parser.parse_args(["dags", "pause", "--dag-id", self.dag_id]),
+ self.parser.parse_args(["dags", "pause", self.dag_id]),
api_client=api_client,
)
@@ -120,7 +120,7 @@ class TestDagCommands:
)
assert self.dag_response_unpaused.is_paused is True
dag_response_dict = dag_command.unpause(
- self.parser.parse_args(["dags", "unpause", "--dag-id", self.dag_id]),
+ self.parser.parse_args(["dags", "unpause", self.dag_id]),
api_client=api_client,
)
assert dag_response_dict["is_paused"] is True
@@ -134,6 +134,6 @@ class TestDagCommands:
)
with pytest.raises(SystemExit):
dag_command.unpause(
- self.parser.parse_args(["dags", "unpause", "--dag-id", self.dag_id]),
+ self.parser.parse_args(["dags", "unpause", self.dag_id]),
api_client=api_client,
)
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
index e0c9ce16d8d..9703c8f866b 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
@@ -17,7 +17,6 @@
from __future__ import annotations
import json
-import os
import pytest
@@ -104,22 +103,3 @@ class TestCliVariableCommands:
self.parser.parse_args(["variables", "import", expected_json_path.as_posix()]),
api_client=api_client,
)
-
- def test_export(self, api_client_maker, tmp_path, monkeypatch):
- api_client = api_client_maker(
- path="/api/v2/variables",
- response_json=self.variable_collection_response.model_dump(),
- expected_http_status_code=200,
- kind=ClientKind.CLI,
- )
-
- monkeypatch.chdir(tmp_path)
- expected_json_path = (tmp_path / self.export_file_name).as_posix()
- variable_command.export(
- self.parser.parse_args(["variables", "export", expected_json_path]),
- api_client=api_client,
- )
- assert os.path.exists(tmp_path / self.export_file_name)
-
- with open(expected_json_path) as f:
- assert json.load(f) == {self.key: {"description": self.description, "value": self.value}}
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py b/scripts/ci/prek/check_airflowctl_command_coverage.py
new file mode 100755
index 00000000000..8a81dd098c6
--- /dev/null
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -0,0 +1,150 @@
+#!/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 = [
+# "rich>=13.6.0",
+# ]
+# ///
+"""
+Check that all airflowctl CLI commands have integration test coverage by comparing commands from operations.py against test_commands in conftest.py.
+"""
+
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
+
+OPERATIONS_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl" / "src" / "airflowctl" / "api" / "operations.py"
+CTL_TESTS_FILE = (
+ AIRFLOW_ROOT_PATH / "airflow-ctl-tests" / "tests" / "airflowctl_tests" / "test_airflowctl_commands.py"
+)
+
+# Operations excluded from CLI (see cli_config.py)
+EXCLUDED_OPERATION_CLASSES = {"BaseOperations", "LoginOperations", "VersionOperations"}
+EXCLUDED_METHODS = {
+ "__init__",
+ "__init_subclass__",
+ "error",
+ "_check_flag_and_exit_if_server_response_error",
+ "bulk",
+ "export",
+}
+
+EXCLUDED_COMMANDS = {
+ "assets delete-dag-queued-events",
+ "assets delete-queued-event",
+ "assets delete-queued-events",
+ "assets get-by-alias",
+ "assets get-dag-queued-event",
+ "assets get-dag-queued-events",
+ "assets get-queued-events",
+ "assets list-by-alias",
+ "assets materialize",
+ "backfill cancel",
+ "backfill create",
+ "backfill create-dry-run",
+ "backfill get",
+ "backfill pause",
+ "backfill unpause",
+ "connections create-defaults",
+ "connections test",
+ "dags delete",
+ "dags get-import-error",
+ "dags get-tags",
+}
+
+
+def parse_operations() -> dict[str, list[str]]:
+ commands: dict[str, list[str]] = {}
+
+ with open(OPERATIONS_FILE) as f:
+ tree = ast.parse(f.read(), filename=str(OPERATIONS_FILE))
+
+ for node in ast.walk(tree):
+ if isinstance(node, ast.ClassDef) and node.name.endswith("Operations"):
+ if node.name in EXCLUDED_OPERATION_CLASSES:
+ continue
+
+ group_name = node.name.replace("Operations", "").lower()
+ commands[group_name] = []
+
+ for child in node.body:
+ if isinstance(child, ast.FunctionDef):
+ method_name = child.name
+ if method_name in EXCLUDED_METHODS or method_name.startswith("_"):
+ continue
+ subcommand = method_name.replace("_", "-")
+ commands[group_name].append(subcommand)
+
+ return commands
+
+
+def parse_tested_commands() -> set[str]:
+ tested: set[str] = set()
+
+ with open(CTL_TESTS_FILE) as f:
+ content = f.read()
+
+ # Match command patterns like "assets list", "dags list-import-errors", etc.
+ # Also handles f-strings like f"dagrun get..." or f'dagrun get...'
+ pattern = r'f?["\']([a-z]+(?:-[a-z]+)*\s+[a-z]+(?:-[a-z]+)*)'
+ for match in re.findall(pattern, content):
+ parts = match.split()
+ if len(parts) >= 2:
+ tested.add(f"{parts[0]} {parts[1]}")
+
+ return tested
+
+
+def main():
+ available = parse_operations()
+ tested = parse_tested_commands()
+
+ missing = []
+ for group, subcommands in sorted(available.items()):
+ for subcommand in sorted(subcommands):
+ cmd = f"{group} {subcommand}"
+ if cmd not in tested and cmd not in EXCLUDED_COMMANDS:
+ missing.append(cmd)
+
+ if missing:
+ console.print("[red]ERROR: Commands not covered by integration tests:[/]")
+ for cmd in missing:
+ console.print(f" [red]- {cmd}[/]")
+ console.print()
+ console.print("[yellow]Fix by either:[/]")
+ console.print(f"1. Add test to {CTL_TESTS_FILE}")
+ console.print(f"2. Add to EXCLUDED_COMMANDS in {__file__}")
+ sys.exit(1)
+
+ total = sum(len(cmds) for cmds in available.values())
+ console.print(
+ f"[green]All {total} CLI commands covered ({len(tested)} tested, {len(EXCLUDED_COMMANDS)} excluded)[/]"
+ )
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/in_container/run_capture_airflowctl_help.py b/scripts/in_container/run_capture_airflowctl_help.py
index 733cc3145b2..b35472bffca 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -84,7 +84,7 @@ def regenerate_help_images_for_all_airflowctl_commands(commands: list[str], skip
os.makedirs(AIRFLOWCTL_IMAGES_PATH, exist_ok=True)
env = os.environ.copy()
env["TERM"] = "xterm-256color"
- env["COLUMNS"] = "65"
+ env["COLUMNS"] = "75"
old_hash_dict = {}
new_hash_dict = {}
@@ -100,8 +100,8 @@ def regenerate_help_images_for_all_airflowctl_commands(commands: list[str], skip
# Check for changes
changed_commands = []
- for command in commands:
- command = command or "main"
+ for command_raw in commands:
+ command = command_raw or "main"
console.print(f"[bright_blue]Checking command: {command}[/]", end="")
if skip_hash_check: