Skip to content

Commit 8edb1f2

Browse files
authored
Merge pull request #256 from ricardojdsilva87/feat/support-github-enterprise-api
feat: support github enterprise api
2 parents c5775b2 + 6c372ac commit 8edb1f2

File tree

9 files changed

+372
-115
lines changed

9 files changed

+372
-115
lines changed

.env-example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ UPDATE_EXISTING = ""
2121
GH_APP_ID = ""
2222
GH_INSTALLATION_ID = ""
2323
GH_PRIVATE_KEY = ""
24+
GITHUB_APP_ENTERPRISE_ONLY = ""

.github/linters/.markdown-lint.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ MD013: false
55
MD025: false
66
# duplicate headers
77
MD024: false
8+
# MD033/no-inline-html - Inline HTML
9+
MD033:
10+
# Allowed elements
11+
allowed_elements: [br, li, ul]

README.md

Lines changed: 109 additions & 37 deletions
Large diffs are not rendered by default.

auth.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def auth_to_github(
1010
gh_app_installation_id: int | None,
1111
gh_app_private_key_bytes: bytes,
1212
ghe: str,
13+
gh_app_enterprise_only: bool,
1314
) -> github3.GitHub:
1415
"""
1516
Connect to GitHub.com or GitHub Enterprise, depending on env variables.
@@ -20,18 +21,22 @@ def auth_to_github(
2021
gh_app_installation_id (int | None): the GitHub App Installation ID
2122
gh_app_private_key_bytes (bytes): the GitHub App Private Key
2223
ghe (str): the GitHub Enterprise URL
24+
gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only
2325
2426
Returns:
2527
github3.GitHub: the GitHub connection object
2628
"""
2729
if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
28-
gh = github3.github.GitHub()
30+
if ghe and gh_app_enterprise_only:
31+
gh = github3.github.GitHubEnterprise(url=ghe)
32+
else:
33+
gh = github3.github.GitHub()
2934
gh.login_as_app_installation(
3035
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
3136
)
3237
github_connection = gh
3338
elif ghe and token:
34-
github_connection = github3.github.GitHubEnterprise(ghe, token=token)
39+
github_connection = github3.github.GitHubEnterprise(url=ghe, token=token)
3540
elif token:
3641
github_connection = github3.login(token=token)
3742
else:
@@ -45,12 +50,17 @@ def auth_to_github(
4550

4651

4752
def get_github_app_installation_token(
48-
gh_app_id: str, gh_app_private_key_bytes: bytes, gh_app_installation_id: str
53+
ghe: str,
54+
gh_app_id: str,
55+
gh_app_private_key_bytes: bytes,
56+
gh_app_installation_id: str,
4957
) -> str | None:
5058
"""
5159
Get a GitHub App Installation token.
60+
API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
5261
5362
Args:
63+
ghe (str): the GitHub Enterprise endpoint
5464
gh_app_id (str): the GitHub App ID
5565
gh_app_private_key_bytes (bytes): the GitHub App Private Key
5666
gh_app_installation_id (str): the GitHub App Installation ID
@@ -59,7 +69,8 @@ def get_github_app_installation_token(
5969
str: the GitHub App token
6070
"""
6171
jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id)
62-
url = f"https://api.github.com/app/installations/{gh_app_installation_id}/access_tokens"
72+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
73+
url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens"
6374

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

env.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def get_env_vars(
9898
int | None,
9999
int | None,
100100
bytes,
101+
bool,
101102
str,
102103
str,
103104
list[str],
@@ -132,6 +133,7 @@ def get_env_vars(
132133
gh_app_id (int | None): The GitHub App ID to use for authentication
133134
gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication
134135
gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication
136+
gh_app_enterprise_only (bool): Set this to true if the GH APP is created on GHE and needs to communicate with GHE api only
135137
token (str): The GitHub token to use for authentication
136138
ghe (str): The GitHub Enterprise URL to use for authentication
137139
exempt_repositories_list (list[str]): A list of repositories to exempt from the action
@@ -183,6 +185,7 @@ def get_env_vars(
183185
gh_app_id = get_int_env_var("GH_APP_ID")
184186
gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8")
185187
gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID")
188+
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")
186189

187190
if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
188191
raise ValueError(
@@ -340,6 +343,7 @@ def get_env_vars(
340343
gh_app_id,
341344
gh_app_installation_id,
342345
gh_app_private_key_bytes,
346+
gh_app_enterprise_only,
343347
token,
344348
ghe,
345349
exempt_repositories_list,

evergreen.py

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def main(): # pragma: no cover
2121
gh_app_id,
2222
gh_app_installation_id,
2323
gh_app_private_key,
24+
gh_app_enterprise_only,
2425
token,
2526
ghe,
2627
exempt_repositories_list,
@@ -46,12 +47,17 @@ def main(): # pragma: no cover
4647

4748
# Auth to GitHub.com or GHE
4849
github_connection = auth.auth_to_github(
49-
token, gh_app_id, gh_app_installation_id, gh_app_private_key, ghe
50+
token,
51+
gh_app_id,
52+
gh_app_installation_id,
53+
gh_app_private_key,
54+
ghe,
55+
gh_app_enterprise_only,
5056
)
5157

5258
if not token and gh_app_id and gh_app_installation_id and gh_app_private_key:
5359
token = auth.get_github_app_installation_token(
54-
gh_app_id, gh_app_private_key, gh_app_installation_id
60+
ghe, gh_app_id, gh_app_private_key, gh_app_installation_id
5561
)
5662

5763
# If Project ID is set, lookup the global project ID
@@ -61,7 +67,7 @@ def main(): # pragma: no cover
6167
raise ValueError(
6268
"ORGANIZATION environment variable was not set. Please set it"
6369
)
64-
project_id = get_global_project_id(token, organization, project_id)
70+
project_id = get_global_project_id(ghe, token, organization, project_id)
6571

6672
# Get the repositories from the organization, team name, or list of repositories
6773
repos = get_repos_iterator(
@@ -78,13 +84,13 @@ def main(): # pragma: no cover
7884

7985
# Check all the things to see if repo is eligble for a pr/issue
8086
if repo.full_name in exempt_repositories_list:
81-
print("Skipping " + repo.full_name + " (exempted)")
87+
print(f"Skipping {repo.full_name} (exempted)")
8288
continue
8389
if repo.archived:
84-
print("Skipping " + repo.full_name + " (archived)")
90+
print(f"Skipping {repo.full_name} (archived)")
8591
continue
8692
if repo.visibility.lower() not in filter_visibility:
87-
print("Skipping " + repo.full_name + " (visibility-filtered)")
93+
print(f"Skipping {repo.full_name} (visibility-filtered)")
8894
continue
8995
existing_config = None
9096
filename_list = [".github/dependabot.yaml", ".github/dependabot.yml"]
@@ -97,19 +103,17 @@ def main(): # pragma: no cover
97103

98104
if existing_config and not update_existing:
99105
print(
100-
"Skipping "
101-
+ repo.full_name
102-
+ " (dependabot file already exists and update_existing is False)"
106+
f"Skipping {repo.full_name} (dependabot file already exists and update_existing is False)"
103107
)
104108
continue
105109

106110
if created_after_date and is_repo_created_date_before(
107111
repo.created_at, created_after_date
108112
):
109-
print("Skipping " + repo.full_name + " (created after filter)")
113+
print(f"Skipping {repo.full_name} (created after filter)")
110114
continue
111115

112-
print("Checking " + repo.full_name + " for compatible package managers")
116+
print(f"Checking {repo.full_name} for compatible package managers")
113117
# Try to detect package managers and build a dependabot file
114118
dependabot_file = build_dependabot_file(
115119
repo,
@@ -133,42 +137,36 @@ def main(): # pragma: no cover
133137
if not skip:
134138
print("\tEligible for configuring dependabot.")
135139
count_eligible += 1
136-
print("\tConfiguration:\n" + dependabot_file)
140+
print(f"\tConfiguration:\n {dependabot_file}")
137141
if follow_up_type == "pull":
138142
# Try to detect if the repo already has an open pull request for dependabot
139143
skip = check_pending_pulls_for_duplicates(title, repo)
140144
if not skip:
141145
print("\tEligible for configuring dependabot.")
142146
count_eligible += 1
143-
print("\tConfiguration:\n" + dependabot_file)
147+
print(f"\tConfiguration:\n {dependabot_file}")
144148
continue
145149

146150
# Get dependabot security updates enabled if possible
147151
if enable_security_updates:
148-
if not is_dependabot_security_updates_enabled(repo.owner, repo.name, token):
149-
enable_dependabot_security_updates(repo.owner, repo.name, token)
152+
if not is_dependabot_security_updates_enabled(
153+
ghe, repo.owner, repo.name, token
154+
):
155+
enable_dependabot_security_updates(ghe, repo.owner, repo.name, token)
150156

151157
if follow_up_type == "issue":
152158
skip = check_pending_issues_for_duplicates(title, repo)
153159
if not skip:
154160
count_eligible += 1
155-
body_issue = (
156-
body
157-
+ "\n\n```yaml\n"
158-
+ "# "
159-
+ dependabot_filename_to_use
160-
+ "\n"
161-
+ dependabot_file
162-
+ "\n```"
163-
)
161+
body_issue = f"{body}\n\n```yaml\n# {dependabot_filename_to_use} \n{dependabot_file}\n```"
164162
issue = repo.create_issue(title, body_issue)
165-
print("\tCreated issue " + issue.html_url)
163+
print(f"\tCreated issue {issue.html_url}")
166164
if project_id:
167165
issue_id = get_global_issue_id(
168-
token, organization, repo.name, issue.number
166+
ghe, token, organization, repo.name, issue.number
169167
)
170-
link_item_to_project(token, project_id, issue_id)
171-
print("\tLinked issue to project " + project_id)
168+
link_item_to_project(ghe, token, project_id, issue_id)
169+
print(f"\tLinked issue to project {project_id}")
172170
else:
173171
# Try to detect if the repo already has an open pull request for dependabot
174172
skip = check_pending_pulls_for_duplicates(title, repo)
@@ -186,19 +184,19 @@ def main(): # pragma: no cover
186184
dependabot_filename_to_use,
187185
existing_config,
188186
)
189-
print("\tCreated pull request " + pull.html_url)
187+
print(f"\tCreated pull request {pull.html_url}")
190188
if project_id:
191189
pr_id = get_global_pr_id(
192-
token, organization, repo.name, pull.number
190+
ghe, token, organization, repo.name, pull.number
193191
)
194-
response = link_item_to_project(token, project_id, pr_id)
192+
response = link_item_to_project(ghe, token, project_id, pr_id)
195193
if response:
196-
print("\tLinked pull request to project " + project_id)
194+
print(f"\tLinked pull request to project {project_id}")
197195
except github3.exceptions.NotFoundError:
198196
print("\tFailed to create pull request. Check write permissions.")
199197
continue
200198

201-
print("Done. " + str(count_eligible) + " repositories were eligible.")
199+
print(f"Done. {str(count_eligible)} repositories were eligible.")
202200

203201

204202
def is_repo_created_date_before(repo_created_at: str, created_after_date: str):
@@ -209,11 +207,13 @@ def is_repo_created_date_before(repo_created_at: str, created_after_date: str):
209207
)
210208

211209

212-
def is_dependabot_security_updates_enabled(owner, repo, access_token):
213-
"""Check if Dependabot security updates are enabled at the
214-
/repos/:owner/:repo/automated-security-fixes endpoint using the requests library
210+
def is_dependabot_security_updates_enabled(ghe, owner, repo, access_token):
211+
"""
212+
Check if Dependabot security updates are enabled at the /repos/:owner/:repo/automated-security-fixes endpoint using the requests library
213+
API: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#check-if-automated-security-fixes-are-enabled-for-a-repository
215214
"""
216-
url = f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes"
215+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
216+
url = f"{api_endpoint}/repos/{owner}/{repo}/automated-security-fixes"
217217
headers = {
218218
"Authorization": f"Bearer {access_token}",
219219
"Accept": "application/vnd.github.london-preview+json",
@@ -247,9 +247,13 @@ def check_existing_config(repo, filename):
247247
return None
248248

249249

250-
def enable_dependabot_security_updates(owner, repo, access_token):
251-
"""Enable Dependabot security updates at the /repos/:owner/:repo/automated-security-fixes endpoint using the requests library"""
252-
url = f"https://api.github.com/repos/{owner}/{repo}/automated-security-fixes"
250+
def enable_dependabot_security_updates(ghe, owner, repo, access_token):
251+
"""
252+
Enable Dependabot security updates at the /repos/:owner/:repo/automated-security-fixes endpoint using the requests library
253+
API: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#enable-automated-security-fixes
254+
"""
255+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
256+
url = f"{api_endpoint}/repos/{owner}/{repo}/automated-security-fixes"
253257
headers = {
254258
"Authorization": f"Bearer {access_token}",
255259
"Accept": "application/vnd.github.london-preview+json",
@@ -290,7 +294,7 @@ def check_pending_pulls_for_duplicates(title, repo) -> bool:
290294
skip = False
291295
for pull_request in pull_requests:
292296
if pull_request.title.startswith(title):
293-
print("\tPull request already exists: " + pull_request.html_url)
297+
print(f"\tPull request already exists: {pull_request.html_url}")
294298
skip = True
295299
break
296300
return skip
@@ -302,7 +306,7 @@ def check_pending_issues_for_duplicates(title, repo) -> bool:
302306
skip = False
303307
for issue in issues:
304308
if issue.title.startswith(title):
305-
print("\tIssue already exists: " + issue.html_url)
309+
print(f"\tIssue already exists: {issue.html_url}")
306310
skip = True
307311
break
308312
return skip
@@ -344,9 +348,13 @@ def commit_changes(
344348
return pull
345349

346350

347-
def get_global_project_id(token, organization, number):
348-
"""Fetches the project ID from GitHub's GraphQL API."""
349-
url = "https://api.github.com/graphql"
351+
def get_global_project_id(ghe, token, organization, number):
352+
"""
353+
Fetches the project ID from GitHub's GraphQL API.
354+
API: https://docs.github.com/en/graphql/guides/forming-calls-with-graphql
355+
"""
356+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
357+
url = f"{api_endpoint}/graphql"
350358
headers = {"Authorization": f"Bearer {token}"}
351359
data = {
352360
"query": f'query{{organization(login: "{organization}") {{projectV2(number: {number}){{id}}}}}}'
@@ -366,9 +374,13 @@ def get_global_project_id(token, organization, number):
366374
return None
367375

368376

369-
def get_global_issue_id(token, organization, repository, issue_number):
370-
"""Fetches the issue ID from GitHub's GraphQL API"""
371-
url = "https://api.github.com/graphql"
377+
def get_global_issue_id(ghe, token, organization, repository, issue_number):
378+
"""
379+
Fetches the issue ID from GitHub's GraphQL API
380+
API: https://docs.github.com/en/graphql/guides/forming-calls-with-graphql
381+
"""
382+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
383+
url = f"{api_endpoint}/graphql"
372384
headers = {"Authorization": f"Bearer {token}"}
373385
data = {
374386
"query": f"""
@@ -396,9 +408,13 @@ def get_global_issue_id(token, organization, repository, issue_number):
396408
return None
397409

398410

399-
def get_global_pr_id(token, organization, repository, pr_number):
400-
"""Fetches the pull request ID from GitHub's GraphQL API"""
401-
url = "https://api.github.com/graphql"
411+
def get_global_pr_id(ghe, token, organization, repository, pr_number):
412+
"""
413+
Fetches the pull request ID from GitHub's GraphQL API
414+
API: https://docs.github.com/en/graphql/guides/forming-calls-with-graphql
415+
"""
416+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
417+
url = f"{api_endpoint}/graphql"
402418
headers = {"Authorization": f"Bearer {token}"}
403419
data = {
404420
"query": f"""
@@ -426,9 +442,13 @@ def get_global_pr_id(token, organization, repository, pr_number):
426442
return None
427443

428444

429-
def link_item_to_project(token, project_id, item_id):
430-
"""Links an item (issue or pull request) to a project in GitHub."""
431-
url = "https://api.github.com/graphql"
445+
def link_item_to_project(ghe, token, project_id, item_id):
446+
"""
447+
Links an item (issue or pull request) to a project in GitHub.
448+
API: https://docs.github.com/en/graphql/guides/forming-calls-with-graphql
449+
"""
450+
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
451+
url = f"{api_endpoint}/graphql"
432452
headers = {"Authorization": f"Bearer {token}"}
433453
data = {
434454
"query": f'mutation {{addProjectV2ItemById(input: {{projectId: "{project_id}", contentId: "{item_id}"}}) {{item {{id}}}}}}'

0 commit comments

Comments
 (0)