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 3a7d6a4f10b [airflow-ctl/v0-1-test] Fix airflowctl backfill 
create-dry-run printing one all-null row (#76224) (#76478)
3a7d6a4f10b is described below

commit 3a7d6a4f10b69775e276a82d3e050a4d23a57d46
Author: Shahar Epstein <[email protected]>
AuthorDate: Wed Nov 4 20:19:34 2026 +0200

    [airflow-ctl/v0-1-test] Fix airflowctl backfill create-dry-run printing one 
all-null row (#76224) (#76478)
    
    The dry-run endpoint returns the runs a backfill would create, not a
    backfill. Parsing that payload as a backfill dropped every row, and the
    missing-field fallback for older servers turned the mismatch into a
    single all-null row plus a misleading "older Airflow line" warning.
    
    (cherry picked from commit d07cb5f727d504218f9ce354e00534a1680ce096)
    
    Conflict resolution: this branch has no validate_response helper 
(main-only),
    so create_dry_run uses model_validate_json like its neighbours. The test 
split
    is kept because main's parametrized test_create depends on main-only
    exclude_defaults behaviour; only test_create_dry_run changes. Dropped the 
e2e
    comment about a pre-3.2 warning, which this branch does not emit on this 
path.
    
    On this branch the symptom differs: with no missing-field fallback, the
    command exited 1 with a "Response did not match the airflowctl model
    BackfillResponse" error instead of printing a null row.
    
    Claude-Session: https://claude.ai/code/session_01BVx6g9nLEf9mMXrtSEjLR7
    
    Co-authored-by: Y-C <[email protected]>
---
 .../tests/airflowctl_tests/test_airflowctl_commands.py  | 17 +++++++++++++++++
 airflow-ctl/src/airflowctl/api/operations.py            |  7 +++++--
 airflow-ctl/tests/airflow_ctl/api/test_operations.py    | 16 ++++++++++++++--
 scripts/ci/prek/check_airflowctl_command_coverage.py    |  1 -
 4 files changed, 36 insertions(+), 5 deletions(-)

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 aa3da0d8140..926694f0fb3 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -16,6 +16,8 @@
 # under the License.
 from __future__ import annotations
 
+import json
+
 import pytest
 from packaging.version import Version
 
@@ -243,6 +245,21 @@ def test_airflowctl_commands_skip_keyring(
     )
 
 
+def test_backfill_create_dry_run_lists_the_runs_it_would_create(api_token, 
run_command, tmp_path):
+    # 2024 stays clear of the runs triggered above, which a dry run would 
leave out.
+    output = run_command(
+        command="backfill create-dry-run --dag-id example_bash_operator "
+        "--from-date 2024-01-01T00:00:00+00:00 --to-date 
2024-01-03T00:00:00+00:00 -o json",
+        env_vars={"AIRFLOW_HOME": str(tmp_path), "AIRFLOW_CLI_TOKEN": 
api_token},
+        skip_login=True,
+    )
+    assert [row["logical_date"] for row in 
json.loads(output.splitlines()[-1])] == [
+        "2024-01-01T00:00:00Z",
+        "2024-01-02T00:00:00Z",
+        "2024-01-03T00:00:00Z",
+    ]
+
+
 @pytest.mark.parametrize("command", NO_AUTH_TEST_COMMANDS)
 def test_airflowctl_no_auth_commands(command: str, run_command, tmp_path):
     """Test airflowctl no-auth commands without login or persisted 
credentials."""
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index 09fa2544229..3cfab165827 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -63,6 +63,7 @@ from airflowctl.api.datamodels.generated import (
     DAGVersionCollectionResponse,
     DagVersionResponse,
     DAGWarningCollectionResponse,
+    DryRunBackfillCollectionResponse,
     ImportErrorCollectionResponse,
     ImportErrorResponse,
     JobCollectionResponse,
@@ -415,12 +416,14 @@ class BackfillOperations(BaseOperations):
         )
         return BackfillResponse.model_validate_json(self.response.content)
 
-    def create_dry_run(self, backfill: BackfillPostBody) -> BackfillResponse | 
ServerResponseError:
+    def create_dry_run(
+        self, backfill: BackfillPostBody
+    ) -> DryRunBackfillCollectionResponse | ServerResponseError:
         """Create a dry run backfill."""
         self.response = self.client.post(
             "backfills/dry_run", json=backfill.model_dump(mode="json", 
exclude_none=True)
         )
-        return BackfillResponse.model_validate_json(self.response.content)
+        return 
DryRunBackfillCollectionResponse.model_validate_json(self.response.content)
 
     def get(self, backfill_id: str) -> BackfillResponse | ServerResponseError:
         """Get a backfill."""
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 1621bd015dc..f29ef445270 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -81,6 +81,8 @@ from airflowctl.api.datamodels.generated import (
     DAGWarningCollectionResponse,
     DAGWarningResponse,
     DagWarningType,
+    DryRunBackfillCollectionResponse,
+    DryRunBackfillResponse,
     ImportErrorCollectionResponse,
     ImportErrorResponse,
     JobCollectionResponse,
@@ -740,6 +742,16 @@ class TestBackfillOperations:
         backfills=[backfill_response],
         total_entries=1,
     )
+    dry_run_response = DryRunBackfillCollectionResponse(
+        backfills=[
+            DryRunBackfillResponse(
+                logical_date=datetime.datetime(2025, 1, 1, 0, 0, 0),
+                partition_key=None,
+                partition_date=None,
+            )
+        ],
+        total_entries=1,
+    )
 
     def test_create(self):
         expected_body = self.backfill_body.model_dump(mode="json", 
exclude_none=True)
@@ -761,11 +773,11 @@ class TestBackfillOperations:
             assert request.url.path == "/api/v2/backfills/dry_run"
             assert request.headers.get("content-type", 
"").startswith("application/json")
             assert json.loads(request.content.decode()) == expected_body
-            return httpx.Response(200, 
json=json.loads(self.backfill_response.model_dump_json()))
+            return httpx.Response(200, 
json=json.loads(self.dry_run_response.model_dump_json()))
 
         client = make_api_client(transport=httpx.MockTransport(handle_request))
         response = client.backfills.create_dry_run(backfill=self.backfill_body)
-        assert response == self.backfill_response
+        assert response == self.dry_run_response
 
     def test_get(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py 
b/scripts/ci/prek/check_airflowctl_command_coverage.py
index 6ee54d369bb..c98ed756e7c 100755
--- a/scripts/ci/prek/check_airflowctl_command_coverage.py
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -61,7 +61,6 @@ EXCLUDED_COMMANDS = {
     "assets materialize",
     "backfill cancel",
     "backfill create",
-    "backfill create-dry-run",
     "backfill get",
     "backfill pause",
     "backfill unpause",

Reply via email to