Skip to content

Commit d6c4576

Browse files
authored
Merge pull request #225 from ricardojdsilva87/feat/support-github-enterprise-api
feat: support github enterprise api
2 parents f827303 + 6613b90 commit d6c4576

8 files changed

+160
-190
lines changed

.env-example

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ ADDITIONAL_METRICS = ""
22
GH_APP_ID = ""
33
GH_APP_INSTALLATION_ID = ""
44
GH_APP_PRIVATE_KEY = ""
5+
GITHUB_APP_ENTERPRISE_ONLY = ""
56
GH_ENTERPRISE_URL = ""
67
GH_TOKEN = ""
78
INACTIVE_DAYS = 365

.github/linters/.isort.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
[settings]
22
profile = black
3+
known_third_party = github3,dateutil,dotenv
4+
known_first_party = auth

.github/linters/.python-lint

+4-1
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,10 @@ disable=raw-checker-failed,
431431
use-symbolic-message-instead,
432432
use-implicit-booleaness-not-comparison-to-string,
433433
use-implicit-booleaness-not-comparison-to-zero,
434-
import-error
434+
import-error,
435+
line-too-long,
436+
too-many-arguments,
437+
too-many-positional-arguments
435438

436439
# Enable the message, report, category or checker with the given id(s). You can
437440
# either give multiple identifier separated by comma (,) or put this option

README.md

+8-6
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ All feedback regarding our GitHub Actions, as a whole, should be communicated th
3131
## Use as a GitHub Action
3232

3333
1. Create a repository to host this GitHub Action or select an existing repository.
34-
1. Create the env values from the sample workflow below (GH_TOKEN, ORGANIZATION, EXEMPT_TOPICS) with your information as plain text or repository secrets. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
34+
1. Create the env values from the sample workflow below (`GH_TOKEN`, `ORGANIZATION`, `EXEMPT_TOPICS`) with your information as plain text or repository secrets. More info on creating secrets can be found [here](https://docs.github.com/en/actions/security-guides/encrypted-secrets).
3535
Note: Your GitHub token will need to have read access to all the repositories in the organization that you want evaluated
3636
1. Copy the below example workflow to your repository and put it in the `.github/workflows/` directory with the file extension `.yml` (ie. `.github/workflows/stale_repos.yml`)
3737

@@ -45,11 +45,12 @@ This action can be configured to authenticate with GitHub App Installation or Pe
4545

4646
##### GitHub App Installation
4747

48-
| field | required | default | description |
49-
| ------------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50-
| `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. |
51-
| `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. |
52-
| `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. |
48+
| field | required | default | description |
49+
| ---------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50+
| `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. |
51+
| `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. |
52+
| `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. |
53+
| `GITHUB_APP_ENTERPRISE_ONLY` | False | `false` | Set this input to `true` if your app is created in GHE and communicates with GHE. |
5354

5455
##### Personal Access Token (PAT)
5556

@@ -243,6 +244,7 @@ jobs:
243244
GH_APP_ID: ${{ secrets.GH_APP_ID }}
244245
GH_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }}
245246
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
247+
#GITHUB_APP_ENTERPRISE_ONLY: true --> Set this if the gh app was created in GHE and the endpoint is also a GHE instance
246248
ORGANIZATION: ${{ secrets.ORGANIZATION }}
247249
EXEMPT_TOPICS: "keep,template"
248250
INACTIVE_DAYS: 365

auth.py

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""This is the module that contains functions related to authenticating to GitHub with a personal access token."""
2+
3+
import github3
4+
5+
6+
def auth_to_github(
7+
token: str,
8+
gh_app_id: int | None,
9+
gh_app_installation_id: int | None,
10+
gh_app_private_key_bytes: bytes,
11+
ghe: str,
12+
gh_app_enterprise_only: bool,
13+
) -> github3.GitHub:
14+
"""
15+
Connect to GitHub.com or GitHub Enterprise, depending on env variables.
16+
17+
Args:
18+
token (str): the GitHub personal access token
19+
gh_app_id (int | None): the GitHub App ID
20+
gh_app_installation_id (int | None): the GitHub App Installation ID
21+
gh_app_private_key_bytes (bytes): the GitHub App Private Key
22+
ghe (str): the GitHub Enterprise URL
23+
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
24+
25+
Returns:
26+
github3.GitHub: the GitHub connection object
27+
"""
28+
if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
29+
if ghe and gh_app_enterprise_only:
30+
gh = github3.github.GitHubEnterprise(url=ghe)
31+
else:
32+
gh = github3.github.GitHub()
33+
gh.login_as_app_installation(
34+
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
35+
)
36+
github_connection = gh
37+
elif ghe and token:
38+
github_connection = github3.github.GitHubEnterprise(url=ghe, token=token)
39+
elif token:
40+
github_connection = github3.login(token=token)
41+
else:
42+
raise ValueError(
43+
"GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set"
44+
)
45+
46+
if not github_connection:
47+
raise ValueError("Unable to authenticate to GitHub")
48+
return github_connection # type: ignore

stale_repos.py

+19-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python
22
""" Find stale repositories in a GitHub organization. """
3+
34
import fnmatch
45
import json
56
import os
@@ -10,6 +11,8 @@
1011
from dateutil.parser import parse
1112
from dotenv import load_dotenv
1213

14+
import auth
15+
1316

1417
def main(): # pragma: no cover
1518
"""
@@ -32,8 +35,22 @@ def main(): # pragma: no cover
3235
dotenv_path = join(dirname(__file__), ".env")
3336
load_dotenv(dotenv_path)
3437

35-
# Auth to GitHub.com
36-
github_connection = auth_to_github()
38+
token = os.getenv("ORGANIZATION")
39+
gh_app_id = os.getenv("GH_APP_ID")
40+
gh_app_installation_id = os.getenv("GH_APP_INSTALLATION_ID")
41+
gh_app_private_key = os.getenv("GH_APP_PRIVATE_KEY").encode("utf8")
42+
ghe = os.getenv("GH_ENTERPRISE_URL")
43+
gh_app_enterprise_only = os.getenv("GITHUB_APP_ENTERPRISE_ONLY")
44+
45+
# Auth to GitHub.com or GHE
46+
github_connection = auth.auth_to_github(
47+
token,
48+
gh_app_id,
49+
gh_app_installation_id,
50+
gh_app_private_key,
51+
ghe,
52+
gh_app_enterprise_only,
53+
)
3754

3855
# Set the threshold for inactive days
3956
inactive_days_threshold = os.getenv("INACTIVE_DAYS")
@@ -346,32 +363,6 @@ def get_int_env_var(env_var_name):
346363
return None
347364

348365

349-
def auth_to_github():
350-
"""Connect to GitHub.com or GitHub Enterprise, depending on env variables."""
351-
gh_app_id = get_int_env_var("GH_APP_ID")
352-
gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8")
353-
gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID")
354-
ghe = os.getenv("GH_ENTERPRISE_URL", default="").strip()
355-
token = os.getenv("GH_TOKEN")
356-
357-
if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
358-
gh = github3.github.GitHub()
359-
gh.login_as_app_installation(
360-
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
361-
)
362-
github_connection = gh
363-
elif ghe and token:
364-
github_connection = github3.github.GitHubEnterprise(ghe, token=token)
365-
elif token:
366-
github_connection = github3.login(token=os.getenv("GH_TOKEN"))
367-
else:
368-
raise ValueError("GH_TOKEN environment variable not set")
369-
370-
if not github_connection:
371-
raise ValueError("Unable to authenticate to GitHub")
372-
return github_connection # type: ignore
373-
374-
375366
def set_repo_data(
376367
repo, days_inactive, active_date_disp, visibility, additional_metrics
377368
):

test_auth.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Test cases for the auth module."""
2+
3+
import unittest
4+
from unittest.mock import MagicMock, patch
5+
6+
import auth
7+
8+
9+
class TestAuth(unittest.TestCase):
10+
"""
11+
Test case for the auth module.
12+
"""
13+
14+
@patch("github3.login")
15+
def test_auth_to_github_with_token(self, mock_login):
16+
"""
17+
Test the auth_to_github function when the token is provided.
18+
"""
19+
mock_login.return_value = "Authenticated to GitHub.com"
20+
21+
result = auth.auth_to_github("token", "", "", b"", "", False)
22+
23+
self.assertEqual(result, "Authenticated to GitHub.com")
24+
25+
def test_auth_to_github_without_token(self):
26+
"""
27+
Test the auth_to_github function when the token is not provided.
28+
Expect a ValueError to be raised.
29+
"""
30+
with self.assertRaises(ValueError) as context_manager:
31+
auth.auth_to_github("", "", "", b"", "", False)
32+
the_exception = context_manager.exception
33+
self.assertEqual(
34+
str(the_exception),
35+
"GH_TOKEN or the set of [GH_APP_ID, GH_APP_INSTALLATION_ID, GH_APP_PRIVATE_KEY] environment variables are not set",
36+
)
37+
38+
@patch("github3.github.GitHubEnterprise")
39+
def test_auth_to_github_with_ghe(self, mock_ghe):
40+
"""
41+
Test the auth_to_github function when the GitHub Enterprise URL is provided.
42+
"""
43+
mock_ghe.return_value = "Authenticated to GitHub Enterprise"
44+
result = auth.auth_to_github(
45+
"token", "", "", b"", "https://github.example.com", False
46+
)
47+
48+
self.assertEqual(result, "Authenticated to GitHub Enterprise")
49+
50+
@patch("github3.github.GitHubEnterprise")
51+
def test_auth_to_github_with_ghe_and_ghe_app(self, mock_ghe):
52+
"""
53+
Test the auth_to_github function when the GitHub Enterprise URL is provided and the app was created in GitHub Enterprise URL.
54+
"""
55+
mock = mock_ghe.return_value
56+
mock.login_as_app_installation = MagicMock(return_value=True)
57+
result = auth.auth_to_github(
58+
"", "123", "123", b"123", "https://github.example.com", True
59+
)
60+
mock.login_as_app_installation.assert_called_once()
61+
self.assertEqual(result, mock)
62+
63+
@patch("github3.github.GitHub")
64+
def test_auth_to_github_with_app(self, mock_gh):
65+
"""
66+
Test the auth_to_github function when app credentials are provided
67+
"""
68+
mock = mock_gh.return_value
69+
mock.login_as_app_installation = MagicMock(return_value=True)
70+
result = auth.auth_to_github(
71+
"", "123", "123", b"123", "https://github.example.com", False
72+
)
73+
mock.login_as_app_installation.assert_called_once()
74+
self.assertEqual(result, mock)
75+
76+
77+
if __name__ == "__main__":
78+
unittest.main()

0 commit comments

Comments
 (0)