Skip to content

Commit d080795

Browse files
committed
add GitHub App Authentication as an option for DagBundles
1 parent 06c75db commit d080795

3 files changed

Lines changed: 200 additions & 105 deletions

File tree

providers/git/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ dependencies = [
6464
"GitPython>=3.1.44",
6565
]
6666

67+
# The optional dependencies should be modified in place in the generated file
68+
# Any change in the dependencies is preserved when the file is regenerated
69+
[project.optional-dependencies]
70+
github = [
71+
"pygithub>=2.1.1",
72+
]
73+
6774
[dependency-groups]
6875
dev = [
6976
"apache-airflow",

providers/git/src/airflow/providers/git/hooks/git.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class GitHook(BaseHook):
4848
* ``ssh_config_file`` — path to a custom SSH config file.
4949
* ``host_proxy_cmd`` — SSH ProxyCommand string (e.g. for bastion/jump hosts).
5050
* ``ssh_port`` — non-default SSH port.
51+
* ``github_app_id`` — App ID needed for GitHub App Authentication.
52+
* ``github_installation_id`` — Installation ID needed for GitHub App Authentication.
5153
"""
5254

5355
conn_name_attr = "git_conn_id"
@@ -75,6 +77,8 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]:
7577
"ssh_config_file": "",
7678
"host_proxy_cmd": "",
7779
"ssh_port": "",
80+
"github_app_id": "",
81+
"github_installation_id": "",
7882
}
7983
)
8084
},
@@ -103,10 +107,26 @@ def __init__(
103107
self.host_proxy_cmd = extra.get("host_proxy_cmd")
104108
self.ssh_port: int | None = int(extra["ssh_port"]) if extra.get("ssh_port") else None
105109

110+
# GitHub App Auth Options
111+
self.github_app_id = extra.get("github_app_id")
112+
self.github_installation_id: int | None = (
113+
int(extra["github_installation_id"]) if extra.get("github_installation_id") else None
114+
)
115+
106116
self.env: dict[str, str] = {}
107117

108118
if self.key_file and self.private_key:
109119
raise AirflowException("Both 'key_file' and 'private_key' cannot be provided at the same time")
120+
if (self.github_app_id and not self.github_installation_id) or (
121+
not self.github_app_id and self.github_installation_id
122+
):
123+
raise AirflowException(
124+
"Both 'github_app_id' and 'github_installation_id' must be provided to use GitHub App Authentication"
125+
)
126+
if self.github_app_id and self.github_installation_id and self.private_key:
127+
if not self.key_file and not self.private_key:
128+
raise AirflowException("Missing inline private_key or key_file for GitHub App Auth")
129+
self.user_name, self.auth_token = self._get_github_app_token()
110130
self._process_git_auth_url()
111131

112132
def _build_ssh_command(self, key_path: str | None = None) -> str:
@@ -134,6 +154,17 @@ def _build_ssh_command(self, key_path: str | None = None) -> str:
134154

135155
return " ".join(parts)
136156

157+
def _get_github_app_token(self):
158+
from github import Auth as GithubAuth, Github as GithubClient
159+
160+
github_auth = GithubAuth.AppAuth(
161+
app_id=self.github_app_id, private_key=self.private_key
162+
).get_installation_auth(installation_id=self.github_installation_id)
163+
164+
# Client is needed to generate the token
165+
GithubClient(auth=github_auth)
166+
return "x-access-token", github_auth.token
167+
137168
def _process_git_auth_url(self):
138169
if not isinstance(self.repo_url, str):
139170
return

0 commit comments

Comments
 (0)