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 d07cb5f727d Fix airflowctl backfill create-dry-run printing one
all-null row (#76224)
d07cb5f727d is described below
commit d07cb5f727d504218f9ce354e00534a1680ce096
Author: Y-C <[email protected]>
AuthorDate: Thu Nov 5 01:48:50 2026 +0800
Fix airflowctl backfill create-dry-run printing one all-null row (#76224)
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.
---
.../airflowctl_tests/test_airflowctl_commands.py | 18 ++++++++++++++++
airflow-ctl/src/airflowctl/api/operations.py | 7 +++++--
.../tests/airflow_ctl/api/test_operations.py | 24 ++++++++++++++++++----
.../ci/prek/check_airflowctl_command_coverage.py | 1 -
4 files changed, 43 insertions(+), 7 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 fef33b01081..e7eda083307 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
@@ -262,6 +264,22 @@ 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,
+ )
+ # A server older than 3.2 omits the partition fields, and that warning
precedes the JSON.
+ 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 cec290720eb..342c61ee0ea 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -66,6 +66,7 @@ from airflowctl.api.datamodels.generated import (
DAGVersionCollectionResponse,
DagVersionResponse,
DAGWarningCollectionResponse,
+ DryRunBackfillCollectionResponse,
ImportErrorCollectionResponse,
ImportErrorResponse,
JobCollectionResponse,
@@ -490,13 +491,15 @@ class BackfillOperations(BaseOperations):
)
return validate_response(self.response.content, BackfillResponse)
- 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,
exclude_defaults=True),
)
- return validate_response(self.response.content, BackfillResponse)
+ return validate_response(self.response.content,
DryRunBackfillCollectionResponse)
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 670e1876007..0a963bbd61b 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -80,6 +80,8 @@ from airflowctl.api.datamodels.generated import (
DAGWarningCollectionResponse,
DAGWarningResponse,
DagWarningType,
+ DryRunBackfillCollectionResponse,
+ DryRunBackfillResponse,
ImportErrorCollectionResponse,
ImportErrorResponse,
JobCollectionResponse,
@@ -772,15 +774,29 @@ 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,
+ )
@pytest.mark.parametrize(
- ("operation", "path"), [("create", "backfills"), ("create_dry_run",
"backfills/dry_run")]
+ ("operation", "path", "server_response"),
+ [
+ ("create", "backfills", backfill_response),
+ ("create_dry_run", "backfills/dry_run", dry_run_response),
+ ],
)
@pytest.mark.parametrize(
("request_fields", "expected_fields"),
[({}, {}), ({"drain_dag": False}, {}), ({"drain_dag": True},
{"drain_dag": True})],
)
- def test_create(self, operation, path, request_fields, expected_fields):
+ def test_create(self, operation, path, server_response, request_fields,
expected_fields):
expected_body = {
"dag_id": "dag_id",
"from_date": "2024-12-31T23:59:59",
@@ -795,13 +811,13 @@ class TestBackfillOperations:
assert request.url.path == f"/api/v2/{path}"
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(server_response.model_dump_json()))
client = make_api_client(transport=httpx.MockTransport(handle_request))
response = getattr(client.backfills, operation)(
backfill=self.backfill_body.model_copy(update=request_fields)
)
- assert response == self.backfill_response
+ assert response == server_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 3e3be605f65..a0473158282 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",