Skip to content

Commit a6458ea

Browse files
committed
feat: update github app auth to support client id and app id
1 parent 263ef47 commit a6458ea

4 files changed

Lines changed: 1565 additions & 1620 deletions

File tree

airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2564,8 +2564,6 @@ components:
25642564
- text
25652565
- href
25662566
title: ExtraMenuItem
2567-
description: Define a menu item that can be added to the menu by auth managers
2568-
or plugins.
25692567
GanttResponse:
25702568
properties:
25712569
dag_id:

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

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class GitHook(BaseHook):
4949
* ``ssh_config_file`` — path to a custom SSH config file.
5050
* ``host_proxy_cmd`` — SSH ProxyCommand string (e.g. for bastion/jump hosts).
5151
* ``ssh_port`` — non-default SSH port.
52-
* ``github_app_id`` — GitHub App ID used for GitHub App authentication. Requires the GitHub App
52+
* ``github_client_id`` — GitHub Client ID (or App ID) used for GitHub App authentication. Requires the GitHub App
5353
private key to be provided as a PEM-encoded key via either ``private_key`` (inline) or
5454
``key_file`` (path to key file).
5555
* ``github_installation_id`` — GitHub App installation ID used for GitHub App authentication.
@@ -80,7 +80,7 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]:
8080
"ssh_config_file": "",
8181
"host_proxy_cmd": "",
8282
"ssh_port": "",
83-
"github_app_id": "",
83+
"github_client_id": "",
8484
"github_installation_id": "",
8585
}
8686
)
@@ -111,16 +111,16 @@ def __init__(
111111
self.ssh_port: int | None = int(extra["ssh_port"]) if extra.get("ssh_port") else None
112112

113113
# GitHub App Auth Options
114-
raw_github_app_id = extra.get("github_app_id")
115-
if raw_github_app_id is not None:
114+
raw_github_client_id = extra.get("github_client_id")
115+
if raw_github_client_id is not None:
116116
try:
117-
self.github_app_id: int | None = int(raw_github_app_id)
118-
except (TypeError, ValueError) as exc:
119-
raise ValueError(
120-
f"Invalid 'github_app_id' value {raw_github_app_id!r}. It must be an integer."
121-
) from exc
117+
# Accept either integer or string IDs (GitHubIntegration accepts both)
118+
self.github_client_id: int | str | None = int(raw_github_client_id)
119+
except (TypeError, ValueError):
120+
# Keep as string when it is not an integer
121+
self.github_client_id = str(raw_github_client_id)
122122
else:
123-
self.github_app_id = None
123+
self.github_client_id = None
124124

125125
raw_github_installation_id = extra.get("github_installation_id")
126126
if raw_github_installation_id is not None:
@@ -136,13 +136,13 @@ def __init__(
136136

137137
if self.key_file and self.private_key:
138138
raise AirflowException("Both 'key_file' and 'private_key' cannot be provided at the same time")
139-
if (self.github_app_id and not self.github_installation_id) or (
140-
not self.github_app_id and self.github_installation_id
139+
if (self.github_client_id is not None and self.github_installation_id is None) or (
140+
self.github_client_id is None and self.github_installation_id is not None
141141
):
142142
raise ValueError(
143-
"Both 'github_app_id' and 'github_installation_id' must be provided to use GitHub App Authentication"
143+
"Both 'github_client_id' and 'github_installation_id' must be provided to use GitHub App Authentication"
144144
)
145-
if self.github_app_id and self.github_installation_id:
145+
if self.github_client_id is not None and self.github_installation_id is not None:
146146
if not self.key_file and not self.private_key:
147147
raise ValueError("Missing inline private_key or key_file for GitHub App Auth")
148148
if self.key_file and not self.private_key:
@@ -200,19 +200,18 @@ def _build_ssh_command(self, key_path: str | None = None) -> str:
200200

201201
def _get_github_app_token(self):
202202
try:
203-
from github import Auth as GithubAuth, Github as GithubClient
203+
from github import GithubIntegration
204204
except ImportError as exc:
205205
raise ImportError(
206206
"The PyGithub library is required for GitHub App authentication. Please install it with 'pip install apache-airflow-providers-git[github]'"
207207
) from exc
208208

209-
github_auth = GithubAuth.AppAuth(
210-
app_id=self.github_app_id, private_key=self.github_app_private_key
211-
).get_installation_auth(installation_id=self.github_installation_id)
209+
integration = GithubIntegration(
210+
integration_id=self.github_client_id, private_key=self.github_app_private_key
211+
)
212+
access_token = integration.get_access_token(installation_id=self.github_installation_id).token
212213

213-
# Client is needed to generate the token even though we don't use the client directly
214-
GithubClient(auth=github_auth)
215-
return "x-access-token", github_auth.token
214+
return "x-access-token", access_token
216215

217216
def _process_git_auth_url(self):
218217
if not isinstance(self.repo_url, str):

providers/git/tests/unit/git/hooks/test_git.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def setup_connections(self, create_connection_without_db):
151151
host=AIRFLOW_HTTPS_URL,
152152
conn_type="git",
153153
extra={
154-
"github_app_id": "12345",
154+
"github_client_id": "12345",
155155
"github_installation_id": "67890",
156156
"private_key": "inline_pem_key",
157157
},
@@ -162,7 +162,7 @@ def setup_connections(self, create_connection_without_db):
162162
conn_id=CONN_APP_ONLY_APP_ID,
163163
host=AIRFLOW_HTTPS_URL,
164164
conn_type="git",
165-
extra={"github_app_id": "12345"},
165+
extra={"github_client_id": "12345"},
166166
)
167167
)
168168
create_connection_without_db(
@@ -178,7 +178,7 @@ def setup_connections(self, create_connection_without_db):
178178
conn_id=CONN_APP_NO_KEY,
179179
host=AIRFLOW_HTTPS_URL,
180180
conn_type="git",
181-
extra={"github_app_id": "12345", "github_installation_id": "67890"},
181+
extra={"github_client_id": "12345", "github_installation_id": "67890"},
182182
)
183183
)
184184
create_connection_without_db(
@@ -187,7 +187,7 @@ def setup_connections(self, create_connection_without_db):
187187
host=AIRFLOW_HTTPS_URL,
188188
conn_type="git",
189189
extra={
190-
"github_app_id": "not_an_int",
190+
"github_client_id": "not_an_int",
191191
"github_installation_id": "67890",
192192
"private_key": "inline_pem_key",
193193
},
@@ -199,7 +199,7 @@ def setup_connections(self, create_connection_without_db):
199199
host=AIRFLOW_HTTPS_URL,
200200
conn_type="git",
201201
extra={
202-
"github_app_id": "12345",
202+
"github_client_id": "12345",
203203
"github_installation_id": "not_an_int",
204204
"private_key": "inline_pem_key",
205205
},
@@ -442,34 +442,34 @@ def test_passphrase_askpass_cleaned_up(self, create_connection_without_db):
442442

443443
def test_only_app_id_without_installation_id_raises(self):
444444
with pytest.raises(
445-
AirflowException, match="Both 'github_app_id' and 'github_installation_id' must be provided"
445+
ValueError, match="Both 'github_client_id' and 'github_installation_id' must be provided"
446446
):
447447
GitHook(git_conn_id=CONN_APP_ONLY_APP_ID)
448448

449449
def test_only_installation_id_without_app_id_raises(self):
450450
with pytest.raises(
451-
AirflowException,
452-
match="Both 'github_app_id' and 'github_installation_id' must be provided",
451+
ValueError,
452+
match="Both 'github_client_id' and 'github_installation_id' must be provided",
453453
):
454454
GitHook(git_conn_id=CONN_APP_ONLY_INSTALLATION_ID)
455455

456456
def test_app_id_and_installation_id_without_key_raises(self):
457457
with pytest.raises(
458-
AirflowException,
458+
ValueError,
459459
match="Missing inline private_key or key_file for GitHub App Auth",
460460
):
461461
GitHook(git_conn_id=CONN_APP_NO_KEY)
462462

463463
def test_invalid_github_app_id_raises(self):
464464
with pytest.raises(
465-
AirflowException,
466-
match="Invalid 'github_app_id' value",
465+
ValueError,
466+
match="Invalid 'github_client_id' value",
467467
):
468468
GitHook(git_conn_id=CONN_APP_INVALID_APP_ID)
469469

470470
def test_invalid_github_installation_id_raises(self):
471471
with pytest.raises(
472-
AirflowException,
472+
ValueError,
473473
match="Invalid 'github_installation_id' value",
474474
):
475475
GitHook(git_conn_id=CONN_APP_INVALID_INSTALLATION_ID)
@@ -483,7 +483,7 @@ def test_app_auth_with_key_file_reads_file(self, create_connection_without_db, t
483483
host=AIRFLOW_HTTPS_URL,
484484
conn_type="git",
485485
extra={
486-
"github_app_id": "12345",
486+
"github_client_id": "12345",
487487
"github_installation_id": "67890",
488488
"key_file": str(key_file),
489489
},
@@ -505,13 +505,13 @@ def test_app_auth_with_missing_key_file_raises(self, create_connection_without_d
505505
host=AIRFLOW_HTTPS_URL,
506506
conn_type="git",
507507
extra={
508-
"github_app_id": "12345",
508+
"github_client_id": "12345",
509509
"github_installation_id": "67890",
510510
"key_file": "/nonexistent/path/key.pem",
511511
},
512512
)
513513
)
514-
with pytest.raises(AirflowException, match="Failed to read GitHub App private key file"):
514+
with pytest.raises(OSError, match="Failed to read GitHub App private key file"):
515515
GitHook(git_conn_id="git_app_missing_key_file")
516516

517517
def test_app_auth_success_injects_token_into_https_url(self):
@@ -535,7 +535,7 @@ def test_app_auth_success_stores_app_id_and_installation_id(self):
535535
lambda self: ("x-access-token", mock_token),
536536
)
537537
hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
538-
assert hook.github_app_id == 12345
538+
assert hook.github_client_id == 12345
539539
assert hook.github_installation_id == 67890
540540

541541
@pytest.mark.parametrize(
@@ -554,7 +554,7 @@ def test_app_id_and_installation_id_parsed_as_int(
554554
host=AIRFLOW_HTTPS_URL,
555555
conn_type="git",
556556
extra={
557-
"github_app_id": app_id,
557+
"github_client_id": app_id,
558558
"github_installation_id": installation_id,
559559
"private_key": "inline_pem_key",
560560
},
@@ -566,5 +566,5 @@ def test_app_id_and_installation_id_parsed_as_int(
566566
lambda self: ("x-access-token", "token"),
567567
)
568568
hook = GitHook(git_conn_id="git_app_int_check")
569-
assert isinstance(hook.github_app_id, int)
569+
assert isinstance(hook.github_client_id, int)
570570
assert isinstance(hook.github_installation_id, int)

0 commit comments

Comments
 (0)