def get_github_api_for_repo(keychain, owner, repo, session=None): gh = GitHub( session=session or GitHubSession(default_read_timeout=30, default_connect_timeout=30) ) # Apply retry policy gh.session.mount("http://", adapter) gh.session.mount("https://", adapter) GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") APP_KEY = os.environ.get("GITHUB_APP_KEY", "").encode("utf-8") APP_ID = os.environ.get("GITHUB_APP_ID") if APP_ID and APP_KEY: installation = INSTALLATIONS.get((owner, repo)) if installation is None: gh.login_as_app(APP_KEY, APP_ID, expire_in=120) try: installation = gh.app_installation_for_repository(owner, repo) except github3.exceptions.NotFoundError: raise GithubException( f"Could not access {owner}/{repo} using GitHub app. " "Does the app need to be installed for this repository?" ) INSTALLATIONS[(owner, repo)] = installation gh.login_as_app_installation(APP_KEY, APP_ID, installation.id) elif GITHUB_TOKEN: gh.login(token=GITHUB_TOKEN) else: github_config = keychain.get_service("github") gh.login(github_config.username, github_config.password) return gh
def get_github_api_for_repo(keychain, owner, repo): gh = GitHub() # Apply retry policy gh.session.mount("http://", adapter) gh.session.mount("https://", adapter) APP_KEY = os.environ.get("GITHUB_APP_KEY", "").encode("utf-8") APP_ID = os.environ.get("GITHUB_APP_ID") if APP_ID and APP_KEY: installation = INSTALLATIONS.get((owner, repo)) if installation is None: gh.login_as_app(APP_KEY, APP_ID, expire_in=120) try: installation = gh.app_installation_for_repository(owner, repo) except github3.exceptions.NotFoundError: raise GithubException( "Could not access {}/{} using GitHub app. " "Does the app need to be installed for this repository?". format(owner, repo)) INSTALLATIONS[(owner, repo)] = installation gh.login_as_app_installation(APP_KEY, APP_ID, installation.id) else: github_config = keychain.get_service("github") gh.login(github_config.username, github_config.password) return gh
def gh_as_app(repo_owner, repo_name): app_id = settings.GITHUB_APP_ID app_key = settings.GITHUB_APP_KEY gh = GitHub() gh.login_as_app(app_key, app_id, expire_in=120) installation = gh.app_installation_for_repository(repo_owner, repo_name) gh.login_as_app_installation(app_key, app_id, installation.id) return gh
class NoisePageRepoClient(): def __init__(self, private_key, app_id): """ Connect to github and create a Github client and a client specific to the Github app installation""" self.private_key = private_key self.owner = REPO_OWNER self.repo = REPO_NAME self.git_client = GitHub() self.git_client.login_as_app(private_key_pem=str.encode(private_key), app_id=app_id) self.noisepage_repo_client = self.git_client.app_installation_for_repository( self.owner, self.repo) self.access_token = {"token": None, "exp": 0} def is_valid_installation_id(self, id): """ Check whether an installation ID is the NoisePage installation This will prevent other Github users from using the app """ return id == self.noisepage_repo_client.id def _get_jwt(self): """ This creates a JWT that can be used to retrieve an authentication token for the Github app.""" jwt = JWT() now = int(time.time()) payload = { "iat": now, "exp": now + (60), "iss": self.noisepage_repo_client.app_id } private_key = jwk_from_pem(str.encode(self.private_key)) return { "jwt": jwt.encode(payload, private_key, alg='RS256'), "exp": payload.get('exp') } def _get_installation_access_token(self): """ Get the installation access token for making API calls not supported by github3.py. Only get a new token if the current one has expired. """ if time.time() >= self.access_token.get('exp'): auth_token = self._get_jwt() headers = { 'Authorization': f'Bearer {auth_token.get("jwt")}', 'Accept': 'application/vnd.github.v3+json' } response = requests.post( url=self.noisepage_repo_client.access_tokens_url, headers=headers) response.raise_for_status() self.access_token = { "token": response.json().get('token'), "exp": auth_token.get('exp') } return self.access_token.get('token') def create_check_run(self, create_body): """ Create a check run for the performance cop """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/check-runs" response = requests.post(url=url, json=create_body, headers=headers) response.raise_for_status() return response.json() def update_check_run(self, check_run_id, update_body): """ Update a check run to mark it as complete """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/check-runs/{check_run_id}" response = requests.patch(url=url, json=update_body, headers=headers) response.raise_for_status() return response.json() def get_commit_status(self, commit_sha): """ Get the status of a commit """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/commits/{commit_sha}/status" response = requests.get(url=url, headers=headers) response.raise_for_status() return response.json() def get_commit_check_run_by_name(self, commit_sha, name): """ Get the check runs for a commit """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/commits/{commit_sha}/check-runs" response = requests.get(url=url, headers=headers) response.raise_for_status() check_runs = response.json() check = find_check_run_by_name(check_runs.get('check_runs'), name) if check: return check return {}
class NoisePageRepoClient(): """ Class for communicating with GitHub. Attributes ---------- private_key : str The private key of the GitHub App. owner : str The GitHub username of the repository owner. repo : str The name of the GitHub repo. git_client : GitHub object The client used to communicate with GitHub. noisepage_repo_client : Installation The client used to communicate with GitHub as the GitHub App installation. access_token : dict An access_token for authentication. """ def __init__(self, private_key, app_id): """ Connect to github and create a Github client and a client specific to the Github app installation. Parameters ---------- private_key : str The private key of the Github App. app_id : int The unique id of the Github App. """ self.private_key = private_key self.owner = REPO_OWNER self.repo = REPO_NAME # Client for communicating with GitHub API self.git_client = GitHub() self.git_client.login_as_app(private_key_pem=str.encode(private_key), app_id=app_id) # Information about app installation associated with repo self.noisepage_repo_client = self.git_client.app_installation_for_repository( self.owner, self.repo) # Login as installation allows interactions that require repository permissions self.git_client.login_as_app_installation( private_key_pem=str.encode(private_key), app_id=app_id, installation_id=self.noisepage_repo_client.id) self.access_token = {"token": None, "exp": 0} def is_valid_installation_id(self, id): """ Check whether an installation ID is the NoisePage installation. This will prevent other GitHub repositories from using the app. Parameters ---------- id : int The id of the GitHub App installation. Returns ------- bool True if the id matches an allowed GitHub App installation. False otherwise. """ return id == self.noisepage_repo_client.id def _get_jwt(self): """ This creates a JWT that can be used to retrieve an authentication token for the GitHub app. Returns ------- dict A dict containing the 'jwt' and expiration datetime. """ jwt = JWT() now = int(time.time()) payload = { "iat": now, "exp": now + (60), "iss": self.noisepage_repo_client.app_id } private_key = jwk_from_pem(str.encode(self.private_key)) return { "jwt": jwt.encode(payload, private_key, alg='RS256'), "exp": payload.get('exp') } def _get_installation_access_token(self): """ Get the installation access token for making API calls not supported by github3.py. Only get a new token if the current one has expired. This sets the class's `access_token` attribute to the new token and updates the `exp`. Returns ------- str The access token needed to make authenticated requests to GitHub. """ if time.time() >= self.access_token.get('exp'): auth_token = self._get_jwt() headers = { 'Authorization': f'Bearer {auth_token.get("jwt")}', 'Accept': 'application/vnd.github.v3+json' } response = requests.post( url=self.noisepage_repo_client.access_tokens_url, headers=headers) response.raise_for_status() self.access_token = { "token": response.json().get('token'), "exp": auth_token.get('exp') } return self.access_token.get('token') def create_check_run(self, create_body): """ Create a check run. Parameters ---------- create_body : dict The body of the request that will create a new check run. Returns ------- Response The response of the API request. Raises ------ HTTPError If the API request failed. """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/check-runs" response = requests.post(url=url, json=create_body, headers=headers) response.raise_for_status() return response.json() def update_check_run(self, check_run_id, update_body): """ Update a check run to mark it as complete. This is typically used to mark the check run as complete. Parameters ---------- check_run_id : int The id of the check run to be updated. update_body : dict The body of the request that will create a new check run. Returns ------- Response The response of the API request. Raises ------ HTTPError If the API request failed. """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/check-runs/{check_run_id}" response = requests.patch(url=url, json=update_body, headers=headers) response.raise_for_status() return response.json() def get_commit_status(self, commit_sha): """ Get the status of a commit. This was originally used to check if the CI was complete but this lead to timing issues because the API endpoint isn't strictly consistent. An event could be sent to say the CI is complete but this endpoint will say that it is still pending. Parameters ---------- commit_sha : str The commit hash to check the status of. Returns ------- Response The response of the API request. Raises ------ HTTPError If the API request failed. """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/commits/{commit_sha}/status" response = requests.get(url=url, headers=headers) response.raise_for_status() return response.json() def create_pr_comments_for_commit(self, commit_sha, comment_body): """ Add a comment to all PRs that a commit is associated with. Parameters ---------- commit_sha : str The commit to add comments for. comment_body : str The comment to be added to the PRs. Markdown is accepted. """ for pr in self.find_commit_prs(commit_sha): logger.debug(pr) pr.issue.create_comment(comment_body) def find_commit_prs(self, commit_sha): """ Get all open PRs associated with a commit. Parameters ---------- commit_sha : str The commit to find the PRs for. Returns ------- list of IssueSearchResult The PR search results that are associated with this commit. """ search_query = f'{commit_sha}+type:pr+repo:{self.owner}/{self.repo}+state:open' return self.git_client.search_issues(search_query) def get_commit_check_run_by_name(self, commit_sha, name): """ Get the check runs for a commit. This is typically used to find the check run, in order to discover its id. Parameters ---------- commit_sha : str The commit for which the check run was created. name : str The name of the check. Returns ------- check : dict The check run that was created for the commit that matches the name parameter. If no check matches the name, then this is an empty dict. Raises ------ HTTPError If the API request failed. """ token = self._get_installation_access_token() headers = { 'Authorization': f'Bearer {token}', "Accept": "application/vnd.github.v3+json" } url = f"{GITHUB_BASE_URL}repos/{self.owner}/{self.repo}/commits/{commit_sha}/check-runs" response = requests.get(url=url, headers=headers) response.raise_for_status() check_runs = response.json() check = find_check_run_by_name(check_runs.get('check_runs'), name) if check: return check return {}