From 66e033b42ebcd8ebee54ed33934f80038a3a01e7 Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Fri, 10 Jul 2026 16:18:01 -0700 Subject: [PATCH 1/6] Document commit SHA support for GitDagBundle tracking_ref GitDagBundle.tracking_ref already works with a full commit SHA in practice (checkout, promote, and rollback), but the provider docs only described it as a branch or tag. Clarify the docstring and provider docs, add a commit-SHA example, note static-ref refresh behavior, and add test coverage for SHA-pinned tracking_ref. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 25 ++++++++ .../src/airflow/providers/git/bundles/git.py | 2 +- .../git/tests/unit/git/bundles/test_git.py | 60 +++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index d551235bb860a..ce43eb8a4777a 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -41,3 +41,28 @@ 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, which is useful when combined with ``refresh_interval`` to +pick up new changes automatically. Tags and commit SHAs point to a fixed commit (assuming tags are +not moved), so they're useful for pinning a bundle to known-good code. + +When ``tracking_ref`` is a commit SHA, new commits pushed to the repository are not picked up by +``refresh_interval``, since a SHA always refers to the same commit. To promote or roll back to a +different SHA, update the bundle configuration's ``tracking_ref`` to the desired SHA. diff --git a/providers/git/src/airflow/providers/git/bundles/git.py b/providers/git/src/airflow/providers/git/bundles/git.py index 275aa6b3f634b..d1a9621936036 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) diff --git a/providers/git/tests/unit/git/bundles/test_git.py b/providers/git/tests/unit/git/bundles/test_git.py index 65ebbfd085ef5..9f8db63c46105 100644 --- a/providers/git/tests/unit/git/bundles/test_git.py +++ b/providers/git/tests/unit/git/bundles/test_git.py @@ -701,6 +701,66 @@ def test_refresh_tag_moved_forward_and_backward(self, mock_githook, git_repo): 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_supports_commit_sha(self, mock_githook, git_repo): + """Ensure that tracking_ref accepts a full commit SHA and checks out that commit.""" + repo_path, repo = git_repo + mock_githook.return_value.repo_url = repo_path + first_commit = repo.head.commit + + # Add a second commit so the SHA-pinned bundle can be verified against it + file_path = repo_path / "new_test.py" + with open(file_path, "w") as f: + f.write("hello world") + repo.index.add([file_path]) + repo.index.commit("Another 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 + + files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()} + assert {"test_dag.py"} == files_in_repo + + assert_repo_is_closed(bundle) + + @mock.patch("airflow.providers.git.bundles.git.GitHook") + def test_tracking_ref_commit_sha_promote_and_rollback(self, mock_githook, git_repo): + """Ensure a SHA-pinned tracking_ref can be promoted to a new SHA and rolled back. + + This mirrors how a bundle config change is applied in practice: a new bundle + object is created with the updated ``tracking_ref``. + """ + 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 first commit's SHA + 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 + + # Promote: config change re-creates the bundle pointed at the second commit's SHA + 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 + + # Rollback: config change re-creates the bundle pointed back at the first commit's SHA + 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_refresh_after_force_push_does_not_reclone(self, mock_githook, git_repo): """Refresh after force-push must fetch+reset, never clone.""" From 8fe81d9c4aadc5e2c4121789095d072d1fd66a32 Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Fri, 10 Jul 2026 16:22:03 -0700 Subject: [PATCH 2/6] Clarify that promoting a SHA-pinned tracking_ref requires a restart Unlike moving a branch or tag ref, repointing tracking_ref to a new commit SHA is a dag_bundle_config_list config change, so it only takes effect after the Dag processor and workers are restarted to reload the configuration. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index ce43eb8a4777a..e9650de1c67d2 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -60,9 +60,13 @@ bundle to that exact commit: ]' Branches move as new commits are pushed, which is useful when combined with ``refresh_interval`` to -pick up new changes automatically. Tags and commit SHAs point to a fixed commit (assuming tags are -not moved), so they're useful for pinning a bundle to known-good code. +pick up new changes automatically without any restart. Tags and commit SHAs point to a fixed commit +(assuming tags are not moved), so they're useful for pinning a bundle to known-good code. When ``tracking_ref`` is a commit SHA, new commits pushed to the repository are not picked up by -``refresh_interval``, since a SHA always refers to the same commit. To promote or roll back to a -different SHA, update the bundle configuration's ``tracking_ref`` to the desired SHA. +``refresh_interval``, since a SHA always refers to the same commit. Promoting or rolling back to a +different SHA means changing the ``tracking_ref`` value itself, which is an ``[dag_processor] +dag_bundle_config_list`` config change like any other: it takes effect only after the Dag processor +and workers are restarted to reload the configuration. This is unlike moving a branch/tag ref, where +the config is unchanged and the existing ``refresh_interval`` polling picks up the new commit with no +restart required. From 12bd11ba97235a590459f82fbed2c58a63b8310d Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Fri, 10 Jul 2026 16:23:33 -0700 Subject: [PATCH 3/6] Distinguish tracking_ref promotion from Dag bundle version rerun Clarify that promoting/rolling back a SHA-pinned tracking_ref (an environment-level config change) is a different mechanism from Dag bundle versioning, which lets individual runs be rerun with their original commit regardless of the bundle's current tracking_ref. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index e9650de1c67d2..8a8805ac21fdf 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -70,3 +70,9 @@ dag_bundle_config_list`` config change like any other: it takes effect only afte and workers are restarted to reload the configuration. This is unlike moving a branch/tag ref, where the config is unchanged and the existing ``refresh_interval`` polling picks up the new commit with no restart required. + +This is also distinct from Dag bundle versioning. Each Dag run already records the commit its bundle +resolved to, so it can be rerun with that exact code regardless of the bundle's current +``tracking_ref`` (see :ref:`rerun_with_latest_version `). +That only affects individual reruns; it does not change which commit new Dag parses and runs use. +Promoting or rolling back the active environment still requires changing ``tracking_ref`` itself. From 2f9bc7e22d530f2ea9e745d54c32b0fedec4f530 Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Fri, 10 Jul 2026 16:26:23 -0700 Subject: [PATCH 4/6] Simplify tracking_ref docs note and drop redundant SHA test The promote/rollback test already exercises basic SHA checkout as its first step, so the standalone test was redundant. Also tighten the docs note down to two paragraphs without dropping any of the facts. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 25 +++++--------- .../git/tests/unit/git/bundles/test_git.py | 34 ++++--------------- 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index 8a8805ac21fdf..87bc2fdd120ba 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -59,20 +59,13 @@ bundle to that exact commit: } ]' -Branches move as new commits are pushed, which is useful when combined with ``refresh_interval`` to -pick up new changes automatically without any restart. Tags and commit SHAs point to a fixed commit -(assuming tags are not moved), so they're useful for pinning a bundle to known-good code. +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 promoting or rolling back a SHA means changing ``tracking_ref`` in +``dag_bundle_config_list`` itself, which requires restarting the Dag processor and workers to take +effect. -When ``tracking_ref`` is a commit SHA, new commits pushed to the repository are not picked up by -``refresh_interval``, since a SHA always refers to the same commit. Promoting or rolling back to a -different SHA means changing the ``tracking_ref`` value itself, which is an ``[dag_processor] -dag_bundle_config_list`` config change like any other: it takes effect only after the Dag processor -and workers are restarted to reload the configuration. This is unlike moving a branch/tag ref, where -the config is unchanged and the existing ``refresh_interval`` polling picks up the new commit with no -restart required. - -This is also distinct from Dag bundle versioning. Each Dag run already records the commit its bundle -resolved to, so it can be rerun with that exact code regardless of the bundle's current -``tracking_ref`` (see :ref:`rerun_with_latest_version `). -That only affects individual reruns; it does not change which commit new Dag parses and runs use. -Promoting or rolling back the active environment still requires changing ``tracking_ref`` itself. +This is different from Dag bundle versioning: each Dag run already records the commit its bundle +resolved to and can be rerun with that code regardless of the current ``tracking_ref`` (see +:ref:`rerun_with_latest_version `). That only affects +individual reruns, not which commit future runs use. diff --git a/providers/git/tests/unit/git/bundles/test_git.py b/providers/git/tests/unit/git/bundles/test_git.py index 9f8db63c46105..e3b9431388de0 100644 --- a/providers/git/tests/unit/git/bundles/test_git.py +++ b/providers/git/tests/unit/git/bundles/test_git.py @@ -701,36 +701,13 @@ def test_refresh_tag_moved_forward_and_backward(self, mock_githook, git_repo): 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_supports_commit_sha(self, mock_githook, git_repo): - """Ensure that tracking_ref accepts a full commit SHA and checks out that commit.""" - repo_path, repo = git_repo - mock_githook.return_value.repo_url = repo_path - first_commit = repo.head.commit - - # Add a second commit so the SHA-pinned bundle can be verified against it - file_path = repo_path / "new_test.py" - with open(file_path, "w") as f: - f.write("hello world") - repo.index.add([file_path]) - repo.index.commit("Another 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 - - files_in_repo = {f.name for f in bundle.path.iterdir() if f.is_file()} - assert {"test_dag.py"} == files_in_repo - - assert_repo_is_closed(bundle) - @mock.patch("airflow.providers.git.bundles.git.GitHook") def test_tracking_ref_commit_sha_promote_and_rollback(self, mock_githook, git_repo): - """Ensure a SHA-pinned tracking_ref can be promoted to a new SHA and rolled back. + """Ensure tracking_ref accepts a full commit SHA, and a SHA-pinned bundle can be + promoted to a new SHA and rolled back. - This mirrors how a bundle config change is applied in practice: a new bundle - object is created with the updated ``tracking_ref``. + Promotion/rollback is simulated by creating a new bundle object with the updated + tracking_ref, mirroring how a bundle config change is applied in practice. """ repo_path, repo = git_repo mock_githook.return_value.repo_url = repo_path @@ -746,6 +723,9 @@ def test_tracking_ref_commit_sha_promote_and_rollback(self, mock_githook, git_re 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 + assert_repo_is_closed(bundle) # Promote: config change re-creates the bundle pointed at the second commit's SHA bundle = GitDagBundle(name="test", git_conn_id=CONN_HTTPS, tracking_ref=second_commit.hexsha) From a5cd40f01ff78b21b2843bb5827afcfe30219783 Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Fri, 10 Jul 2026 16:31:28 -0700 Subject: [PATCH 5/6] Drop bundle-versioning digression from tracking_ref docs The rerun_with_latest_version distinction is a separate feature and isn't needed to document tracking_ref's SHA support; keep the note focused on the restart caveat. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index 87bc2fdd120ba..63c627a9cf9a4 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -64,8 +64,3 @@ without a restart. Tags and commit SHAs are static (assuming tags aren't moved), to known-good code — but promoting or rolling back a SHA means changing ``tracking_ref`` in ``dag_bundle_config_list`` itself, which requires restarting the Dag processor and workers to take effect. - -This is different from Dag bundle versioning: each Dag run already records the commit its bundle -resolved to and can be rerun with that code regardless of the current ``tracking_ref`` (see -:ref:`rerun_with_latest_version `). That only affects -individual reruns, not which commit future runs use. From 4563f137c1548dccb97db744ad6d66ff617b7c6d Mon Sep 17 00:00:00 2001 From: Cole Heflin Date: Mon, 13 Jul 2026 09:38:27 -0700 Subject: [PATCH 6/6] Clarify only the Dag processor needs restarting for tracking_ref promotion The doc previously said promoting or rolling back a SHA-pinned tracking_ref requires restarting both the Dag processor and workers, but only the Dag processor needs to be restarted to pick up the change. Co-Authored-By: Otto --- providers/git/docs/bundles/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index 63c627a9cf9a4..9e9fd2c6362b7 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -62,5 +62,4 @@ bundle to that exact commit: 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 promoting or rolling back a SHA means changing ``tracking_ref`` in -``dag_bundle_config_list`` itself, which requires restarting the Dag processor and workers to take -effect. +``dag_bundle_config_list`` itself, which requires restarting the Dag processor to take effect.