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

henry3260 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 e3671dd24e1 Keep Dag run execution API endpoints working for older 
Task SDK clients (#70138)
e3671dd24e1 is described below

commit e3671dd24e10d4962ae037f75b7c7eb10e0d6967
Author: Henry Chen <[email protected]>
AuthorDate: Tue Aug 11 21:40:15 2026 +0800

    Keep Dag run execution API endpoints working for older Task SDK clients 
(#70138)
    
    * Keep Dag run execution API endpoints working for older Task SDK clients
    
    * Cover populated partition_date in Dag run version tests
    
    A converter that only dropped the field when it was null would have
    passed the previous fixtures, while still breaking 2026-04-06 clients
    on partitioned Dags. Setting a real value on one run makes both
    endpoint tests prove the field is stripped, not merely absent.
    
    * Rename team_name converter to say it handles direct DagRun responses
    
    Without the _response suffix the name reads like the nested dag_run
    converter, which is what remove_partition_date_from_dag_run further
    down actually is; matching the partition_date naming keeps the two
    converter kinds distinguishable at a glance.
---
 .../execution_api/versions/v2026_06_30.py          | 27 +++++++
 .../versions/v2026_06_30/test_dag_runs.py          | 90 ++++++++++++++++++++++
 2 files changed, 117 insertions(+)

diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_06_30.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_06_30.py
index 930bd1426a4..0a316810691 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_06_30.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_06_30.py
@@ -99,6 +99,20 @@ class AddTeamNameField(VersionChange):
         if "dag_run" in response.body and isinstance(response.body["dag_run"], 
dict):
             response.body["dag_run"].pop("team_name", None)
 
+    @convert_response_to_previous_version_for(DagRun)  # type: ignore[arg-type]
+    def remove_team_name_from_dag_run_response(response: ResponseInfo) -> 
None:  # type: ignore[misc]
+        """Remove the ``team_name`` field from responses returning a DagRun 
directly."""
+        if isinstance(response.body, dict):
+            response.body.pop("team_name", None)
+
+    # Schema-based converters are matched against the route's response model 
by identity, so a
+    # route annotated ``DagRun | None`` never matches ``DagRun`` and has to be 
addressed by path.
+    @convert_response_to_previous_version_for("/dag-runs/previous", ["GET"])  
# type: ignore[arg-type]
+    def remove_team_name_from_previous_dag_run(response: ResponseInfo) -> 
None:  # type: ignore[misc]
+        """Remove the ``team_name`` field from the previous-run response."""
+        if isinstance(response.body, dict):
+            response.body.pop("team_name", None)
+
 
 class AddAssetsByAliasEndpoint(VersionChange):
     """Add endpoint to resolve assets from an AssetAlias."""
@@ -141,3 +155,16 @@ class AddPartitionDateField(VersionChange):
         """Strip ``partition_date`` from the nested ``dag_run`` payload for 
older clients."""
         if "dag_run" in response.body and isinstance(response.body["dag_run"], 
dict):
             response.body["dag_run"].pop("partition_date", None)
+
+    @convert_response_to_previous_version_for(DagRun)  # type: ignore[arg-type]
+    def remove_partition_date_from_dag_run_response(response: ResponseInfo) -> 
None:  # type: ignore[misc]
+        """Strip ``partition_date`` from responses returning a DagRun 
directly."""
+        if isinstance(response.body, dict):
+            response.body.pop("partition_date", None)
+
+    # See remove_team_name_from_previous_dag_run: ``DagRun | None`` needs a 
path-based converter.
+    @convert_response_to_previous_version_for("/dag-runs/previous", ["GET"])  
# type: ignore[arg-type]
+    def remove_partition_date_from_previous_dag_run(response: ResponseInfo) -> 
None:  # type: ignore[misc]
+        """Strip ``partition_date`` from the previous-run response."""
+        if isinstance(response.body, dict):
+            response.body.pop("partition_date", None)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_dag_runs.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_dag_runs.py
new file mode 100644
index 00000000000..d89c585d364
--- /dev/null
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_dag_runs.py
@@ -0,0 +1,90 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import pytest
+
+from airflow._shared.timezones import timezone
+from airflow.utils.state import DagRunState
+
+pytestmark = pytest.mark.db_test
+
+ADDED_IN_2026_06_30 = frozenset({"team_name", "partition_date"})
+
+
[email protected]
+def old_ver_client(client):
+    """Last released execution API before ``team_name`` and ``partition_date`` 
were added."""
+    client.headers["Airflow-API-Version"] = "2026-04-06"
+    return client
+
+
[email protected]
+def dag_runs(session, dag_maker):
+    with dag_maker(dag_id="test_dag_run_fields", session=session, 
serialized=True):
+        pass
+    run1 = dag_maker.create_dagrun(
+        state=DagRunState.SUCCESS,
+        logical_date=timezone.datetime(2025, 1, 1),
+        run_id="run1",
+    )
+    # A populated value proves the converters strip the field, not just drop a 
null key.
+    run1.partition_date = timezone.datetime(2025, 1, 1)
+    dag_maker.create_dagrun(
+        state=DagRunState.SUCCESS,
+        logical_date=timezone.datetime(2025, 1, 10),
+        run_id="run2",
+    )
+    session.commit()
+
+
[email protected]("dag_runs")
+def test_get_dag_run_omits_new_fields(old_ver_client):
+    response = 
old_ver_client.get("/execution/dag-runs/test_dag_run_fields/run1")
+
+    assert response.status_code == 200
+    assert not ADDED_IN_2026_06_30 & response.json().keys()
+
+
[email protected]("dag_runs")
+def test_get_previous_dag_run_omits_new_fields(old_ver_client):
+    response = old_ver_client.get(
+        "/execution/dag-runs/previous",
+        params={
+            "dag_id": "test_dag_run_fields",
+            "logical_date": timezone.datetime(2025, 1, 10).isoformat(),
+        },
+    )
+
+    assert response.status_code == 200
+    assert response.json()["run_id"] == "run1"
+    assert not ADDED_IN_2026_06_30 & response.json().keys()
+
+
[email protected]("dag_runs")
+def test_get_previous_dag_run_without_a_match(old_ver_client):
+    response = old_ver_client.get(
+        "/execution/dag-runs/previous",
+        params={
+            "dag_id": "test_dag_run_fields",
+            "logical_date": timezone.datetime(2024, 1, 1).isoformat(),
+        },
+    )
+
+    assert response.status_code == 200
+    assert response.json() is None

Reply via email to