diff --git a/providers/git/docs/bundles/index.rst b/providers/git/docs/bundles/index.rst index d551235bb860a..9e9fd2c6362b7 100644 --- a/providers/git/docs/bundles/index.rst +++ b/providers/git/docs/bundles/index.rst @@ -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. 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..e3b9431388de0 100644 --- a/providers/git/tests/unit/git/bundles/test_git.py +++ b/providers/git/tests/unit/git/bundles/test_git.py @@ -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."""