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",