Skip to content
This repository was archived by the owner on May 1, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[run]
omit =
# omit test files
test_*.py
13 changes: 7 additions & 6 deletions .env-example
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
GH_ACTOR=""
GH_ACTOR = ""
GH_ENTERPRISE_URL = ""
GH_TOKEN=""
ORGANIZATION=""
PR_TITLE=""
PR_BODY=""
REPOS_JSON_LOCATION=""
GH_TOKEN = ""
ORGANIZATION = ""
PR_TITLE = ""
PR_BODY = ""
REPOS_JSON_LOCATION = ""

# GITHUB APP
GH_APP_ID = ""
GH_INSTALLATION_ID = ""
GH_PRIVATE_KEY = ""
GITHUB_APP_ENTERPRISE_ONLY = ""
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ It is desirable, for example, for all Open Source and InnerSource projects to ha
## Use as a GitHub Action

1. Create a repository to host this GitHub Action or select an existing repository.
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).
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).
Note: Your GitHub token will need to have read/write access to all the repositories in the `repos.json` file.
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`)

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

##### GitHub App Installation

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

##### Personal Access Token (PAT)

Expand Down Expand Up @@ -106,6 +107,51 @@ jobs:
PR_BODY: ${{ secrets.PR_BODY }}
```

#### Using GitHub app

```yaml
name: Find proper repos and open CONTRIBUTING.md prs

on:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: Open CONTRIBUTING.md in OSS if it doesnt exist
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Find OSS repository in organization
uses: docker://ghcr.io/zkoppert/innersource-crawler:v1
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
ORGANIZATION: ${{ secrets.ORGANIZATION }}
TOPIC: open-source

- name: Open pull requests in OSS repository that are missing contrib files
uses: docker://ghcr.io/github/automatic-contrib-prs:v2
env:
GH_APP_ID: ${{ secrets.GH_APP_ID }}
GH_APP_INSTALLATION_ID: ${{ secrets.GH_APP_INSTALLATION_ID }}
GH_APP_PRIVATE_KEY: ${{ secrets.GH_APP_PRIVATE_KEY }}
# GITHUB_APP_ENTERPRISE_ONLY: True --> Set to true when created GHE App needs to communicate with GHE api
GH_ENTERPRISE_URL: ${{ github.server_url }}
# GH_TOKEN: ${{ secrets.GH_TOKEN }} --> the token input is not used if the github app inputs are set
ORGANIZATION: ${{ secrets.ORGANIZATION }}
GH_ACTOR: ${{ secrets.GH_ACTOR }}
PR_TITLE: ${{ secrets.PR_TITLE }}
PR_BODY: ${{ secrets.PR_BODY }}
```

## Scaling for large organizations

- 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.
Expand Down
64 changes: 48 additions & 16 deletions auth.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
"""
This is the module that contains functions related to authenticating to GitHub with
a personal access token or GitHub App, depending on the environment variables set.
"""
"""This is the module that contains functions related to authenticating to GitHub with a personal access token."""

import github3
import requests


def auth_to_github(
token: str | None,
gh_app_id: int | None,
gh_app_installation_id: int | None,
gh_app_private_key_bytes: bytes,
gh_enterprise_url: str | None,
token: str | None,
ghe: str | None,
gh_app_enterprise_only: bool,
) -> github3.GitHub:
"""
Connect to GitHub.com or GitHub Enterprise, depending on env variables.

Args:
gh_app_id (int | None): the GitHub App ID
gh_installation_id (int | None): the GitHub App Installation ID
gh_app_private_key (bytes): the GitHub App Private Key
gh_enterprise_url (str): the GitHub Enterprise URL
token (str): the GitHub personal access token
gh_app_id (int | None): the GitHub App ID
gh_app_installation_id (int | None): the GitHub App Installation ID
gh_app_private_key_bytes (bytes): the GitHub App Private Key
ghe (str): the GitHub Enterprise URL
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

Returns:
github3.GitHub: the GitHub connection object
"""

if gh_app_id and gh_app_private_key_bytes and gh_app_installation_id:
gh = github3.github.GitHub()
if ghe and gh_app_enterprise_only:
gh = github3.github.GitHubEnterprise(url=ghe)
else:
gh = github3.github.GitHub()
gh.login_as_app_installation(
gh_app_private_key_bytes, gh_app_id, gh_app_installation_id
)
github_connection = gh
elif gh_enterprise_url and token:
github_connection = github3.github.GitHubEnterprise(
gh_enterprise_url, token=token
)
elif ghe and token:
github_connection = github3.github.GitHubEnterprise(url=ghe, token=token)
elif token:
github_connection = github3.login(token=token)
else:
Expand All @@ -47,3 +47,35 @@ def auth_to_github(
if not github_connection:
raise ValueError("Unable to authenticate to GitHub")
return github_connection # type: ignore


def get_github_app_installation_token(
ghe: str | None,
gh_app_id: int | None,
gh_app_private_key_bytes: bytes,
gh_app_installation_id: int | None,
) -> str | None:
"""
Get a GitHub App Installation token.
API: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation

Args:
ghe (str): the GitHub Enterprise endpoint
gh_app_id (str): the GitHub App ID
gh_app_private_key_bytes (bytes): the GitHub App Private Key
gh_app_installation_id (str): the GitHub App Installation ID

Returns:
str: the GitHub App token
"""
jwt_headers = github3.apps.create_jwt_headers(gh_app_private_key_bytes, gh_app_id)
api_endpoint = f"{ghe}/api/v3" if ghe else "https://api.github.com"
url = f"{api_endpoint}/app/installations/{gh_app_installation_id}/access_tokens"

try:
response = requests.post(url, headers=jwt_headers, json=None, timeout=5)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return None
return response.json().get("token")
25 changes: 24 additions & 1 deletion env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@
MAX_BODY_LENGTH = 65536


def get_bool_env_var(env_var_name: str, default: bool = False) -> bool:
"""Get a boolean environment variable.

Args:
env_var_name: The name of the environment variable to retrieve.
default: The default value to return if the environment variable is not set.

Returns:
The value of the environment variable as a boolean.
"""
ev = os.environ.get(env_var_name, "")
if ev == "" and default:
return default
return ev.strip().lower() == "true"


def get_int_env_var(env_var_name: str, default: int = -1) -> int | None:
"""Get an integer environment variable.

Expand Down Expand Up @@ -39,8 +55,9 @@ class EnvVars:
gh_app_id (int | None): The GitHub App ID to use for authentication
gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication
gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication
gh_token (str | None): GitHub personal access token (PAT) for API authentication
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
ghe (str): The GitHub Enterprise URL to use for authentication
gh_token (str | None): GitHub personal access token (PAT) for API authentication
gh_actor (str): The GitHub actor to use for authentication
organization (str): The GitHub organization to use for the PR
pr_body (str): The PR body to use for the PR
Expand All @@ -54,6 +71,7 @@ def __init__(
gh_app_id: int | None,
gh_app_installation_id: int | None,
gh_app_private_key_bytes: bytes,
gh_app_enterprise_only: bool,
gh_enterprise_url: str | None,
gh_token: str | None,
organization: str | None,
Expand All @@ -65,6 +83,7 @@ def __init__(
self.gh_app_id = gh_app_id
self.gh_app_installation_id = gh_app_installation_id
self.gh_app_private_key_bytes = gh_app_private_key_bytes
self.gh_app_enterprise_only = gh_app_enterprise_only
self.gh_enterprise_url = gh_enterprise_url
self.gh_token = gh_token
self.organization = organization
Expand All @@ -79,6 +98,7 @@ def __repr__(self):
f"{self.gh_app_id},"
f"{self.gh_app_installation_id},"
f"{self.gh_app_private_key_bytes},"
f"{self.gh_app_enterprise_only}"
f"{self.gh_enterprise_url},"
f"{self.gh_token},"
f"{self.organization},"
Expand All @@ -100,6 +120,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
gh_app_id (int | None): The GitHub App ID to use for authentication
gh_app_installation_id (int | None): The GitHub App Installation ID to use for authentication
gh_app_private_key_bytes (bytes): The GitHub App Private Key as bytes to use for authentication
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
gh_enterprise_url (str): The GitHub Enterprise URL to use for authentication
gh_token (str | None): The GitHub token to use for authentication
organization (str): The GitHub organization to use for the PR
Expand All @@ -116,6 +137,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
gh_app_id = get_int_env_var("GH_APP_ID")
gh_app_private_key_bytes = os.environ.get("GH_APP_PRIVATE_KEY", "").encode("utf8")
gh_app_installation_id = get_int_env_var("GH_APP_INSTALLATION_ID")
gh_app_enterprise_only = get_bool_env_var("GITHUB_APP_ENTERPRISE_ONLY")

if gh_app_id and (not gh_app_private_key_bytes or not gh_app_installation_id):
raise ValueError(
Expand Down Expand Up @@ -157,6 +179,7 @@ def get_env_vars(test: bool = False) -> EnvVars:
gh_app_id,
gh_app_installation_id,
gh_app_private_key_bytes,
gh_app_enterprise_only,
gh_enterprise_url,
gh_token,
organization,
Expand Down
27 changes: 20 additions & 7 deletions open_contrib_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,34 @@
pr_title = env_vars.pr_title
repos_json_location = env_vars.repos_json_location
token = env_vars.gh_token
gh_app_id = env_vars.gh_app_id
gh_app_installation_id = env_vars.gh_app_installation_id
gh_app_private_key_bytes = env_vars.gh_app_private_key_bytes
ghe = env_vars.gh_enterprise_url
gh_app_enterprise_only = env_vars.gh_app_enterprise_only

# Auth to GitHub.com
github_connection = auth.auth_to_github(
env_vars.gh_app_id,
env_vars.gh_app_installation_id,
env_vars.gh_app_private_key_bytes,
env_vars.gh_enterprise_url,
token,
gh_app_id,
gh_app_installation_id,
gh_app_private_key_bytes,
ghe,
gh_app_enterprise_only,
)

if not token and gh_app_id and gh_app_installation_id and gh_app_private_key_bytes:
token = auth.get_github_app_installation_token(
ghe, gh_app_id, gh_app_private_key_bytes, gh_app_installation_id
)

endpoint = ghe if ghe else "github.com"
Comment thread
jmeridth marked this conversation as resolved.
Outdated

os.system("git config --global user.name 'GitHub Actions'")
os.system("git config --global user.email 'no-reply@github.com'")
os.system(f"git config --global user.email 'no-reply@{endpoint}'")

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

Expand All @@ -46,7 +59,7 @@
repo_full_name = repo["full_name"]
repo_name = repo["name"]
os.system(
f"git clone https://{gh_actor}:{token}@github.com/{repo_full_name}"
f"git clone https://{gh_actor}:{token}@{endpoint}/{repo_full_name}"
)
# checkout a branch called contributing-doc
BRANCH_NAME = "contributing-doc"
Expand Down
Loading