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

o-nikolas 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 101bc155e0f Thread version_data to callbacks (#69185)
101bc155e0f is described below

commit 101bc155e0fcec8b54ec0e21df0344aa2d2a5997
Author: Niko Oliveira <[email protected]>
AuthorDate: Tue Jul 7 15:35:31 2026 -0700

    Thread version_data to callbacks (#69185)
    
    PR #67217 wired version_data into the task execution path but not
    callbacks, so callbacks for a pinned run initialized their bundle
    without the manifest tasks used. Populate version_data on the
    callback producer paths (ExecuteCallback.make and the
    CallbackRequest creation sites) under the same pin guard as tasks,
    carry it on BaseCallbackRequest, and forward it through
    prepare_callback_bundle to get_bundle.
---
 .../src/airflow/callbacks/callback_requests.py     |  2 +
 airflow-core/src/airflow/dag_processing/manager.py |  6 ++-
 .../src/airflow/executors/workloads/callback.py    |  4 ++
 .../src/airflow/executors/workloads/task.py        |  7 ++-
 .../src/airflow/jobs/scheduler_job_runner.py       | 10 +++-
 airflow-core/src/airflow/models/dag_version.py     | 14 ++++-
 airflow-core/src/airflow/models/dagrun.py          |  6 +++
 airflow-core/src/airflow/models/trigger.py         | 23 ++++++++-
 .../tests/unit/callbacks/test_callback_requests.py | 27 ++++++++++
 .../tests/unit/dag_processing/test_manager.py      | 25 +++++++++
 .../tests/unit/executors/test_workloads.py         | 60 +++++++++++++++++++++-
 airflow-core/tests/unit/jobs/test_scheduler_job.py |  1 +
 airflow-core/tests/unit/models/test_dag_version.py | 29 +++++++++++
 airflow-core/tests/unit/models/test_trigger.py     | 27 ++++++++++
 .../airflow/sdk/execution_time/schema/schema.json  | 39 ++++++++++++++
 ts-sdk/src/generated/supervisor.ts                 | 12 +++++
 16 files changed, 282 insertions(+), 10 deletions(-)

diff --git a/airflow-core/src/airflow/callbacks/callback_requests.py 
b/airflow-core/src/airflow/callbacks/callback_requests.py
index ce48438f0e7..6609dc30c6d 100644
--- a/airflow-core/src/airflow/callbacks/callback_requests.py
+++ b/airflow-core/src/airflow/callbacks/callback_requests.py
@@ -46,6 +46,8 @@ class BaseCallbackRequest(BaseModel):
     """File Path to use to run the callback"""
     bundle_name: str
     bundle_version: str | None
+    version_data: dict[str, Any] | None = None
+    """Optional structured metadata for the pinned bundle version (e.g. an S3 
object manifest)."""
     msg: str | None = None
     """Additional Message that can be used for logging to determine 
failure/task heartbeat timeout"""
 
diff --git a/airflow-core/src/airflow/dag_processing/manager.py 
b/airflow-core/src/airflow/dag_processing/manager.py
index b2428985a65..f41883edf31 100644
--- a/airflow-core/src/airflow/dag_processing/manager.py
+++ b/airflow-core/src/airflow/dag_processing/manager.py
@@ -705,7 +705,11 @@ class DagFileProcessorManager(LoggingMixin):
         Override to source the bundle from an API.
         """
         try:
-            bundle = DagBundlesManager().get_bundle(name=request.bundle_name, 
version=request.bundle_version)
+            bundle = DagBundlesManager().get_bundle(
+                name=request.bundle_name,
+                version=request.bundle_version,
+                version_data=request.version_data,
+            )
         except ValueError:
             self.log.error("Bundle %s no longer configured, skipping 
callback", request.bundle_name)
             return None
diff --git a/airflow-core/src/airflow/executors/workloads/callback.py 
b/airflow-core/src/airflow/executors/workloads/callback.py
index 04f26b8e787..c1842a59006 100644
--- a/airflow-core/src/airflow/executors/workloads/callback.py
+++ b/airflow-core/src/airflow/executors/workloads/callback.py
@@ -114,9 +114,13 @@ class ExecuteCallback(BaseDagBundleWorkload):
     ) -> ExecuteCallback:
         """Create an ExecuteCallback workload from a Callback ORM model."""
         if not bundle_info:
+            from airflow.models.dag_version import _resolve_version_data
+
+            version_data = _resolve_version_data(dag_run.created_dag_version, 
dag_run.bundle_version)
             bundle_info = BundleInfo(
                 name=dag_run.dag_model.bundle_name,
                 version=dag_run.bundle_version,
+                version_data=version_data,
             )
         fname = 
f"executor_callbacks/{dag_run.dag_id}/{dag_run.run_id}/{callback.id}"
 
diff --git a/airflow-core/src/airflow/executors/workloads/task.py 
b/airflow-core/src/airflow/executors/workloads/task.py
index 611457f88a9..68a917118e3 100644
--- a/airflow-core/src/airflow/executors/workloads/task.py
+++ b/airflow-core/src/airflow/executors/workloads/task.py
@@ -102,13 +102,12 @@ class ExecuteTask(BaseDagBundleWorkload):
 
         ser_ti = TaskInstanceDTO.model_validate(ti, from_attributes=True)
         if not bundle_info:
-            version_data = None
-            if ti.dag_version is not None and ti.dag_run.bundle_version is not 
None:
-                version_data = ti.dag_version.version_data
+            from airflow.models.dag_version import _resolve_version_data
+
             bundle_info = BundleInfo(
                 name=ti.dag_model.bundle_name,
                 version=ti.dag_run.bundle_version,
-                version_data=version_data,
+                version_data=_resolve_version_data(ti.dag_version, 
ti.dag_run.bundle_version),
             )
         fname = log_filename_template_renderer()(ti=ti)
 
diff --git a/airflow-core/src/airflow/jobs/scheduler_job_runner.py 
b/airflow-core/src/airflow/jobs/scheduler_job_runner.py
index f4e66fc17c4..b941a5b3580 100644
--- a/airflow-core/src/airflow/jobs/scheduler_job_runner.py
+++ b/airflow-core/src/airflow/jobs/scheduler_job_runner.py
@@ -97,7 +97,7 @@ from airflow.models.connection_test import (
     ConnectionTestState,
 )
 from airflow.models.dag import DagModel
-from airflow.models.dag_version import DagVersion
+from airflow.models.dag_version import DagVersion, _resolve_version_data
 from airflow.models.dagbag import DBDagBag
 from airflow.models.dagbundle import DagBundleModel
 from airflow.models.dagrun import DagRun
@@ -1526,6 +1526,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                         if ti.dag_version and ti.dag_run.bundle_version is not 
None
                         else ti.dag_run.bundle_version
                     )
+                    _version_data = _resolve_version_data(ti.dag_version, 
ti.dag_run.bundle_version)
                     # Backfill dag_version_id for legacy tasks (Pydantic 
requires uuid.UUID).
                     if not _ensure_ti_has_dag_version_id(ti, session, 
cls.logger()):
                         continue
@@ -1533,6 +1534,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                         filepath=ti.dag_model.relative_fileloc or "",
                         bundle_name=_bundle_name,
                         bundle_version=_bundle_version,
+                        version_data=_version_data,
                         ti=ti,
                         msg=msg,
                         task_callback_type=(
@@ -1575,6 +1577,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                     _email_bundle_version = (
                         ti.dag_version.bundle_version if ti.dag_version else 
ti.dag_run.bundle_version
                     )
+                    _email_version_data = 
_resolve_version_data(ti.dag_version, ti.dag_run.bundle_version)
                     # Backfill dag_version_id for legacy tasks (Pydantic 
requires uuid.UUID).
                     if not _ensure_ti_has_dag_version_id(ti, session, 
cls.logger()):
                         continue
@@ -1582,6 +1585,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                         filepath=ti.dag_model.relative_fileloc or "",
                         bundle_name=_email_bundle_name,
                         bundle_version=_email_bundle_version,
+                        version_data=_email_version_data,
                         ti=ti,
                         msg=msg,
                         email_type="retry" if ti.is_eligible_to_retry() else 
"failure",
@@ -3096,6 +3100,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                         if ti.dag_version and ti.dag_run.bundle_version is not 
None
                         else ti.dag_run.bundle_version
                     )
+                    _stuck_version_data = 
_resolve_version_data(ti.dag_version, ti.dag_run.bundle_version)
                     # Backfill dag_version_id for legacy tasks (Pydantic 
requires uuid.UUID).
                     # Note: we cannot use `continue` here because this method 
is not
                     # inside a loop.  If backfilling fails we simply skip the 
callback.
@@ -3104,6 +3109,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                             filepath=ti.dag_model.relative_fileloc or "",
                             bundle_name=_stuck_bundle_name,
                             bundle_version=_stuck_bundle_version,
+                            version_data=_stuck_version_data,
                             ti=ti,
                             msg=msg,
                             context_from_server=TIRunContext(
@@ -3571,6 +3577,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                 if ti.dag_version and ti.dag_run.bundle_version is not None
                 else ti.dag_run.bundle_version
             )
+            _hb_version_data = _resolve_version_data(ti.dag_version, 
ti.dag_run.bundle_version)
             # Backfill dag_version_id for legacy tasks (Pydantic requires 
uuid.UUID).
             if not _ensure_ti_has_dag_version_id(ti, session, self.log):
                 continue
@@ -3578,6 +3585,7 @@ class SchedulerJobRunner(BaseJobRunner, LoggingMixin):
                 filepath=ti.dag_model.relative_fileloc or "",
                 bundle_name=_hb_bundle_name,
                 bundle_version=_hb_bundle_version,
+                version_data=_hb_version_data,
                 ti=ti,
                 msg=str(task_instance_heartbeat_timeout_message_details),
                 context_from_server=TIRunContext(
diff --git a/airflow-core/src/airflow/models/dag_version.py 
b/airflow-core/src/airflow/models/dag_version.py
index e6564a6da06..eb0a5204956 100644
--- a/airflow-core/src/airflow/models/dag_version.py
+++ b/airflow-core/src/airflow/models/dag_version.py
@@ -19,7 +19,7 @@ from __future__ import annotations
 
 import logging
 from datetime import datetime
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Any
 from uuid import UUID
 
 import sqlalchemy as sa
@@ -235,3 +235,15 @@ class DagVersion(Base):
     def version(self) -> str:
         """A human-friendly representation of the version."""
         return f"{self.dag_id}-{self.version_number}"
+
+
+def _resolve_version_data(
+    dag_version: DagVersion | None, bundle_version: str | None
+) -> dict[str, Any] | None:
+    """Return a bundle version's ``version_data`` manifest, but only for 
pinned runs."""
+    # Expose version_data only when the run is pinned (bundle_version set) and 
a DagVersion is
+    # present, so the bundle initializes against the exact version the run 
used. Unpinned runs
+    # follow the latest bundle state, and legacy rows have no DagVersion.
+    if dag_version is not None and bundle_version is not None:
+        return dag_version.version_data
+    return None
diff --git a/airflow-core/src/airflow/models/dagrun.py 
b/airflow-core/src/airflow/models/dagrun.py
index 056151cefa1..27751ed8b56 100644
--- a/airflow-core/src/airflow/models/dagrun.py
+++ b/airflow-core/src/airflow/models/dagrun.py
@@ -1449,12 +1449,18 @@ class DagRun(Base, LoggingMixin):
             )
             relevant_ti = None
         if not execute:
+            from airflow.models.dag_version import _resolve_version_data
+
+            # Only carry version_data for pinned runs so the callback 
initializes the bundle
+            # against the same version the run used.
+            version_data = _resolve_version_data(self.created_dag_version, 
self.bundle_version)
             return DagCallbackRequest(
                 filepath=self.dag_model.relative_fileloc,
                 dag_id=self.dag_id,
                 run_id=self.run_id,
                 bundle_name=self.dag_model.bundle_name,
                 bundle_version=self.bundle_version,
+                version_data=version_data,
                 context_from_server=DagRunContext(
                     dag_run=self,
                     last_ti=relevant_ti,
diff --git a/airflow-core/src/airflow/models/trigger.py 
b/airflow-core/src/airflow/models/trigger.py
index e457b2ec936..d52daa649bc 100644
--- a/airflow-core/src/airflow/models/trigger.py
+++ b/airflow-core/src/airflow/models/trigger.py
@@ -570,12 +570,31 @@ def _(event: BaseTaskEndEvent, *, task_instance: 
TaskInstance, session: Session)
         if event.task_instance_state in (TaskInstanceState.SUCCESS, 
TaskInstanceState.FAILED):
             if task_instance.dag_model.relative_fileloc is None:
                 raise RuntimeError("relative_fileloc should not be None for a 
finished task")
+            from airflow.models.dag_version import _resolve_version_data
+
+            # Derive bundle identity from the TI's dag_version (falling back 
to dag_run/dag_model
+            # for legacy/unpinned runs), mirroring the other callback sites so 
bundle_version and
+            # version_data always describe the same version.
+            bundle_name = (
+                task_instance.dag_version.bundle_name
+                if task_instance.dag_version
+                else task_instance.dag_model.bundle_name
+            )
+            bundle_version = (
+                task_instance.dag_version.bundle_version
+                if task_instance.dag_version and 
task_instance.dag_run.bundle_version is not None
+                else task_instance.dag_run.bundle_version
+            )
+            version_data = _resolve_version_data(
+                task_instance.dag_version, task_instance.dag_run.bundle_version
+            )
             request = TaskCallbackRequest(
                 filepath=task_instance.dag_model.relative_fileloc,
                 ti=task_instance,
                 task_callback_type=event.task_instance_state,
-                bundle_name=task_instance.dag_model.bundle_name,
-                bundle_version=task_instance.dag_run.bundle_version,
+                bundle_name=bundle_name,
+                bundle_version=bundle_version,
+                version_data=version_data,
             )
             log.info("Sending callback: %s", request)
             try:
diff --git a/airflow-core/tests/unit/callbacks/test_callback_requests.py 
b/airflow-core/tests/unit/callbacks/test_callback_requests.py
index 434223e3747..9b85d61e5db 100644
--- a/airflow-core/tests/unit/callbacks/test_callback_requests.py
+++ b/airflow-core/tests/unit/callbacks/test_callback_requests.py
@@ -118,6 +118,33 @@ class TestCallbackRequest:
 
         assert request.is_failure_callback == expected_is_failure
 
+    def test_version_data_round_trips_and_defaults_none(self):
+        """version_data survives JSON serialization and defaults to None when 
omitted."""
+        version_data = {"schema_version": 1, "files": {"dags/my_dag.py": 
"ver123"}}
+        request = DagCallbackRequest(
+            filepath="filepath",
+            dag_id="fake_dag",
+            run_id="fake_run",
+            is_failure_callback=False,
+            bundle_name="testing",
+            bundle_version="abc123",
+            version_data=version_data,
+        )
+        result = DagCallbackRequest.from_json(request.to_json())
+        assert result.version_data == version_data
+
+        # Omitted -> defaults to None and round-trips as None.
+        unpinned = DagCallbackRequest(
+            filepath="filepath",
+            dag_id="fake_dag",
+            run_id="fake_run",
+            is_failure_callback=False,
+            bundle_name="testing",
+            bundle_version=None,
+        )
+        assert unpinned.version_data is None
+        assert DagCallbackRequest.from_json(unpinned.to_json()).version_data 
is None
+
 
 class TestDagRunContext:
     def test_dagrun_context_creation(self):
diff --git a/airflow-core/tests/unit/dag_processing/test_manager.py 
b/airflow-core/tests/unit/dag_processing/test_manager.py
index 4b55b6b2f1e..ee2d7cd76d4 100644
--- a/airflow-core/tests/unit/dag_processing/test_manager.py
+++ b/airflow-core/tests/unit/dag_processing/test_manager.py
@@ -1375,6 +1375,7 @@ class TestDagFileProcessorManager:
                             "filepath": "dag_callback_dag.py",
                             "bundle_name": "testing",
                             "bundle_version": None,
+                            "version_data": None,
                             "msg": None,
                             "dag_id": "dag_id",
                             "run_id": "run_id",
@@ -1966,6 +1967,30 @@ class TestDagFileProcessorManager:
         assert manager.prepare_callback_bundle(request) is bundle
         bundle.initialize.assert_called_once()
 
+    @mock.patch("airflow.dag_processing.manager.DagBundlesManager")
+    def test_prepare_callback_bundle_forwards_version_data(self, 
mock_bundle_manager):
+        manager = DagFileProcessorManager(max_runs=1)
+        bundle = MagicMock(spec=BaseDagBundle)
+        bundle.supports_versioning = True
+        mock_bundle_manager.return_value.get_bundle.return_value = bundle
+
+        version_data = {"schema_version": 1, "files": {"dags/my_dag.py": 
"ver123"}}
+        request = DagCallbackRequest(
+            filepath="file1.py",
+            dag_id="dag1",
+            run_id="run1",
+            is_failure_callback=False,
+            bundle_name="testing",
+            bundle_version="some_commit_hash",
+            version_data=version_data,
+            msg=None,
+        )
+
+        manager.prepare_callback_bundle(request)
+        mock_bundle_manager.return_value.get_bundle.assert_called_once_with(
+            name="testing", version="some_commit_hash", 
version_data=version_data
+        )
+
     @mock.patch("airflow.dag_processing.manager.DagBundlesManager")
     def 
test_prepare_callback_bundle_skips_initialize_for_unversioned_request(self, 
mock_bundle_manager):
         manager = DagFileProcessorManager(max_runs=1)
diff --git a/airflow-core/tests/unit/executors/test_workloads.py 
b/airflow-core/tests/unit/executors/test_workloads.py
index 63a249a36fd..1ef04477d08 100644
--- a/airflow-core/tests/unit/executors/test_workloads.py
+++ b/airflow-core/tests/unit/executors/test_workloads.py
@@ -28,7 +28,7 @@ from airflow.api_fastapi.auth.tokens import JWTGenerator
 from airflow.executors import workloads
 from airflow.executors.workloads import TaskInstance, TaskInstanceDTO, base as 
workloads_base
 from airflow.executors.workloads.base import BaseWorkloadSchema, BundleInfo
-from airflow.executors.workloads.callback import CallbackDTO, 
CallbackFetchMethod
+from airflow.executors.workloads.callback import CallbackDTO, 
CallbackFetchMethod, ExecuteCallback
 from airflow.executors.workloads.task import ExecuteTask
 from airflow.executors.workloads.types import state_class_for_key
 from airflow.models.callback import CallbackKey
@@ -250,3 +250,61 @@ class TestExecuteTaskMakeVersionData:
 
         assert workload.bundle_info.version == "abc123"
         assert workload.bundle_info.version_data is None
+
+
+class TestExecuteCallbackMakeVersionData:
+    """Tests for ExecuteCallback.make() threading version_data through 
BundleInfo."""
+
+    @staticmethod
+    def _make_mocks(bundle_version, version_data, *, 
has_created_dag_version=True):
+        """Build mock Callback + DagRun with the attributes 
ExecuteCallback.make() reads."""
+        from unittest.mock import Mock
+
+        callback = Mock()
+        callback.id = uuid4()
+        callback.fetch_method = CallbackFetchMethod.IMPORT_PATH
+        callback.data = {"path": "my_module.my_callback"}
+
+        dag_run = Mock()
+        dag_run.dag_id = "test_dag"
+        dag_run.run_id = "test_run"
+        dag_run.bundle_version = bundle_version
+        dag_run.dag_model.bundle_name = "test-bundle"
+        dag_run.dag_model.relative_fileloc = "dags/test_dag.py"
+        if has_created_dag_version:
+            dag_run.created_dag_version.version_data = version_data
+        else:
+            dag_run.created_dag_version = None
+
+        return callback, dag_run
+
+    def test_pinned_run_populates_version_data(self):
+        """When the run is pinned, version_data from created_dag_version flows 
to BundleInfo."""
+        version_data = {"schema_version": 1, "files": {"dags/my_dag.py": 
"ver123"}}
+        callback, dag_run = self._make_mocks(bundle_version="abc123", 
version_data=version_data)
+
+        workload = ExecuteCallback.make(callback=callback, dag_run=dag_run)
+
+        assert workload.bundle_info.version == "abc123"
+        assert workload.bundle_info.version_data == version_data
+
+    def test_unpinned_run_suppresses_present_version_data(self):
+        """An unpinned run must not expose version_data even when 
created_dag_version carries it."""
+        version_data = {"schema_version": 1, "files": {"dags/my_dag.py": 
"ver123"}}
+        callback, dag_run = self._make_mocks(bundle_version=None, 
version_data=version_data)
+
+        workload = ExecuteCallback.make(callback=callback, dag_run=dag_run)
+
+        assert workload.bundle_info.version is None
+        assert workload.bundle_info.version_data is None
+
+    def test_missing_created_dag_version_yields_none(self):
+        """A pinned run without a created_dag_version yields no 
version_data."""
+        callback, dag_run = self._make_mocks(
+            bundle_version="abc123", version_data=None, 
has_created_dag_version=False
+        )
+
+        workload = ExecuteCallback.make(callback=callback, dag_run=dag_run)
+
+        assert workload.bundle_info.version == "abc123"
+        assert workload.bundle_info.version_data is None
diff --git a/airflow-core/tests/unit/jobs/test_scheduler_job.py 
b/airflow-core/tests/unit/jobs/test_scheduler_job.py
index ef5ea043060..59bc50a7f12 100644
--- a/airflow-core/tests/unit/jobs/test_scheduler_job.py
+++ b/airflow-core/tests/unit/jobs/test_scheduler_job.py
@@ -730,6 +730,7 @@ class TestSchedulerJob:
             ti=mock.ANY,
             bundle_name="dag_maker",
             bundle_version=None,
+            version_data=None,
             msg=f"Executor {executor} reported that the task instance "
             f"<TaskInstance: 
test_process_executor_events_with_callback.dummy_task test [queued] 
ti_id={ti1.id}> "
             "finished with state failed, but the task instance's state 
attribute is queued. "
diff --git a/airflow-core/tests/unit/models/test_dag_version.py 
b/airflow-core/tests/unit/models/test_dag_version.py
index ec600c7be66..da4923d8771 100644
--- a/airflow-core/tests/unit/models/test_dag_version.py
+++ b/airflow-core/tests/unit/models/test_dag_version.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 from datetime import timedelta
+from unittest import mock
 
 import pytest
 from sqlalchemy import func, select
@@ -181,3 +182,31 @@ class TestDagVersion:
         retrieved = DagVersion.get_latest_version("test_no_version_data", 
session=session)
         assert retrieved.version_data is None
         assert retrieved.bundle_version == "abc123"
+
+
+class TestResolveVersionData:
+    """Unit tests for the _resolve_version_data pin-guard helper."""
+
+    @pytest.mark.parametrize(
+        ("dag_version", "bundle_version", "expected"),
+        [
+            pytest.param(
+                mock.Mock(version_data={"schema_version": 1}),
+                "abc123",
+                {"schema_version": 1},
+                id="pinned-with-data",
+            ),
+            pytest.param(
+                mock.Mock(version_data={"schema_version": 1}),
+                None,
+                None,
+                id="unpinned-suppresses-present-data",
+            ),
+            pytest.param(None, "abc123", None, id="missing-dag-version"),
+            pytest.param(None, None, None, id="unpinned-and-missing"),
+        ],
+    )
+    def test_resolve_version_data(self, dag_version, bundle_version, expected):
+        from airflow.models.dag_version import _resolve_version_data
+
+        assert _resolve_version_data(dag_version, bundle_version) == expected
diff --git a/airflow-core/tests/unit/models/test_trigger.py 
b/airflow-core/tests/unit/models/test_trigger.py
index 669d98a7704..91a6a92e27c 100644
--- a/airflow-core/tests/unit/models/test_trigger.py
+++ b/airflow-core/tests/unit/models/test_trigger.py
@@ -335,6 +335,33 @@ def test_submit_event_task_end(mock_utcnow, session, 
create_task_instance, event
     assert actual_xcoms == expected_xcoms
 
 
+@patch("airflow.callbacks.database_callback_sink.DatabaseCallbackSink.send")
+def test_submit_event_task_end_callback_includes_version_data(mock_send, 
session, create_task_instance):
+    """A finished deferred task's callback carries version_data for a pinned 
run, and its
+    bundle_version is derived from the same dag_version so the two cannot 
diverge."""
+    version_data = {"schema_version": 1, "files": {"dags/my_dag.py": "ver123"}}
+
+    trigger = Trigger(classpath="does.not.matter", kwargs={})
+    session.add(trigger)
+    task_instance = create_task_instance(
+        session=session, logical_date=timezone.utcnow(), state=State.DEFERRED
+    )
+    task_instance.trigger_id = trigger.id
+    # Pin the run and attach a manifest to the TI's dag_version.
+    task_instance.dag_run.bundle_version = "some_hash"
+    task_instance.dag_version.bundle_version = "some_hash"
+    task_instance.dag_version.version_data = version_data
+    session.commit()
+
+    Trigger.submit_event(trigger.id, TaskSuccessEvent(), session=session)
+    session.flush()
+
+    mock_send.assert_called_once()
+    request = mock_send.call_args.kwargs["callback"]
+    assert request.bundle_version == "some_hash"
+    assert request.version_data == version_data
+
+
 @pytest.fixture
 def create_triggerer():
     """Fixture factory which creates individual test Triggerer instances."""
diff --git a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json 
b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
index 643bda0a5c6..01e3c8970b5 100644
--- a/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
+++ b/task-sdk/src/airflow/sdk/execution_time/schema/schema.json
@@ -753,6 +753,19 @@
           ],
           "title": "Bundle Version"
         },
+        "version_data": {
+          "anyOf": [
+            {
+              "additionalProperties": true,
+              "type": "object"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Version Data"
+        },
         "msg": {
           "anyOf": [
             {
@@ -1651,6 +1664,19 @@
           ],
           "title": "Bundle Version"
         },
+        "version_data": {
+          "anyOf": [
+            {
+              "additionalProperties": true,
+              "type": "object"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Version Data"
+        },
         "msg": {
           "anyOf": [
             {
@@ -3889,6 +3915,19 @@
           ],
           "title": "Bundle Version"
         },
+        "version_data": {
+          "anyOf": [
+            {
+              "additionalProperties": true,
+              "type": "object"
+            },
+            {
+              "type": "null"
+            }
+          ],
+          "default": null,
+          "title": "Version Data"
+        },
         "msg": {
           "anyOf": [
             {
diff --git a/ts-sdk/src/generated/supervisor.ts 
b/ts-sdk/src/generated/supervisor.ts
index fe61155f39e..004ad49e7ba 100644
--- a/ts-sdk/src/generated/supervisor.ts
+++ b/ts-sdk/src/generated/supervisor.ts
@@ -120,6 +120,9 @@ export type Type11 = "DRCount";
 export type Filepath = string;
 export type BundleName = string;
 export type BundleVersion = string | null;
+export type VersionData1 = {
+  [k: string]: unknown;
+} | null;
 export type Msg = string | null;
 export type DagId1 = string;
 export type RunId1 = string;
@@ -192,6 +195,9 @@ export type BundleName1 = string;
 export type Filepath1 = string;
 export type BundleName2 = string;
 export type BundleVersion1 = string | null;
+export type VersionData2 = {
+  [k: string]: unknown;
+} | null;
 export type Msg1 = string | null;
 /**
  * All possible states that a Task Instance can be in.
@@ -243,6 +249,9 @@ export type Type13 = "TaskCallbackRequest";
 export type Filepath2 = string;
 export type BundleName3 = string;
 export type BundleVersion2 = string | null;
+export type VersionData3 = {
+  [k: string]: unknown;
+} | null;
 export type Msg2 = string | null;
 export type EmailType = "failure" | "retry";
 export type Type14 = "EmailRequest";
@@ -849,6 +858,7 @@ export interface DagCallbackRequest {
   filepath: Filepath;
   bundle_name: BundleName;
   bundle_version: BundleVersion;
+  version_data?: VersionData1;
   msg?: Msg;
   dag_id: DagId1;
   run_id: RunId1;
@@ -959,6 +969,7 @@ export interface TaskCallbackRequest {
   filepath: Filepath1;
   bundle_name: BundleName2;
   bundle_version: BundleVersion1;
+  version_data?: VersionData2;
   msg?: Msg1;
   ti: TaskInstance;
   task_callback_type?: TaskInstanceState | null;
@@ -1019,6 +1030,7 @@ export interface EmailRequest {
   filepath: Filepath2;
   bundle_name: BundleName3;
   bundle_version: BundleVersion2;
+  version_data?: VersionData3;
   msg?: Msg2;
   ti: TaskInstance;
   email_type?: EmailType;

Reply via email to