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

vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new 334ca49afc7 [v3-3-test] Gate asset event partition_key behind 
execution API 2026-06-30 (#72327) (#72827)
334ca49afc7 is described below

commit 334ca49afc73e443c7c9738b055efba61d0a83bf
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Sep 10 16:26:40 2026 +0530

    [v3-3-test] Gate asset event partition_key behind execution API 2026-06-30 
(#72327) (#72827)
    
    (cherry picked from commit 27e882439f66aa113d301eae37d45bba633c9acd)
    
    Co-authored-by: Neel Dalsania <[email protected]>
    Co-authored-by: Rahul Vats <[email protected]>
---
 airflow-core/newsfragments/72327.bugfix.rst        |  1 +
 .../api_fastapi/execution_api/versions/__init__.py |  2 +
 .../execution_api/versions/v2026_04_06.py          |  7 +-
 .../execution_api/versions/v2026_06_30.py          | 23 ++++++
 .../versions/v2026_04_06/test_task_instances.py    | 54 +-------------
 .../versions/v2026_06_30/test_dag_runs.py          | 57 +++++++++++++++
 .../versions/v2026_06_30/test_task_instances.py    | 82 +++++++++++++++++++++-
 7 files changed, 164 insertions(+), 62 deletions(-)

diff --git a/airflow-core/newsfragments/72327.bugfix.rst 
b/airflow-core/newsfragments/72327.bugfix.rst
new file mode 100644
index 00000000000..05a35cdce40
--- /dev/null
+++ b/airflow-core/newsfragments/72327.bugfix.rst
@@ -0,0 +1 @@
+Fix asset-triggered tasks failing before they start for workers running Task 
SDK 1.2.x (Airflow 3.2.x), which request execution API version ``2026-04-06``. 
Those workers received a ``partition_key`` field on the Dag run's consumed 
asset events that their models reject, raising an ``extra_forbidden`` 
validation error. The field is now gated behind version ``2026-06-30``, where 
it was introduced; workers on ``2026-06-30`` or newer are unaffected.
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py
index dc7035d31e3..97a525436c8 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/__init__.py
@@ -44,6 +44,7 @@ from airflow.api_fastapi.execution_api.versions.v2026_06_30 
import (
     AddAssetsByAliasEndpoint,
     AddAwaitingInputStatePayload,
     AddConnectionTestEndpoint,
+    AddConsumedAssetEventPartitionKeyField,
     AddPartitionDateField,
     AddRetryPolicyFields,
     AddTaskAndAssetStateStoreEndpoints,
@@ -65,6 +66,7 @@ bundle = VersionBundle(
         AddTaskAndAssetStateStoreEndpoints,
         AddAssetsByAliasEndpoint,
         AddPartitionDateField,
+        AddConsumedAssetEventPartitionKeyField,
     ),
     Version(
         "2026-04-06",
diff --git 
a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py 
b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
index e3b995011f4..4710de4f665 100644
--- a/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
+++ b/airflow-core/src/airflow/api_fastapi/execution_api/versions/v2026_04_06.py
@@ -29,7 +29,6 @@ from airflow.api_fastapi.execution_api.datamodels.asset_event 
import (
 )
 from airflow.api_fastapi.execution_api.datamodels.dagrun import 
TriggerDAGRunPayload
 from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
-    AssetEventDagRunReference,
     DagRun,
     TIDeferredStatePayload,
     TIRunContext,
@@ -37,14 +36,13 @@ from 
airflow.api_fastapi.execution_api.datamodels.taskinstance import (
 
 
 class AddPartitionKeyField(VersionChange):
-    """Add the `partition_key` field to DagRun model."""
+    """Add ``partition_key`` to the Dag run, asset event, asset reference and 
trigger payload models."""
 
     description = __doc__
 
     instructions_to_migrate_to_previous_version = (
         schema(DagRun).field("partition_key").didnt_exist,
         schema(AssetEventResponse).field("partition_key").didnt_exist,
-        schema(AssetEventDagRunReference).field("partition_key").didnt_exist,
         schema(TriggerDAGRunPayload).field("partition_key").didnt_exist,
         schema(DagRunAssetReference).field("partition_key").didnt_exist,
     )
@@ -55,9 +53,6 @@ class AddPartitionKeyField(VersionChange):
         dag_run = response.body.get("dag_run")
         if isinstance(dag_run, dict):
             dag_run.pop("partition_key", None)
-            for event in dag_run.get("consumed_asset_events") or ():
-                if isinstance(event, dict):
-                    event.pop("partition_key", None)
 
     @convert_response_to_previous_version_for(AssetEventsResponse)  # type: 
ignore[arg-type]
     def remove_partition_key_from_asset_events(response: ResponseInfo) -> 
None:  # type: ignore[misc]
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 0a316810691..85d46420040 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
@@ -26,6 +26,7 @@ from cadwyn import (
 )
 
 from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
+    AssetEventDagRunReference,
     DagRun,
     TaskInstance,
     TIAwaitingInputStatePayload,
@@ -168,3 +169,25 @@ class AddPartitionDateField(VersionChange):
         """Strip ``partition_date`` from the previous-run response."""
         if isinstance(response.body, dict):
             response.body.pop("partition_date", None)
+
+
+class AddConsumedAssetEventPartitionKeyField(VersionChange):
+    """Expose the upstream partition key on the asset events that triggered a 
consumer Dag run."""
+
+    description = __doc__
+
+    instructions_to_migrate_to_previous_version = (
+        schema(AssetEventDagRunReference).field("partition_key").didnt_exist,
+    )
+
+    # Only ``TIRunContext`` can carry these events: 
``DagRun.safe_extract_from_orm`` defaults
+    # ``consumed_asset_events`` to ``[]`` whenever the relationship is not 
already loaded, and
+    # ``/run`` is the only route that loads it -- so bare ``DagRun`` responses 
never carry one.
+    @convert_response_to_previous_version_for(TIRunContext)  # type: 
ignore[arg-type]
+    def remove_partition_key_from_consumed_asset_events(response: 
ResponseInfo) -> None:  # type: ignore[misc]
+        """Strip ``partition_key`` from each consumed asset event for older 
clients."""
+        dag_run = response.body.get("dag_run")
+        if isinstance(dag_run, dict):
+            for event in dag_run.get("consumed_asset_events") or ():
+                if isinstance(event, dict):
+                    event.pop("partition_key", None)
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
index 4a8b59c362e..d8552917277 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_04_06/test_task_instances.py
@@ -20,11 +20,10 @@ from __future__ import annotations
 import pytest
 
 from airflow._shared.timezones import timezone
-from airflow.models.asset import AssetActive, AssetEvent, AssetModel
 from airflow.serialization.serialized_objects import BaseSerialization
 from airflow.utils.state import DagRunState, State
 
-from tests_common.test_utils.db import clear_db_assets, clear_db_runs
+from tests_common.test_utils.db import clear_db_runs
 from tests_common.test_utils.format_datetime import 
from_datetime_to_zulu_without_ms
 
 pytestmark = pytest.mark.db_test
@@ -254,54 +253,3 @@ class TestNextKwargsBackwardCompat:
         assert response.status_code == 200
         # Head version gets the plain dict directly -- no BaseSerialization 
wrapping
         assert response.json()["next_kwargs"] == {"cheesecake": True, "event": 
"payload"}
-
-
-class TestConsumedEventPartitionKeyBackwardCompat:
-    """The partition_key on consumed asset events is stripped for 
pre-2026-04-06 clients."""
-
-    @pytest.fixture(autouse=True)
-    def _freeze_time(self, time_machine):
-        time_machine.move_to(TIMESTAMP_STR, tick=False)
-
-    def setup_method(self):
-        clear_db_runs()
-        clear_db_assets()
-
-    def teardown_method(self):
-        clear_db_runs()
-        clear_db_assets()
-
-    def _create_ti_with_consumed_event(self, session, create_task_instance):
-        ti = create_task_instance(
-            task_id="test_consumed_event_partition_key_compat",
-            state=State.QUEUED,
-            session=session,
-            start_date=TIMESTAMP,
-        )
-        asset = AssetModel(name="upstream", uri="s3://bucket/upstream", 
group="asset", extra={})
-        session.add_all([asset, AssetActive.for_asset(asset)])
-        session.flush()
-        ti.dag_run.consumed_asset_events.append(
-            AssetEvent(asset_id=asset.id, source_dag_id="src", 
source_run_id="r1", partition_key="2024-01-15")
-        )
-        session.commit()
-        return ti
-
-    def test_old_version_strips_partition_key(self, old_ver_client, session, 
create_task_instance):
-        ti = self._create_ti_with_consumed_event(session, create_task_instance)
-
-        response = 
old_ver_client.patch(f"/execution/task-instances/{ti.id}/run", 
json=RUN_PATCH_BODY)
-
-        assert response.status_code == 200
-        events = response.json()["dag_run"]["consumed_asset_events"]
-        assert events
-        assert all("partition_key" not in event for event in events)
-
-    def test_head_version_keeps_partition_key(self, client, session, 
create_task_instance):
-        ti = self._create_ti_with_consumed_event(session, create_task_instance)
-
-        response = client.patch(f"/execution/task-instances/{ti.id}/run", 
json=RUN_PATCH_BODY)
-
-        assert response.status_code == 200
-        events = response.json()["dag_run"]["consumed_asset_events"]
-        assert [event["partition_key"] for event in events] == ["2024-01-15"]
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
index d89c585d364..fe16f4ee8c8 100644
--- 
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
@@ -20,8 +20,11 @@ from __future__ import annotations
 import pytest
 
 from airflow._shared.timezones import timezone
+from airflow.models.asset import AssetActive, AssetEvent, AssetModel
 from airflow.utils.state import DagRunState
 
+from tests_common.test_utils.db import clear_db_assets
+
 pytestmark = pytest.mark.db_test
 
 ADDED_IN_2026_06_30 = frozenset({"team_name", "partition_date"})
@@ -88,3 +91,57 @@ def 
test_get_previous_dag_run_without_a_match(old_ver_client):
 
     assert response.status_code == 200
     assert response.json() is None
+
+
[email protected]
+def dag_run_with_consumed_event(session, dag_maker):
+    """A Dag run that consumed an asset event carrying a ``partition_key``."""
+    # dag_maker resets Dag/DagRun tables between tests but not asset tables, 
and this fixture
+    # commits (the API request needs to see the row), so a leftover asset from 
a prior test
+    # using this fixture would collide on the name/uri unique constraint.
+    clear_db_assets()
+    with dag_maker(dag_id="test_dag_run_consumed_event", session=session, 
serialized=True):
+        pass
+    run = dag_maker.create_dagrun(
+        state=DagRunState.SUCCESS,
+        logical_date=timezone.datetime(2025, 1, 1),
+        run_id="run1",
+    )
+    asset = AssetModel(name="upstream", uri="s3://bucket/upstream", 
group="asset", extra={})
+    session.add_all([asset, AssetActive.for_asset(asset)])
+    session.flush()
+    run.consumed_asset_events.append(
+        AssetEvent(asset_id=asset.id, source_dag_id="src", source_run_id="r1", 
partition_key="2024-01-15")
+    )
+    session.commit()
+    yield
+    clear_db_assets()
+
+
[email protected]("dag_run_with_consumed_event")
+def test_get_dag_run_returns_no_consumed_asset_events(old_ver_client):
+    """Bare-DagRun routes serialize no consumed asset events, so they need no 
partition_key converter."""
+    response = 
old_ver_client.get("/execution/dag-runs/test_dag_run_consumed_event/run1")
+
+    assert response.status_code == 200
+    # The Dag run really has a consumed event carrying a partition_key, but 
this route does not
+    # eager-load the relationship and ``safe_extract_from_orm`` defaults it 
away. Assert the empty
+    # list rather than the absence of the key: an ``all(...)`` over these 
events would hold
+    # vacuously and pass even with the version gate deleted.
+    assert response.json()["consumed_asset_events"] == []
+
+
[email protected]("dag_run_with_consumed_event")
+def test_get_previous_dag_run_returns_no_consumed_asset_events(old_ver_client):
+    """Same invariant via /previous, which also returns a bare DagRun."""
+    response = old_ver_client.get(
+        "/execution/dag-runs/previous",
+        params={
+            "dag_id": "test_dag_run_consumed_event",
+            "logical_date": timezone.datetime(2025, 1, 2).isoformat(),
+        },
+    )
+
+    assert response.status_code == 200
+    assert response.json()["run_id"] == "run1"
+    assert response.json()["consumed_asset_events"] == []
diff --git 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_task_instances.py
 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_task_instances.py
index b2cc016be84..b2536aa4c07 100644
--- 
a/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_task_instances.py
+++ 
b/airflow-core/tests/unit/api_fastapi/execution_api/versions/v2026_06_30/test_task_instances.py
@@ -20,9 +20,10 @@ from __future__ import annotations
 import pytest
 
 from airflow._shared.timezones import timezone
+from airflow.models.asset import AssetActive, AssetEvent, AssetModel
 from airflow.utils.state import DagRunState, State
 
-from tests_common.test_utils.db import clear_db_runs
+from tests_common.test_utils.db import clear_db_assets, clear_db_runs
 
 pytestmark = pytest.mark.db_test
 
@@ -41,8 +42,8 @@ RUN_PATCH_BODY = {
 
 @pytest.fixture
 def old_ver_client(client):
-    """Execution API version immediately before ``partition_date`` was 
added."""
-    client.headers["Airflow-API-Version"] = "2026-06-16"
+    """Last released execution API before this bundle -- the version Task SDK 
1.2.x sends."""
+    client.headers["Airflow-API-Version"] = "2026-04-06"
     return client
 
 
@@ -94,3 +95,78 @@ class TestPartitionDateFieldBackwardCompat:
         dag_run = response.json()["dag_run"]
         assert dag_run["partition_key"] == "2026-05-20"
         assert dag_run["partition_date"] == 
PARTITION_DATE.isoformat().replace("+00:00", "Z")
+
+
+class TestConsumedEventPartitionKeyBackwardCompat:
+    """``partition_key`` on consumed asset events is stripped for clients 
older than 2026-06-30."""
+
+    @pytest.fixture(autouse=True)
+    def _freeze_time(self, time_machine):
+        time_machine.move_to(TIMESTAMP_STR, tick=False)
+
+    def setup_method(self):
+        clear_db_runs()
+        clear_db_assets()
+
+    def teardown_method(self):
+        clear_db_runs()
+        clear_db_assets()
+
+    def _create_ti_with_consumed_event(self, session, create_task_instance):
+        ti = create_task_instance(
+            task_id="test_consumed_event_partition_key_compat",
+            state=State.QUEUED,
+            session=session,
+            start_date=TIMESTAMP,
+        )
+        asset = AssetModel(name="upstream", uri="s3://bucket/upstream", 
group="asset", extra={})
+        session.add_all([asset, AssetActive.for_asset(asset)])
+        session.flush()
+        ti.dag_run.partition_key = "2026-05-20"
+        ti.dag_run.consumed_asset_events.append(
+            AssetEvent(asset_id=asset.id, source_dag_id="src", 
source_run_id="r1", partition_key="2024-01-15")
+        )
+        session.commit()
+        return ti
+
+    def test_old_version_strips_event_key_but_keeps_dag_run_key(
+        self, old_ver_client, session, create_task_instance
+    ):
+        ti = self._create_ti_with_consumed_event(session, create_task_instance)
+
+        response = 
old_ver_client.patch(f"/execution/task-instances/{ti.id}/run", 
json=RUN_PATCH_BODY)
+
+        assert response.status_code == 200
+        dag_run = response.json()["dag_run"]
+        events = dag_run["consumed_asset_events"]
+        assert events
+        assert all("partition_key" not in event for event in events)
+        # The DagRun-level field really was released at 2026-04-06 and must 
survive.
+        assert dag_run["partition_key"] == "2026-05-20"
+
+    @pytest.mark.parametrize("api_version", [None, "2026-06-30"])
+    def test_partition_key_kept_at_2026_06_30_and_newer(
+        self, client, session, create_task_instance, api_version
+    ):
+        """The gate must not over-move: 1.3.x clients send 2026-06-30 and do 
carry this field."""
+        if api_version is not None:
+            client.headers["Airflow-API-Version"] = api_version
+        ti = self._create_ti_with_consumed_event(session, create_task_instance)
+
+        response = client.patch(f"/execution/task-instances/{ti.id}/run", 
json=RUN_PATCH_BODY)
+
+        assert response.status_code == 200
+        dag_run = response.json()["dag_run"]
+        assert [event["partition_key"] for event in 
dag_run["consumed_asset_events"]] == ["2024-01-15"]
+        assert dag_run["partition_key"] == "2026-05-20"
+
+    def test_schema_gates_event_partition_key_at_2026_06_30(self, client):
+        """
+        The schema half of the gate must move with the converter half, or the 
served
+        OpenAPI contradicts the wire format.
+        """
+        old_schema = 
client.get("/execution/openapi.json?version=2026-04-06").json()["components"]["schemas"]
+        new_schema = 
client.get("/execution/openapi.json?version=2026-06-30").json()["components"]["schemas"]
+
+        assert "partition_key" not in 
old_schema["AssetEventDagRunReference"]["properties"]
+        assert "partition_key" in 
new_schema["AssetEventDagRunReference"]["properties"]

Reply via email to