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

potiuk 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 e8c00811fa7 Document commit SHA support for GitDagBundle tracking_ref 
(#69735)
e8c00811fa7 is described below

commit e8c00811fa71ec9bee8bbaae19a64f3505b05633
Author: Cole Heflin <[email protected]>
AuthorDate: Wed Aug 12 13:45:06 2026 -0700

    Document commit SHA support for GitDagBundle tracking_ref (#69735)
---
 providers/git/docs/bundles/index.rst               | 33 ++++++++
 .../git/src/airflow/providers/git/bundles/git.py   |  5 +-
 providers/git/tests/unit/git/bundles/test_git.py   | 89 ++++++++++++++++++++++
 3 files changed, 126 insertions(+), 1 deletion(-)

diff --git a/providers/git/docs/bundles/index.rst 
b/providers/git/docs/bundles/index.rst
index d551235bb86..6e7cf34a0f7 100644
--- a/providers/git/docs/bundles/index.rst
+++ b/providers/git/docs/bundles/index.rst
@@ -41,3 +41,36 @@ Example of using the GitDagBundle:
          }
      }
     ]'
+
+``tracking_ref`` accepts a branch, tag, or full commit SHA. Setting it to a 
commit SHA pins the
+bundle to that exact commit:
+
+.. code-block:: bash
+
+    export AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST='[
+     {
+         "name": "my-git-repo",
+         "classpath": "airflow.providers.git.bundles.git.GitDagBundle",
+         "kwargs": {
+             "repo_url": "https://github.com/org/repo.git";,
+             "tracking_ref": "a3d1850dd1aa1919a61620aa39f202185c9321c0",
+             "subdir": "dags"
+         }
+     }
+    ]'
+
+Branches move as new commits are pushed, so combined with ``refresh_interval`` 
they pick up new code
+without a restart. Tags and commit SHAs are static (assuming tags aren't 
moved), pinning the bundle
+to known-good code — but changing a SHA-pinned ``tracking_ref`` is a 
``dag_bundle_config_list``
+config change, not a ref move, so it only takes effect once the Dag processor 
is restarted and
+reloads the configuration. If ``[dag_processor] disable_bundle_versioning`` 
(or the
+``disable_bundle_versioning`` Dag parameter) is set, workers also resolve code 
from their own
+``tracking_ref`` rather than a recorded bundle version, so they need the 
updated configuration too.
+
+.. note::
+
+    Rolling back a SHA-pinned ``tracking_ref`` after a restart is reliable, 
since the commit's
+    objects are already present in the bundle's local storage. Promoting to a 
*new* SHA can fail
+    to check out that commit unless the bundle's local storage is cleared 
first (for example, a
+    fresh pod, or manually deleting the bundle's directory) — see
+    `GH-71388 <https://github.com/apache/airflow/issues/71388>`_ for the 
underlying limitation.
diff --git a/providers/git/src/airflow/providers/git/bundles/git.py 
b/providers/git/src/airflow/providers/git/bundles/git.py
index 917e3e6a333..930331a0b81 100644
--- a/providers/git/src/airflow/providers/git/bundles/git.py
+++ b/providers/git/src/airflow/providers/git/bundles/git.py
@@ -45,7 +45,7 @@ class GitDagBundle(BaseDagBundle):
     Instead of cloning the repository every time, we clone the repository once 
into a bare repo from the source
     and then do a clone for each version from there.
 
-    :param tracking_ref: Branch or tag for this DAG bundle
+    :param tracking_ref: Branch, tag, or commit SHA for this DAG bundle
     :param subdir: Subdirectory within the repository where the DAGs are 
stored (Optional)
     :param git_conn_id: Connection ID for SSH/token based connection to the 
repository (Optional)
     :param repo_url: Explicit Git repository URL to override the connection's 
host. (Optional)
@@ -212,6 +212,9 @@ class GitDagBundle(BaseDagBundle):
                 raise RuntimeError("Error cloning repository") from e
             except InvalidGitRepositoryError as e:
                 raise RuntimeError(f"Invalid git repository at 
{self.repo_path}") from e
+            # If tracking_ref was just repointed to a SHA that predates this 
working clone's
+            # last fetch, this checkout fails until the clone is fetched or 
storage is cleared;
+            # tracked at https://github.com/apache/airflow/issues/71388
             self.repo.git.checkout(self.tracking_ref)
             self._log.debug("bundle initialize", version=self.version)
             if self.version:
diff --git a/providers/git/tests/unit/git/bundles/test_git.py 
b/providers/git/tests/unit/git/bundles/test_git.py
index 0726d0a5fc8..0962fa1ad0b 100644
--- a/providers/git/tests/unit/git/bundles/test_git.py
+++ b/providers/git/tests/unit/git/bundles/test_git.py
@@ -701,6 +701,95 @@ class TestGitDagBundle:
         files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()}
         assert {"test_dag.py"} == files_in_repo
 
+    @mock.patch("airflow.providers.git.bundles.git.GitHook")
+    def test_tracking_ref_commit_sha_rollback(self, mock_githook, git_repo):
+        """Ensure tracking_ref accepts a full commit SHA, and rolling back to 
an
+        already-local SHA succeeds after a config-driven bundle re-creation.
+        """
+        repo_path, repo = git_repo
+        mock_githook.return_value.repo_url = repo_path
+        first_commit = repo.head.commit
+
+        file_path = repo_path / "new_test.py"
+        with open(file_path, "w") as f:
+            f.write("hello world")
+        repo.index.add([file_path])
+        second_commit = repo.index.commit("Another commit")
+
+        # Initial deploy pinned to the second commit's SHA; both commits are 
already
+        # fetched into local storage since the origin repo had them at clone 
time.
+        bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, 
tracking_ref=second_commit.hexsha)
+        bundle.initialize()
+        assert _version_str(bundle.get_current_version()) == 
second_commit.hexsha
+        files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()}
+        assert {"test_dag.py", "new_test.py"} == files_in_repo
+        assert_repo_is_closed(bundle)
+
+        # Rollback: config change re-creates the bundle pointed back at the 
first commit's
+        # SHA. That commit's objects are already in local storage, so it 
succeeds.
+        bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, 
tracking_ref=first_commit.hexsha)
+        bundle.initialize()
+        assert _version_str(bundle.get_current_version()) == 
first_commit.hexsha
+        files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()}
+        assert {"test_dag.py"} == files_in_repo
+
+    @mock.patch("airflow.providers.git.bundles.git.GitHook")
+    def 
test_tracking_ref_commit_sha_promote_fails_without_clearing_storage(self, 
mock_githook, git_repo):
+        """A SHA created after the bundle's local storage was first populated 
can't be
+        promoted to in-place: the working clone never fetches it before 
checkout.
+
+        This documents a known limitation rather than desired behavior -- it 
should start
+        passing once the fix tracked at 
https://github.com/apache/airflow/issues/71388 lands,
+        at which point this test should be updated to assert success instead.
+        """
+        repo_path, repo = git_repo
+        mock_githook.return_value.repo_url = repo_path
+        first_commit = repo.head.commit
+
+        bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, 
tracking_ref=first_commit.hexsha)
+        bundle.initialize()
+        assert _version_str(bundle.get_current_version()) == 
first_commit.hexsha
+        assert_repo_is_closed(bundle)
+
+        # Created after the bundle's local storage already exists.
+        file_path = repo_path / "new_test.py"
+        with open(file_path, "w") as f:
+            f.write("hello world")
+        repo.index.add([file_path])
+        second_commit = repo.index.commit("Another commit")
+
+        # Promote in-place: config change re-creates the bundle against the 
same local
+        # storage. The new commit's objects were never fetched into the 
working clone.
+        bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, 
tracking_ref=second_commit.hexsha)
+        with pytest.raises(GitCommandError, match="reference is not a 
tree|unable to read tree"):
+            bundle.initialize()
+
+    @mock.patch("airflow.providers.git.bundles.git.GitHook")
+    def test_tracking_ref_commit_sha_promote_succeeds_with_fresh_storage(self, 
mock_githook, git_repo):
+        """Promoting a SHA-pinned tracking_ref to a new commit succeeds once 
local storage
+        is cleared (e.g. a fresh pod), since that re-clones from the updated 
bare mirror.
+        """
+        repo_path, repo = git_repo
+        mock_githook.return_value.repo_url = repo_path
+        first_commit = repo.head.commit
+
+        bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, 
tracking_ref=first_commit.hexsha)
+        bundle.initialize()
+        assert_repo_is_closed(bundle)
+
+        file_path = repo_path / "new_test.py"
+        with open(file_path, "w") as f:
+            f.write("hello world")
+        repo.index.add([file_path])
+        second_commit = repo.index.commit("Another commit")
+
+        # Different bundle name -> fresh local storage, simulating a freshly 
started pod.
+        bundle = GitDagBundle(name="test-fresh", git_conn_id=CONN_HTTPS, 
tracking_ref=second_commit.hexsha)
+        bundle.initialize()
+        assert _version_str(bundle.get_current_version()) == 
second_commit.hexsha
+        files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()}
+        assert {"test_dag.py", "new_test.py"} == files_in_repo
+
     @mock.patch("airflow.providers.git.bundles.git.GitHook")
     def test_refresh_after_force_push_does_not_reclone(self, mock_githook, 
git_repo):
         """Refresh after force-push must fetch+reset, never clone."""

Reply via email to