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

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


The following commit(s) were added to refs/heads/main by this push:
     new 956d8cd2d6b Keep airflowctl dags pause/unpause/drain output parseable 
(#76133)
956d8cd2d6b is described below

commit 956d8cd2d6b19254d86212112e786041439d2a24
Author: Shawn Sun <[email protected]>
AuthorDate: Sat Oct 31 05:59:22 2026 -0700

    Keep airflowctl dags pause/unpause/drain output parseable (#76133)
    
    The "Dag <operation> successful" and "Further Dag details:" status lines,
    and the error message on failure, were printed to stdout ahead of the
    --output data. That made `airflowctl dags pause <dag_id> -o json` produce
    output that is not valid JSON, so piping it to jq or json.loads failed.
    Status and error text belong on stderr, matching how the CLI already
    reports the ambiguous-write warning, so stdout carries only the requested
    output format.
---
 .../src/airflowctl/ctl/commands/dag_command.py     |  7 ++--
 .../airflow_ctl/ctl/commands/test_dag_command.py   | 48 ++++++++++++++++++++++
 2 files changed, 52 insertions(+), 3 deletions(-)

diff --git a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
index 95770afa81d..2dc051769bd 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
@@ -55,12 +55,13 @@ def update_dag_state(
     try:
         response = api_client.dags.update(dag_id=dag_id, dag_body=dag_body)
     except ServerResponseError as e:
-        rich.print(f"[red]Error while trying to {operation} Dag {dag_id}: 
{e}[/red]")
+        rich.print(f"[red]Error while trying to {operation} Dag {dag_id}: 
{e}[/red]", file=sys.stderr)
         sys.exit(1)
 
     response_dict = response.model_dump()
-    rich.print(f"[green]Dag {operation} successful {dag_id}[/green]")
-    rich.print("[green]Further Dag details:[/green]")
+    # Status lines go to stderr so stdout carries only the --output data (e.g. 
JSON for jq).
+    rich.print(f"[green]Dag {operation} successful {dag_id}[/green]", 
file=sys.stderr)
+    rich.print("[green]Further Dag details:[/green]", file=sys.stderr)
     AirflowConsole().print_as(
         data=[response_dict],
         output=output,
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 a11b83adee2..09f6b3b43d4 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
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import datetime
+import json
 from types import SimpleNamespace
 from unittest import mock
 from unittest.mock import Mock, call, create_autospec
@@ -280,6 +281,53 @@ class TestDagCommands:
                 api_client=api_client,
             )
 
+    @pytest.mark.parametrize(
+        ("operation", "response_attr"),
+        [
+            ("pause", "dag_response_paused"),
+            ("unpause", "dag_response_unpaused"),
+            ("drain", "dag_response_draining"),
+        ],
+    )
+    def test_update_dag_state_json_output_is_parseable(
+        self, operation, response_attr, api_client_maker, capsys
+    ):
+        dag_response = getattr(self, response_attr)
+        api_client = api_client_maker(
+            path=f"/api/v2/dags/{self.dag_id}",
+            response_json=dag_response.model_dump(mode="json"),
+            expected_http_status_code=200,
+            kind=ClientKind.CLI,
+        )
+        getattr(dag_command, operation)(
+            self.parser.parse_args(["dags", operation, self.dag_id, 
"--output", "json"]),
+            api_client=api_client,
+        )
+        captured = capsys.readouterr()
+        assert json.loads(captured.out)[0]["dag_id"] == self.dag_id
+        assert _normalize_rich_output(captured.err) == (
+            f"Dag {operation} successful {self.dag_id} Further Dag details:"
+        )
+
+    @pytest.mark.parametrize("operation", ["pause", "unpause", "drain"])
+    def test_update_dag_state_error_goes_to_stderr(self, operation, 
api_client_maker, capsys):
+        api_client = api_client_maker(
+            path=f"/api/v2/dags/{self.dag_id}",
+            response_json={"detail": "DAG not found"},
+            expected_http_status_code=404,
+            kind=ClientKind.CLI,
+        )
+        with pytest.raises(SystemExit) as exc_info:
+            getattr(dag_command, operation)(
+                self.parser.parse_args(["dags", operation, self.dag_id, 
"--output", "json"]),
+                api_client=api_client,
+            )
+        assert exc_info.value.code == 1
+        captured = capsys.readouterr()
+        error_message = f"Error while trying to {operation} Dag {self.dag_id}"
+        assert error_message in _normalize_rich_output(captured.err)
+        assert error_message not in _normalize_rich_output(captured.out)
+
     def test_next_execution(self, api_client_maker):
         api_client = api_client_maker(
             path=f"/api/v2/dags/{self.dag_id}",

Reply via email to