Skip to content
Open
22 changes: 22 additions & 0 deletions providers/git/docs/bundles/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,25 @@ 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 promoting or rolling back a SHA means changing ``tracking_ref`` in
``dag_bundle_config_list`` itself, which requires restarting the Dag processor to take effect.
2 changes: 1 addition & 1 deletion providers/git/src/airflow/providers/git/bundles/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions providers/git/tests/unit/git/bundles/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,46 @@ 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_commit_sha_promote_and_rollback(self, mock_githook, git_repo):
"""Ensure tracking_ref accepts a full commit SHA, and a SHA-pinned bundle can be
promoted to a new SHA and rolled back.

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
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
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)
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."""
Expand Down
Loading