Skip to content

Commit baa4523

Browse files
authored
Merge pull request #418 from ricardojdsilva87/feat/support-github-enterprise-api
feat: support github enterprise api
2 parents aba98f6 + d92a46c commit baa4523

11 files changed

+131
-45
lines changed

Diff for: .env-example

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
GH_APP_ID=""
22
GH_APP_INSTALLATION_ID=""
33
GH_APP_PRIVATE_KEY=""
4+
GITHUB_APP_ENTERPRISE_ONLY=""
45
GH_ENTERPRISE_URL = ""
56
GH_TOKEN = ""
67
HIDE_AUTHOR = "false"

Diff for: README.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,12 @@ This action can be configured to authenticate with GitHub App Installation or Pe
128128

129129
##### GitHub App Installation
130130

131-
| field | required | default | description |
132-
| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133-
| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
134-
| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
135-
| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
131+
| field | required | default | description |
132+
| ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133+
| `GH_APP_ID` | True | `""` | GitHub Application ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
134+
| `GH_APP_INSTALLATION_ID` | True | `""` | GitHub Application Installation ID. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
135+
| `GH_APP_PRIVATE_KEY` | True | `""` | GitHub Application Private Key. See [documentation](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/about-authentication-with-a-github-app) for more details. |
136+
| `GITHUB_APP_ENTERPRISE_ONLY` | False | false | Set this input to `true` if your app is created in GHE and communicates with GHE. |
136137

137138
##### Personal Access Token (PAT)
138139

Diff for: auth.py

+34-14
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,78 @@
1-
"""
2-
This is the module that contains functions related to authenticating
3-
to GitHub.
4-
"""
1+
"""This is the module that contains functions related to authenticating to GitHub with a personal access token."""
52

63
import github3
74
import requests
85

96

107
def auth_to_github(
11-
gh_app_id: str,
12-
gh_app_installation_id: int,
13-
gh_app_private_key_bytes: bytes,
148
token: str,
9+
gh_app_id: int | None,
10+
gh_app_installation_id: int | None,
11+
gh_app_private_key_bytes: bytes,
1512
ghe: str,
13+
gh_app_enterprise_only: bool,
1614
) -> github3.GitHub:
1715
"""
1816
Connect to GitHub.com or GitHub Enterprise, depending on env variables.
1917
18+
Args:
19+
token (str): the GitHub personal access token
20+
gh_app_id (int | None): the GitHub App ID
21+
gh_app_installation_id (int | None): the GitHub App Installation ID
22+
gh_app_private_key_bytes (bytes): the GitHub App Private Key
23+
ghe (str): the GitHub Enterprise URL
24+
gh_app_enterprise_only (bool): Set this to true if the GH APP is created
25+
on GHE and needs to communicate with GHE api only
26+
2027
Returns:
21-
github3.GitHub: A github api connection.
28+
github3.GitHub: the GitHub connection object
2229
"""
23-
2430
if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
25-
gh = github3.github.GitHub()
31+
if ghe and gh_app_enterprise_only:
32+
gh = github3.github.GitHubEnterprise(url=ghe)
33+
else:
34+
gh = github3.github.GitHub()
2635
gh.login_as_app_installation(
2736
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
2837
)
2938
github_connection = gh
3039
elif ghe and token:
31-
github_connection = github3.github.GitHubEnterprise(ghe, token=token)
40+
github_connection = github3.github.GitHubEnterprise(url=ghe, token=token)
3241
elif token:
3342
github_connection = github3.login(token=token)
3443
else:
3544
raise ValueError(
36-
"GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set"
45+
"GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, \
46+
GH_APP_PRIVATE_KEY] environment variables are not set"
3747
)
3848

49+
if not github_connection:
50+
raise ValueError("Unable to authenticate to GitHub")
3951
return github_connection # type: ignore
4052

4153

4254
def get_github_app_installation_token(
43-
gh_app_id: str, gh_app_private_key_bytes: bytes, gh_app_installation_id: str
55+
ghe: str,
56+
gh_app_id: str,
57+
gh_app_private_key_bytes: bytes,
58+
gh_app_installation_id: str,
4459
) -> str | None:
4560
"""
4661
Get a GitHub App Installation token.
62+
API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
63+
4764
Args:
65+
ghe (str): the GitHub Enterprise endpoint
4866
gh_app_id (str): the GitHub App ID
4967
gh_app_private_key_bytes (bytes): the GitHub App Private Key
5068
gh_app_installation_id (str): the GitHub App Installation ID
69+
5170
Returns:
5271
str: the GitHub App token
5372
"""
5473
jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id)
55-
url = f"https://api.github.com/app/installations/{gh_app_installation_id}/access_tokens"
74+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
75+
url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens"
5676

5777
try:
5878
response = requests.post(url, headers=jwt_headers, json=None, timeout=5)

Diff for: config.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class EnvVars:
3939
hide_time_to_first_response (bool): If true, the time to first response metric is hidden
4040
in the output
4141
ignore_users (List[str]): List of usernames to ignore when calculating metrics
42-
labels_to_measure (List[str]): List of labels to measure how much time the lable is applied
42+
labels_to_measure (List[str]): List of labels to measure how much time the label is applied
4343
enable_mentor_count (bool): If set to TRUE, compute number of mentors
4444
min_mentor_comments (str): If set, defines the minimum number of comments for mentors
4545
max_comments_eval (str): If set, defines the maximum number of comments to look
@@ -48,7 +48,7 @@ class EnvVars:
4848
involved commentors in
4949
search_query (str): Search query used to filter issues/prs/discussions on GitHub
5050
non_mentioning_links (bool): If set to TRUE, links do not cause a notification
51-
in the desitnation repository
51+
in the destination repository
5252
report_title (str): The title of the report
5353
output_file (str): The name of the file to write the report to
5454
rate_limit_bypass (bool): If set to TRUE, bypass the rate limit for the GitHub API
@@ -61,6 +61,7 @@ def __init__(
6161
gh_app_id: int | None,
6262
gh_app_installation_id: int | None,
6363
gh_app_private_key_bytes: bytes,
64+
gh_app_enterprise_only: bool,
6465
gh_token: str | None,
6566
ghe: str | None,
6667
hide_author: bool,
@@ -85,6 +86,7 @@ def __init__(
8586
self.gh_app_id = gh_app_id
8687
self.gh_app_installation_id = gh_app_installation_id
8788
self.gh_app_private_key_bytes = gh_app_private_key_bytes
89+
self.gh_app_enterprise_only = gh_app_enterprise_only
8890
self.gh_token = gh_token
8991
self.ghe = ghe
9092
self.ignore_users = ignore_user
@@ -112,6 +114,7 @@ def __repr__(self):
112114
f"{self.gh_app_id},"
113115
f"{self.gh_app_installation_id},"
114116
f"{self.gh_app_private_key_bytes},"
117+
f"{self.gh_app_enterprise_only},"
115118
f"{self.gh_token},"
116119
f"{self.ghe},"
117120
f"{self.hide_author},"
@@ -186,6 +189,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
186189
gh_app_id = get_int_env_var("GH_APP_ID")
187190
gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8")
188191
gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID")
192+
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")
189193

190194
if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
191195
raise ValueError(
@@ -235,6 +239,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
235239
gh_app_id,
236240
gh_app_installation_id,
237241
gh_app_private_key_bytes,
242+
gh_app_enterprise_only,
238243
gh_token,
239244
ghe,
240245
hide_author,

Diff for: discussions.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import requests
1111

1212

13-
def get_discussions(token: str, search_query: str):
13+
def get_discussions(token: str, search_query: str, ghe: str):
1414
"""Get a list of discussions in a GitHub repository that match the search query.
1515
1616
Args:
@@ -51,9 +51,10 @@ def get_discussions(token: str, search_query: str):
5151
variables = {"query": search_query}
5252

5353
# Send the GraphQL request
54+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
5455
headers = {"Authorization": f"Bearer {token}"}
5556
response = requests.post(
56-
"https://api.github.com/graphql",
57+
f"{api_endpoint}/graphql",
5758
json={"query": query, "variables": variables},
5859
headers=headers,
5960
timeout=60,

Diff for: issue_metrics.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -192,23 +192,27 @@ def main(): # pragma: no cover
192192
output_file = env_vars.output_file
193193
rate_limit_bypass = env_vars.rate_limit_bypass
194194

195+
ghe = env_vars.ghe
195196
gh_app_id = env_vars.gh_app_id
196197
gh_app_installation_id = env_vars.gh_app_installation_id
197198
gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes
198-
199-
if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes:
200-
token = get_github_app_installation_token(
201-
gh_app_id, gh_app_private_key_bytes, gh_app_installation_id
202-
)
199+
gh_app_enterprise_only = env_vars.gh_app_enterprise_only
203200

204201
# Auth to GitHub.com
205202
github_connection = auth_to_github(
203+
token,
206204
gh_app_id,
207205
gh_app_installation_id,
208206
gh_app_private_key_bytes,
209-
token,
210-
env_vars.ghe,
207+
ghe,
208+
gh_app_enterprise_only,
211209
)
210+
211+
if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes:
212+
token = get_github_app_installation_token(
213+
ghe, gh_app_id, gh_app_private_key_bytes, gh_app_installation_id
214+
)
215+
212216
enable_mentor_count = env_vars.enable_mentor_count
213217
min_mentor_count = int(env_vars.min_mentor_comments)
214218
max_comments_eval = int(env_vars.max_comments_eval)
@@ -236,7 +240,7 @@ def main(): # pragma: no cover
236240
raise ValueError(
237241
"The search query for discussions cannot include labels to measure"
238242
)
239-
issues = get_discussions(token, search_query)
243+
issues = get_discussions(token, search_query, ghe)
240244
if len(issues) <= 0:
241245
print("No discussions found")
242246
write_to_markdown(

Diff for: markdown_writer.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def write_to_markdown(
100100
non_mentioning_links=False,
101101
report_title="",
102102
output_file="",
103+
ghe="",
103104
) -> None:
104105
"""Write the issues with metrics to a markdown file.
105106
@@ -114,7 +115,7 @@ def write_to_markdown(
114115
file (file object, optional): The file object to write to. If not provided,
115116
a file named "issue_metrics.md" will be created.
116117
num_issues_opened (int): The Number of items that remain opened.
117-
num_issues_closed (int): The number of issues that were closedi.
118+
num_issues_closed (int): The number of issues that were closed.
118119
num_mentor_count (int): The number of very active commentors.
119120
labels (List[str]): A list of the labels that are used in the issues.
120121
search_query (str): The search query used to find the issues.
@@ -126,6 +127,7 @@ def write_to_markdown(
126127
in the destination repository
127128
report_title (str): The title of the report
128129
output_file (str): The name of the file to write the report to
130+
ghe (str): the GitHub Enterprise endpoint
129131
130132
Returns:
131133
None.
@@ -185,15 +187,19 @@ def write_to_markdown(
185187
# Replace any whitespace
186188
issue.title = issue.title.strip()
187189

190+
endpoint = ghe.removeprefix("https://") if ghe else "github.com"
188191
if non_mentioning_links:
189192
file.write(
190193
f"| {issue.title} | "
191-
f"{issue.html_url.replace('https://github.com', 'https://www.github.com')} |"
194+
f"{issue.html_url}".replace(
195+
f"https://{endpoint}", f"https://www.{endpoint}"
196+
)
197+
+ " |"
192198
)
193199
else:
194-
file.write(f"| {issue.title} | " f"{issue.html_url} |")
200+
file.write(f"| {issue.title} | {issue.html_url} |")
195201
if "Author" in columns:
196-
file.write(f" [{issue.author}](https://github.com/{issue.author}) |")
202+
file.write(f" [{issue.author}](https://{endpoint}/{issue.author}) |")
197203
if "Time to first response" in columns:
198204
file.write(f" {issue.time_to_first_response} |")
199205
if "Time to close" in columns:

Diff for: test_auth.py

+25-8
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,49 @@ def test_auth_to_github_with_github_app(self, mock_login):
2525
parameters provided.
2626
"""
2727
mock_login.return_value = MagicMock()
28-
result = auth_to_github(12345, 678910, b"hello", "", "")
28+
result = auth_to_github("", 12345, 678910, b"hello", "", False)
2929

30-
self.assertIsInstance(result, github3.github.GitHub)
30+
self.assertIsInstance(result, github3.github.GitHub, False)
3131

3232
def test_auth_to_github_with_token(self):
3333
"""
3434
Test the auth_to_github function when the token is provided.
3535
"""
36-
result = auth_to_github(None, None, b"", "token", "")
36+
result = auth_to_github("token", None, None, b"", "", False)
3737

38-
self.assertIsInstance(result, github3.github.GitHub)
38+
self.assertIsInstance(result, github3.github.GitHub, False)
3939

4040
def test_auth_to_github_without_authentication_information(self):
4141
"""
4242
Test the auth_to_github function when authentication information is not provided.
4343
Expect a ValueError to be raised.
4444
"""
4545
with self.assertRaises(ValueError):
46-
auth_to_github(None, None, b"", "", "")
46+
auth_to_github("", None, None, b"", "", False)
4747

4848
def test_auth_to_github_with_ghe(self):
4949
"""
5050
Test the auth_to_github function when the GitHub Enterprise URL is provided.
5151
"""
52-
result = auth_to_github(None, None, b"", "token", "https://github.example.com")
52+
result = auth_to_github(
53+
"token", None, None, b"", "https://github.example.com", False
54+
)
55+
56+
self.assertIsInstance(result, github3.github.GitHubEnterprise, False)
5357

54-
self.assertIsInstance(result, github3.github.GitHubEnterprise)
58+
@patch("github3.github.GitHubEnterprise")
59+
def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe):
60+
"""
61+
Test the auth_to_github function when the GitHub Enterprise URL \
62+
is provided and the app was created in GitHub Enterprise URL.
63+
"""
64+
mock = mock_ghe.return_value
65+
mock.login_as_app_installation = MagicMock(return_value=True)
66+
result = auth_to_github(
67+
"", "123", "123", b"123", "https://github.example.com", True
68+
)
69+
mock.login_as_app_installation.assert_called_once()
70+
self.assertEqual(result, mock)
5571

5672
@patch("github3.apps.create_jwt_headers", MagicMock(return_value="gh_token"))
5773
@patch("requests.post")
@@ -64,9 +80,10 @@ def test_get_github_app_installation_token(self, mock_post):
6480
mock_response.raise_for_status.return_value = None
6581
mock_response.json.return_value = {"token": dummy_token}
6682
mock_post.return_value = mock_response
83+
mock_ghe = ""
6784

6885
result = get_github_app_installation_token(
69-
b"gh_private_token", "gh_app_id", "gh_installation_id"
86+
mock_ghe, b"gh_private_token", "gh_app_id", "gh_installation_id"
7087
)
7188

7289
self.assertEqual(result, dummy_token)

0 commit comments

Comments
 (0)