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 9a21400bcc9 Add GitHub App authentication for git DAG bundles (#64422)
9a21400bcc9 is described below

commit 9a21400bcc95e5e0ab480714ed6514327b8c20e7
Author: RaphCodec <[email protected]>
AuthorDate: Tue Aug 25 13:49:23 2026 -0400

    Add GitHub App authentication for git DAG bundles (#64422)
    
    * add GitHub App Authentication as an option for DagBundles in git provider
    
    * update github app tests for git provider
    
    * add auto generated provider docs for optional github dependency
    
    * add pygithub to dev group for git provider
    
    * simplify if clauses, require empty password for github app auth, and add 
return types
    
    * updated github app fields check to fix errors when not using github app 
auth
    
    * update docs to include github app auth description
    
    * remove pygithub upper cap and add localized git cmds to askpass
    
    * fix pygithub lower bounds static test failure
    
    ---------
    
    Co-authored-by: Raphael <[email protected]>
---
 providers/git/docs/connections/git.rst             |  36 +++
 providers/git/docs/index.rst                       |  17 ++
 providers/git/pyproject.toml                       |   8 +
 .../git/src/airflow/providers/git/hooks/git.py     | 120 ++++++++-
 providers/git/tests/unit/git/hooks/test_git.py     | 288 +++++++++++++++++++++
 uv.lock                                            |   9 +
 6 files changed, 476 insertions(+), 2 deletions(-)

diff --git a/providers/git/docs/connections/git.rst 
b/providers/git/docs/connections/git.rst
index 8bde13c1719..9799bbdd0f1 100644
--- a/providers/git/docs/connections/git.rst
+++ b/providers/git/docs/connections/git.rst
@@ -116,3 +116,39 @@ Extra (optional)
             "strict_host_key_checking": "yes",
             "known_hosts_file": "/path/to/known_hosts"
         }
+
+    **GitHub App authentication:**
+
+    In order to use GitHub App authentication the ``github`` extra needs to be 
installed:
+
+    .. code-block:: bash
+
+        pip install 'apache-airflow-providers-git[github]'
+
+    * ``github_app_id``: The App ID of your GitHub App. Note that the GitHub 
App Client ID can also be used.
+    * ``github_installation_id``: The installation ID of your GitHub app.
+    * ``key_file``: Path to a PEM-encoded private key file for your GitHub App.
+    * ``private_key``: An inline PEM-encoded private key string. When 
provided, the hook writes it
+      to a temporary file and uses it for the GitHub App connection.
+      Mutually exclusive with ``key_file``.
+
+
+    Example with key file:
+
+    .. code-block:: json
+
+        {
+            "github_app_id": "1234567",
+            "github_installation_id": "67890",
+            "key_file": "/path/to/private-key.pem"
+        }
+
+    Example with inline private key:
+
+    .. code-block:: json
+
+        {
+            "github_app_id": "1234567",
+            "github_installation_id": "67890",
+            "private_key": "<content of your PEM-encoded private key>"
+        }
diff --git a/providers/git/docs/index.rst b/providers/git/docs/index.rst
index 4a0209f4bf2..beb270f9921 100644
--- a/providers/git/docs/index.rst
+++ b/providers/git/docs/index.rst
@@ -100,6 +100,23 @@ PIP package                                 Version 
required
 ``GitPython``                               ``>=3.1.44``
 ==========================================  ==================
 
+Optional dependencies
+---------------------
+
+These extras install optional third-party libraries that enable additional 
features of the provider.
+Install them when installing from PyPI. For example:
+
+.. code-block:: bash
+
+    pip install apache-airflow-providers-git[github]
+
+
+==========  ===================
+Extra       Dependencies
+==========  ===================
+``github``  ``PyGithub>=2.1.1``
+==========  ===================
+
 Downloading official packages
 -----------------------------
 
diff --git a/providers/git/pyproject.toml b/providers/git/pyproject.toml
index 664dbe6edfc..a4e4dab4240 100644
--- a/providers/git/pyproject.toml
+++ b/providers/git/pyproject.toml
@@ -64,6 +64,13 @@ dependencies = [
     "GitPython>=3.1.44",
 ]
 
+# The optional dependencies should be modified in place in the generated file
+# Any change in the dependencies is preserved when the file is regenerated
+[project.optional-dependencies]
+github = [
+    "PyGithub>=2.1.1",
+]
+
 [dependency-groups]
 dev = [
     "apache-airflow",
@@ -71,6 +78,7 @@ dev = [
     "apache-airflow-devel-common",
     "apache-airflow-providers-common-compat",
     # Additional devel dependencies (do not remove this line and add extra 
development dependencies)
+    "PyGithub>=2.1.1"
 ]
 
 # To build docs:
diff --git a/providers/git/src/airflow/providers/git/hooks/git.py 
b/providers/git/src/airflow/providers/git/hooks/git.py
index 0eafece941a..0f59fde3f2b 100644
--- a/providers/git/src/airflow/providers/git/hooks/git.py
+++ b/providers/git/src/airflow/providers/git/hooks/git.py
@@ -26,11 +26,13 @@ import shlex
 import stat
 import tempfile
 import warnings
+from collections.abc import Generator
+from datetime import datetime, timedelta, timezone
 from typing import Any
 from urllib.parse import quote as urlquote
 
 from airflow.exceptions import AirflowProviderDeprecationWarning
-from airflow.providers.common.compat.sdk import BaseHook
+from airflow.providers.common.compat.sdk import 
AirflowOptionalProviderFeatureException, BaseHook
 
 log = logging.getLogger(__name__)
 
@@ -53,6 +55,10 @@ class GitHook(BaseHook):
     * ``ssh_config_file`` — path to a custom SSH config file.
     * ``host_proxy_cmd`` — SSH ProxyCommand string (e.g. for bastion/jump 
hosts).
     * ``ssh_port`` — non-default SSH port.
+    * ``github_app_id`` — GitHub App ID used for GitHub App authentication. 
Requires the GitHub App
+      private key to be provided as a PEM-encoded key via either 
``private_key`` (inline) or
+      ``key_file`` (path to key file).
+    * ``github_installation_id`` — GitHub App installation ID used for GitHub 
App authentication.
     """
 
     conn_name_attr = "git_conn_id"
@@ -80,6 +86,8 @@ class GitHook(BaseHook):
                         "ssh_config_file": "",
                         "host_proxy_cmd": "",
                         "ssh_port": "",
+                        "github_app_id": "",
+                        "github_installation_id": "",
                     }
                 )
             },
@@ -110,6 +118,11 @@ class GitHook(BaseHook):
         self.host_proxy_cmd = extra.get("host_proxy_cmd")
         self.ssh_port: int | None = int(extra["ssh_port"]) if 
extra.get("ssh_port") else None
 
+        # GitHub App Auth Options
+        self.github_app_id = extra.get("github_app_id")
+        self.github_installation_id = extra.get("github_installation_id")
+        self.github_app_token_exp: datetime | None = None
+
         self.env: dict[str, str] = {}
 
         if self.key_file and self.private_key:
@@ -127,6 +140,21 @@ class GitHook(BaseHook):
                 AirflowProviderDeprecationWarning,
                 stacklevel=2,
             )
+        github_app_fields = (self.github_app_id, self.github_installation_id)
+        if any(github_app_fields) and not all(github_app_fields):
+            raise ValueError(
+                "Both 'github_app_id' and 'github_installation_id' must be 
provided to use GitHub App Authentication"
+            )
+        if all(github_app_fields):
+            if self.auth_token:
+                raise ValueError("Password field must be empty to use GitHub 
App Auth")
+            if not (self.repo_url or "").startswith(("https://";, "http://";)):
+                raise ValueError(
+                    f"GitHub App authentication requires an HTTPS repository 
URL, but got: {self.repo_url!r}"
+                )
+            if self.key_file and not self.private_key:
+                with open(self.key_file, encoding="utf-8") as key_file:
+                    self.private_key = key_file.read()
         self._process_git_auth_url()
 
     _VALID_STRICT_HOST_KEY_CHECKING = frozenset({"yes", "no", "accept-new", 
"off", "ask"})
@@ -183,7 +211,89 @@ class GitHook(BaseHook):
 
         return " ".join(parts)
 
-    def _process_git_auth_url(self):
+    def _get_github_app_token(self):
+        try:
+            from github import Auth, GithubIntegration
+        except ImportError as exc:
+            raise AirflowOptionalProviderFeatureException(
+                "The PyGithub library is required for GitHub App 
authentication. Please install it with 'pip install 
apache-airflow-providers-git[github]'"
+            ) from exc
+
+        auth = Auth.AppAuth(self.github_app_id, self.private_key)
+        integration = GithubIntegration(auth=auth)
+        access_token = 
integration.get_access_token(installation_id=self.github_installation_id)
+        github_app_token_exp = access_token.expires_at
+        log.info(
+            "Successfully obtained GitHub App installation access token 
(expires at: %s)",
+            github_app_token_exp,
+        )
+
+        return "x-access-token", access_token.token, github_app_token_exp
+
+    def _ensure_github_app_token(self) -> None:
+        TOKEN_REFRESH_BUFFER = timedelta(minutes=5)
+        if (
+            self.github_app_token_exp is None
+            or self.github_app_token_exp < datetime.now(timezone.utc) + 
TOKEN_REFRESH_BUFFER
+        ):
+            log.info(
+                "GitHub App token is missing or near expiry (expires at: %s). 
Refreshing token.",
+                self.github_app_token_exp,
+            )
+            self.user_name, self.auth_token, self.github_app_token_exp = 
self._get_github_app_token()
+
+    @contextlib.contextmanager
+    def _github_app_askpass_env(self) -> Generator[None]:
+        if not self.auth_token:
+            yield
+            return
+
+        token = shlex.quote(self.auth_token)
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=True) 
as askpass_script:
+            askpass_script.write(
+                "#!/bin/sh\n"
+                'case "$1" in\n'
+                "  *Username*) echo x-access-token;;\n"
+                f"  *Password*) echo {token};;\n"
+                f"  *) echo {token};;\n"
+                "esac\n"
+            )
+            askpass_script.flush()
+            os.chmod(askpass_script.name, stat.S_IRWXU)
+
+            old_askpass = os.environ.get("GIT_ASKPASS")
+            old_lc_all = os.environ.get("LC_ALL")
+            old_terminal_prompt = os.environ.get("GIT_TERMINAL_PROMPT")
+            try:
+                os.environ["GIT_ASKPASS"] = askpass_script.name
+                os.environ["GIT_TERMINAL_PROMPT"] = "0"
+                self.env["GIT_ASKPASS"] = askpass_script.name
+                self.env["LC_ALL"] = "C"
+                self.env["GIT_TERMINAL_PROMPT"] = "0"
+                yield
+            finally:
+                if old_askpass is None:
+                    self.env.pop("GIT_ASKPASS", None)
+                    os.environ.pop("GIT_ASKPASS", None)
+                else:
+                    self.env["GIT_ASKPASS"] = old_askpass
+                    os.environ["GIT_ASKPASS"] = old_askpass
+
+                if old_lc_all is None:
+                    self.env.pop("LC_ALL", None)
+                    os.environ.pop("LC_ALL", None)
+                else:
+                    self.env["LC_ALL"] = old_lc_all
+                    os.environ["LC_ALL"] = old_lc_all
+
+                if old_terminal_prompt is None:
+                    self.env.pop("GIT_TERMINAL_PROMPT", None)
+                    os.environ.pop("GIT_TERMINAL_PROMPT", None)
+                else:
+                    self.env["GIT_TERMINAL_PROMPT"] = old_terminal_prompt
+                    os.environ["GIT_TERMINAL_PROMPT"] = old_terminal_prompt
+
+    def _process_git_auth_url(self) -> None:
         if not isinstance(self.repo_url, str):
             return
         if self.auth_token and self.repo_url.startswith("https://";):
@@ -240,6 +350,12 @@ class GitHook(BaseHook):
 
     @contextlib.contextmanager
     def configure_hook_env(self):
+        if self.github_app_id is not None and self.github_installation_id is 
not None:
+            self._ensure_github_app_token()
+            with self._github_app_askpass_env():
+                yield
+            return
+
         if self.private_key:
             with tempfile.NamedTemporaryFile(mode="w", delete=True) as 
tmp_keyfile:
                 tmp_keyfile.write(self.private_key)
diff --git a/providers/git/tests/unit/git/hooks/test_git.py 
b/providers/git/tests/unit/git/hooks/test_git.py
index 484791fe19c..30b8717dd70 100644
--- a/providers/git/tests/unit/git/hooks/test_git.py
+++ b/providers/git/tests/unit/git/hooks/test_git.py
@@ -51,6 +51,12 @@ CONN_ONLY_PATH = "my_git_conn_only_path"
 CONN_ONLY_INLINE_KEY = "my_git_conn_only_inline_key"
 CONN_BOTH_PATH_INLINE = "my_git_conn_both_path_inline"
 CONN_NO_REPO_URL = "my_git_conn_no_repo_url"
+CONN_APP_INLINE_KEY = "git_app_inline_key"
+CONN_APP_ONLY_APP_ID = "git_app_only_app_id"
+CONN_APP_ONLY_INSTALLATION_ID = "git_app_only_installation_id"
+CONN_APP_NO_KEY = "git_app_no_key"
+CONN_APP_INVALID_APP_ID = "git_app_invalid_app_id"
+CONN_APP_INVALID_INSTALLATION_ID = "git_app_invalid_installation_id"
 
 
 @pytest.fixture
@@ -122,6 +128,85 @@ class TestGitHook:
                 },
             )
         )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_BOTH_PATH_INLINE,
+                host="path/to/repo",
+                conn_type="git",
+                extra={
+                    "key_file": "path/to/key",
+                    "private_key": "inline_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id="my_git_conn_strict",
+                host=AIRFLOW_GIT,
+                conn_type="git",
+                extra='{"key_file": "/files/pkey.pem", 
"strict_host_key_checking": "yes"}',
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INLINE_KEY,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_ONLY_APP_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_app_id": "12345"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_ONLY_INSTALLATION_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_installation_id": "67890"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_NO_KEY,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_app_id": "12345", "github_installation_id": 
"67890"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INVALID_APP_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "not_an_int",
+                    "github_installation_id": "67890",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INVALID_INSTALLATION_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "not_an_int",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
 
     @pytest.mark.parametrize(
         ("conn_id", "hook_kwargs", "expected_repo_url", "warns_on_default"),
@@ -413,3 +498,206 @@ class TestGitHook:
             assert os.path.exists(askpass_path)
         # Both the askpass script and the temp key file should be cleaned up
         assert not os.path.exists(askpass_path)
+
+    # --- GitHub App auth tests ---
+
+    def test_only_app_id_without_installation_id_raises(self):
+        with pytest.raises(
+            ValueError, match="Both 'github_app_id' and 
'github_installation_id' must be provided"
+        ):
+            GitHook(git_conn_id=CONN_APP_ONLY_APP_ID)
+
+    def test_only_installation_id_without_app_id_raises(self):
+        with pytest.raises(
+            ValueError,
+            match="Both 'github_app_id' and 'github_installation_id' must be 
provided",
+        ):
+            GitHook(git_conn_id=CONN_APP_ONLY_INSTALLATION_ID)
+
+    def 
test_app_id_and_installation_id_without_key_does_not_raise_on_init(self):
+        hook = GitHook(git_conn_id=CONN_APP_NO_KEY)
+
+        assert hook.github_app_id == "12345"
+        assert hook.github_installation_id == "67890"
+        assert hook.private_key is None
+
+    def test_app_auth_with_key_file_reads_file(self, 
create_connection_without_db, tmp_path, monkeypatch):
+        key_file = tmp_path / "app_key.pem"
+        key_file.write_text("file_pem_key_content")
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_key_file",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "key_file": str(key_file),
+                },
+            )
+        )
+        from datetime import datetime, timedelta, timezone
+
+        mock_expiry = datetime.now(timezone.utc) + timedelta(hours=1)
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            lambda self: ("x-access-token", "ghs_test_token", mock_expiry),
+        )
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id="git_app_key_file")
+
+        assert hook.private_key == "file_pem_key_content"
+
+    def test_app_auth_with_missing_key_file_raises(self, 
create_connection_without_db):
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_missing_key_file",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "key_file": "/nonexistent/path/key.pem",
+                },
+            )
+        )
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            with pytest.raises(FileNotFoundError):
+                GitHook(git_conn_id="git_app_missing_key_file")
+
+    def test_app_auth_defers_token_fetch(self, monkeypatch):
+        """GitHub App token is not fetched in __init__, only on 
configure_hook_env."""
+        from datetime import datetime, timedelta, timezone
+
+        mock_called = []
+
+        def mock_get_token(self):
+            mock_called.append(True)
+            return ("x-access-token", "ghs_test_token", 
datetime.now(timezone.utc) + timedelta(hours=1))
+
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            mock_get_token,
+        )
+        # __init__ should NOT call _get_github_app_token
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert len(mock_called) == 0
+        assert hook.auth_token is None
+        assert hook.github_app_id == "12345"
+        assert hook.github_installation_id == "67890"
+
+        # First call to configure_hook_env should trigger the token fetch
+        with hook.configure_hook_env():
+            assert len(mock_called) == 1
+            assert hook.auth_token == "ghs_test_token"
+            assert hook.user_name == "x-access-token"
+
+    def test_app_auth_success_stores_app_id_and_installation_id(self):
+        """App ID and installation ID are stored at __init__ time."""
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert hook.github_app_id == "12345"
+        assert hook.github_installation_id == "67890"
+
+    @pytest.mark.parametrize(
+        ("app_id", "installation_id"),
+        [
+            ("12345", "67890"),
+            (12345, 67890),
+        ],
+    )
+    def test_app_id_and_installation_id_are_stored_as_provided(
+        self, app_id, installation_id, create_connection_without_db, 
monkeypatch
+    ):
+        from datetime import datetime, timedelta, timezone
+
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_int_check",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": app_id,
+                    "github_installation_id": installation_id,
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            lambda self: ("x-access-token", "token", 
datetime.now(timezone.utc) + timedelta(hours=1)),
+        )
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id="git_app_int_check")
+        assert hook.github_app_id == app_id
+        assert hook.github_installation_id == installation_id
+
+    def test_github_app_token_refresh_near_expiry(self, monkeypatch):
+        """Token is refreshed when near expiry during configure_hook_env."""
+        from datetime import datetime, timedelta, timezone
+
+        mock_get_token_call_count = [0]
+
+        def mock_get_token(self):
+            mock_get_token_call_count[0] += 1
+            # First call returns token expiring in 3 minutes
+            if mock_get_token_call_count[0] == 1:
+                return (
+                    "x-access-token",
+                    f"token_{mock_get_token_call_count[0]}",
+                    datetime.now(timezone.utc) + timedelta(minutes=3),
+                )
+            # Second call (refresh) returns token expiring in 1 hour
+            return (
+                "x-access-token",
+                f"token_{mock_get_token_call_count[0]}",
+                datetime.now(timezone.utc) + timedelta(hours=1),
+            )
+
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            mock_get_token,
+        )
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert mock_get_token_call_count[0] == 0  # No call in __init__
+
+        # First configure_hook_env triggers first token fetch
+        with hook.configure_hook_env():
+            assert mock_get_token_call_count[0] == 1
+            assert hook.auth_token == "token_1"
+
+        # Second configure_hook_env triggers refresh (token near expiry)
+        with hook.configure_hook_env():
+            assert mock_get_token_call_count[0] == 2
+            assert hook.auth_token == "token_2"
+
+    def test_github_app_integration_call_shape(self, monkeypatch):
+        """Verify GithubIntegration is called with correct arguments."""
+        from datetime import datetime, timedelta, timezone
+        from unittest import mock
+
+        mock_integration = mock.MagicMock()
+        mock_access_token = mock.MagicMock()
+        mock_access_token.token = "ghs_test_token"
+        mock_access_token.expires_at = datetime.now(timezone.utc) + 
timedelta(hours=1)
+        mock_integration.get_access_token.return_value = mock_access_token
+
+        import sys
+        from types import SimpleNamespace
+
+        fake_github = SimpleNamespace(
+            Auth=SimpleNamespace(AppAuth=lambda app_id, key: "auth"),
+            GithubIntegration=lambda auth: mock_integration,
+        )
+        monkeypatch.setitem(sys.modules, "github", fake_github)
+
+        with pytest.warns(AirflowProviderDeprecationWarning, 
match="accept-new"):
+            hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        with hook.configure_hook_env():
+            # Verify get_access_token was called with installation_id kwarg
+            assert mock_integration.get_access_token.call_count == 1
+            _, kwargs = mock_integration.get_access_token.call_args
+            assert "installation_id" in kwargs
+            assert str(kwargs["installation_id"]) == "67890"
diff --git a/uv.lock b/uv.lock
index 5ebd3ca54a1..8e2e61c1860 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5694,12 +5694,18 @@ dependencies = [
     { name = "gitpython" },
 ]
 
+[package.optional-dependencies]
+github = [
+    { name = "pygithub" },
+]
+
 [package.dev-dependencies]
 dev = [
     { name = "apache-airflow" },
     { name = "apache-airflow-devel-common" },
     { name = "apache-airflow-providers-common-compat" },
     { name = "apache-airflow-task-sdk" },
+    { name = "pygithub" },
 ]
 docs = [
     { name = "apache-airflow-devel-common", extra = ["docs"] },
@@ -5710,7 +5716,9 @@ requires-dist = [
     { name = "apache-airflow", editable = "." },
     { name = "apache-airflow-providers-common-compat", editable = 
"providers/common/compat" },
     { name = "gitpython", specifier = ">=3.1.44" },
+    { name = "pygithub", marker = "extra == 'github'", specifier = ">=2.1.1" },
 ]
+provides-extras = ["github"]
 
 [package.metadata.requires-dev]
 dev = [
@@ -5718,6 +5726,7 @@ dev = [
     { name = "apache-airflow-devel-common", editable = "devel-common" },
     { name = "apache-airflow-providers-common-compat", editable = 
"providers/common/compat" },
     { name = "apache-airflow-task-sdk", editable = "task-sdk" },
+    { name = "pygithub", specifier = ">=2.1.1" },
 ]
 docs = [{ name = "apache-airflow-devel-common", extras = ["docs"], editable = 
"devel-common" }]
 

Reply via email to