Skip to content
This repository was archived by the owner on May 1, 2025. It is now read-only.

Commit a716356

Browse files
authored
Merge pull request #142 from ricardojdsilva87/feat/support-github-enterprise-api
feat: support github enterprise api
2 parents dbf7801 + 9a3893c commit a716356

File tree

8 files changed

+304
-57
lines changed

8 files changed

+304
-57
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[run]
2+
omit =
3+
# omit test files
4+
test_*.py

.env-example

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
GH_ACTOR=""
1+
GH_ACTOR = ""
22
GH_ENTERPRISE_URL = ""
3-
GH_TOKEN=""
4-
ORGANIZATION=""
5-
PR_TITLE=""
6-
PR_BODY=""
7-
REPOS_JSON_LOCATION=""
3+
GH_TOKEN = ""
4+
ORGANIZATION = ""
5+
PR_TITLE = ""
6+
PR_BODY = ""
7+
REPOS_JSON_LOCATION = ""
88

99
# GITHUB APP
1010
GH_APP_ID = ""
1111
GH_INSTALLATION_ID = ""
1212
GH_PRIVATE_KEY = ""
13+
GITHUB_APP_ENTERPRISE_ONLY = ""

README.md

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ It is desirable, for example, for all Open Source and InnerSource projects to ha
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, GH_ACTOR, PR_TITLE, PR_BODY, and ORGANIZATION) with your information as 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`, `GH_ACTOR`, `PR_TITLE`, `PR_BODY`, and `ORGANIZATION`) with your information as 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/write access to all the repositories in the `repos.json` file.
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/auto-contrib-file.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

@@ -106,6 +107,51 @@ jobs:
106107
PR_BODY: ${{ secrets.PR_BODY }}
107108
```
108109
110+
#### Using GitHub app
111+
112+
```yaml
113+
name: Find proper repos and open CONTRIBUTING.md prs
114+
115+
on:
116+
workflow_dispatch:
117+
118+
permissions:
119+
contents: read
120+
121+
jobs:
122+
build:
123+
name: Open CONTRIBUTING.md in OSS if it doesnt exist
124+
runs-on: ubuntu-latest
125+
permissions:
126+
contents: read
127+
pull-requests: write
128+
129+
steps:
130+
- name: Checkout code
131+
uses: actions/checkout@v4
132+
133+
- name: Find OSS repository in organization
134+
uses: docker://ghcr.io/zkoppert/innersource-crawler:v1
135+
env:
136+
GH_TOKEN: ${{ secrets.GH_TOKEN }}
137+
ORGANIZATION: ${{ secrets.ORGANIZATION }}
138+
TOPIC: open-source
139+
140+
- name: Open pull requests in OSS repository that are missing contrib files
141+
uses: docker://ghcr.io/github/automatic-contrib-prs:v2
142+
env:
143+
GH_APP_ID: ${{ secrets.GH_APP_ID }}
144+
GH_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }}
145+
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
146+
# GITHUB_APP_ENTERPRISE_ONLY: True --> Set to true when created GHE App needs to communicate with GHE api
147+
GH_ENTERPRISE_URL: ${{ github.server_url }}
148+
# GH_TOKEN: ${{ secrets.GH_TOKEN }} --> the token input is not used if the github app inputs are set
149+
ORGANIZATION: ${{ secrets.ORGANIZATION }}
150+
GH_ACTOR: ${{ secrets.GH_ACTOR }}
151+
PR_TITLE: ${{ secrets.PR_TITLE }}
152+
PR_BODY: ${{ secrets.PR_BODY }}
153+
```
154+
109155
## Scaling for large organizations
110156
111157
- GitHub Actions workflows have time limits currently set at 72 hours per run. If you are operating on more than 1400 repos or so with this action, it will take several runs to complete.

auth.py

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,42 @@
1-
"""
2-
This is the module that contains functions related to authenticating to GitHub with
3-
a personal access token or GitHub App, depending on the environment variables set.
4-
"""
1+
"""This is the module that contains functions related to authenticating to GitHub with a personal access token."""
52

63
import github3
4+
import requests
75

86

97
def auth_to_github(
8+
token: str | None,
109
gh_app_id: int | None,
1110
gh_app_installation_id: int | None,
1211
gh_app_private_key_bytes: bytes,
13-
gh_enterprise_url: str | None,
14-
token: str | None,
12+
ghe: str | None,
13+
gh_app_enterprise_only: bool,
1514
) -> github3.GitHub:
1615
"""
1716
Connect to GitHub.com or GitHub Enterprise, depending on env variables.
1817
1918
Args:
20-
gh_app_id (int | None): the GitHub App ID
21-
gh_installation_id (int | None): the GitHub App Installation ID
22-
gh_app_private_key (bytes): the GitHub App Private Key
23-
gh_enterprise_url (str): the GitHub Enterprise URL
2419
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 on GHE and needs to communicate with GHE api only
2525
2626
Returns:
2727
github3.GitHub: the GitHub connection object
2828
"""
29-
3029
if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
31-
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()
3234
gh.login_as_app_installation(
3335
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
3436
)
3537
github_connection = gh
36-
elif gh_enterprise_url and token:
37-
github_connection = github3.github.GitHubEnterprise(
38-
gh_enterprise_url, token=token
39-
)
38+
elif ghe and token:
39+
github_connection = github3.github.GitHubEnterprise(url=ghe, token=token)
4040
elif token:
4141
github_connection = github3.login(token=token)
4242
else:
@@ -47,3 +47,35 @@ def auth_to_github(
4747
if not github_connection:
4848
raise ValueError("Unable to authenticate to GitHub")
4949
return github_connection # type: ignore
50+
51+
52+
def get_github_app_installation_token(
53+
ghe: str | None,
54+
gh_app_id: int | None,
55+
gh_app_private_key_bytes: bytes,
56+
gh_app_installation_id: int | None,
57+
) -> str | None:
58+
"""
59+
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
61+
62+
Args:
63+
ghe (str): the GitHub Enterprise endpoint
64+
gh_app_id (str): the GitHub App ID
65+
gh_app_private_key_bytes (bytes): the GitHub App Private Key
66+
gh_app_installation_id (str): the GitHub App Installation ID
67+
68+
Returns:
69+
str: the GitHub App token
70+
"""
71+
jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id)
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"
74+
75+
try:
76+
response = requests.post(url, headers=jwt_headers, json=None, timeout=5)
77+
response.raise_for_status()
78+
except requests.exceptions.RequestException as e:
79+
print(f"Request failed: {e}")
80+
return None
81+
return response.json().get("token")

env.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@
1111
MAX_BODY_LENGTH = 65536
1212

1313

14+
def get_bool_env_var(env_var_name: str, default: bool = False) -> bool:
15+
"""Get a boolean environment variable.
16+
17+
Args:
18+
env_var_name: The name of the environment variable to retrieve.
19+
default: The default value to return if the environment variable is not set.
20+
21+
Returns:
22+
The value of the environment variable as a boolean.
23+
"""
24+
ev = os.environ.get(env_var_name, "")
25+
if ev == "" and default:
26+
return default
27+
return ev.strip().lower() == "true"
28+
29+
1430
def get_int_env_var(env_var_name: str, default: int = -1) -> int | None:
1531
"""Get an integer environment variable.
1632
@@ -39,8 +55,9 @@ class EnvVars:
3955
gh_app_id (int | None): The GitHub App ID to use for authentication
4056
gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication
4157
gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication
42-
gh_token (str | None): GitHub personal access token (PAT) for API authentication
58+
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
4359
ghe (str): The GitHub Enterprise URL to use for authentication
60+
gh_token (str | None): GitHub personal access token (PAT) for API authentication
4461
gh_actor (str): The GitHub actor to use for authentication
4562
organization (str): The GitHub organization to use for the PR
4663
pr_body (str): The PR body to use for the PR
@@ -54,6 +71,7 @@ def __init__(
5471
gh_app_id: int | None,
5572
gh_app_installation_id: int | None,
5673
gh_app_private_key_bytes: bytes,
74+
gh_app_enterprise_only: bool,
5775
gh_enterprise_url: str | None,
5876
gh_token: str | None,
5977
organization: str | None,
@@ -65,6 +83,7 @@ def __init__(
6583
self.gh_app_id = gh_app_id
6684
self.gh_app_installation_id = gh_app_installation_id
6785
self.gh_app_private_key_bytes = gh_app_private_key_bytes
86+
self.gh_app_enterprise_only = gh_app_enterprise_only
6887
self.gh_enterprise_url = gh_enterprise_url
6988
self.gh_token = gh_token
7089
self.organization = organization
@@ -79,6 +98,7 @@ def __repr__(self):
7998
f"{self.gh_app_id},"
8099
f"{self.gh_app_installation_id},"
81100
f"{self.gh_app_private_key_bytes},"
101+
f"{self.gh_app_enterprise_only}"
82102
f"{self.gh_enterprise_url},"
83103
f"{self.gh_token},"
84104
f"{self.organization},"
@@ -100,6 +120,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
100120
gh_app_id (int | None): The GitHub App ID to use for authentication
101121
gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication
102122
gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication
123+
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
103124
gh_enterprise_url (str): The GitHub Enterprise URL to use for authentication
104125
gh_token (str | None): The GitHub token to use for authentication
105126
organization (str): The GitHub organization to use for the PR
@@ -116,6 +137,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
116137
gh_app_id = get_int_env_var("GH_APP_ID")
117138
gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8")
118139
gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID")
140+
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")
119141

120142
if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
121143
raise ValueError(
@@ -157,6 +179,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
157179
gh_app_id,
158180
gh_app_installation_id,
159181
gh_app_private_key_bytes,
182+
gh_app_enterprise_only,
160183
gh_enterprise_url,
161184
gh_token,
162185
organization,

open_contrib_pr.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,34 @@
1717
pr_title = env_vars.pr_title
1818
repos_json_location = env_vars.repos_json_location
1919
token = env_vars.gh_token
20+
gh_app_id = env_vars.gh_app_id
21+
gh_app_installation_id = env_vars.gh_app_installation_id
22+
gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes
23+
ghe = env_vars.gh_enterprise_url
24+
gh_app_enterprise_only = env_vars.gh_app_enterprise_only
2025

2126
# Auth to GitHub.com
2227
github_connection = auth.auth_to_github(
23-
env_vars.gh_app_id,
24-
env_vars.gh_app_installation_id,
25-
env_vars.gh_app_private_key_bytes,
26-
env_vars.gh_enterprise_url,
2728
token,
29+
gh_app_id,
30+
gh_app_installation_id,
31+
gh_app_private_key_bytes,
32+
ghe,
33+
gh_app_enterprise_only,
2834
)
2935

36+
if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes:
37+
token = auth.get_github_app_installation_token(
38+
ghe, gh_app_id, gh_app_private_key_bytes, gh_app_installation_id
39+
)
40+
41+
endpoint = ghe.removeprefix("https://") if ghe else "github.com"
42+
3043
os.system("git config --global user.name 'GitHub Actions'")
31-
os.system("git config --global user.email 'no-reply@github.com'")
44+
os.system(f"git config --global user.email 'no-reply@{endpoint}'")
3245

3346
# Get innersource repos from organization
34-
os.system(f"git clone https://{gh_actor}:{token}@github.com/{repos_json_location}")
47+
os.system(f"git clone https://{gh_actor}:{token}@{endpoint}/{repos_json_location}")
3548
with open(str(repos_json_location), "r", encoding="utf-8") as repos_file:
3649
innersource_repos = json.loads(repos_file.read())
3750

@@ -46,7 +59,7 @@
4659
repo_full_name = repo["full_name"]
4760
repo_name = repo["name"]
4861
os.system(
49-
f"git clone https://{gh_actor}:{token}@github.com/{repo_full_name}"
62+
f"git clone https://{gh_actor}:{token}@{endpoint}/{repo_full_name}"
5063
)
5164
# checkout a branch called contributing-doc
5265
BRANCH_NAME = "contributing-doc"

0 commit comments

Comments
 (0)