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

shahar1 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this 
push:
     new 073142e561e [airflow-ctl/v0-1-test] Keep airflowctl dags 
pause/unpause/drain output parseable (#76133) (#76172)
073142e561e is described below

commit 073142e561ee79c120592f67e7ecb1d687e10b05
Author: Shahar Epstein <[email protected]>
AuthorDate: Sat Oct 31 22:57:18 2026 +0200

    [airflow-ctl/v0-1-test] Keep airflowctl dags pause/unpause/drain output 
parseable (#76133) (#76172)
    
    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.
    
    Backport note: this branch has no `dags drain`, so the drain cases of the 
new tests were dropped; the source change is identical to main.
    
    (cherry picked from commit 956d8cd2d6b19254d86212112e786041439d2a24)
    
    Co-authored-by: Shawn Sun <[email protected]>
---
 .../src/airflowctl/ctl/commands/dag_command.py     |  7 ++--
 .../airflow_ctl/ctl/commands/test_dag_command.py   | 47 ++++++++++++++++++++++
 2 files changed, 51 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 d2acee3d24a..3160277fad3 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
@@ -48,12 +48,13 @@ def update_dag_state(
             dag_id=dag_id, dag_body=DAGPatchBody(is_paused=operation == 
"pause")
         )
     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 62c402afb6c..6153db29677 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 unittest import mock
 
 import httpx
@@ -187,6 +188,52 @@ class TestDagCommands:
                 api_client=api_client,
             )
 
+    @pytest.mark.parametrize(
+        ("operation", "response_attr"),
+        [
+            ("pause", "dag_response_paused"),
+            ("unpause", "dag_response_unpaused"),
+        ],
+    )
+    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"])
+    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