async def get_token_for_install(*, session: http.AsyncClient, installation_id: str) -> str: """ https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#authenticating-as-an-installation """ token = installation_cache.get(installation_id) if token is not None and not token.expired: return token.token app_token = generate_jwt(private_key=conf.PRIVATE_KEY, app_identifier=conf.GITHUB_APP_ID) throttler = get_thottler_for_installation( # this isn't a real installation ID, but it provides rate limiting # for our GithubApp instead of the installations we typically act as installation_id=APPLICATION_ID) async with throttler: res = await session.post( conf.v3_url(f"/app/installations/{installation_id}/access_tokens"), headers=dict( Accept="application/vnd.github.machine-man-preview+json", Authorization=f"Bearer {app_token}", ), ) if res.status_code > 300: raise Exception(f"Failed to get token, github response: {res.text}") token_response = TokenResponse(**res.json()) installation_cache[installation_id] = token_response return token_response.token
async def get_open_pull_requests( self, base: Optional[str] = None, head: Optional[str] = None ) -> Optional[List[GetOpenPullRequestsResponseSchema]]: """ https://developer.github.com/v3/pulls/#list-pull-requests """ log = self.log.bind(base=base, head=head) headers = await get_headers(installation_id=self.installation_id) params = dict(state="open", sort="updated") if base is not None: params["base"] = base if head is not None: params["head"] = head async with self.throttler: res = await self.session.get( conf.v3_url(f"/repos/{self.owner}/{self.repo}/pulls"), params=params, headers=headers, ) try: res.raise_for_status() except http.HTTPError: log.warning("problem finding prs", res=res, exc_info=True) return None return [GetOpenPullRequestsResponseSchema.parse_obj(pr) for pr in res.json()]
def list_installs() -> None: app_token = generate_jwt( private_key=conf.PRIVATE_KEY, app_identifier=conf.GITHUB_APP_ID ) results: List[Dict[str, Any]] = [] headers = dict( Accept="application/vnd.github.machine-man-preview+json", Authorization=f"Bearer {app_token}", ) url = conf.v3_url("/app/installations") while True: res = requests.get(url, headers=headers) res.raise_for_status() results += res.json() try: url = res.links["next"]["url"] except (KeyError, IndexError): break for r in results: try: install_url = r["account"]["html_url"] install_id = r["id"] click.echo(f"install:{install_id} for {install_url}") except (KeyError, IndexError): pass
async def update_ref(self, *, ref: str, sha: str) -> http.Response: """ https://docs.github.com/en/rest/reference/git#update-a-reference """ headers = await get_headers(installation_id=self.installation_id) url = conf.v3_url(f"/repos/{self.owner}/{self.repo}/git/refs/heads/{ref}") async with self.throttler: return await self.session.patch(url, headers=headers, json=dict(sha=sha))
async def update_branch(self, *, pull_number: int) -> http.Response: headers = await get_headers(installation_id=self.installation_id) async with self.throttler: return await self.session.put( conf.v3_url( f"/repos/{self.owner}/{self.repo}/pulls/{pull_number}/update-branch" ), headers=headers, )
async def create_comment(self, body: str, pull_number: int) -> http.Response: headers = await get_headers(installation_id=self.installation_id) async with self.throttler: return await self.session.post( conf.v3_url( f"/repos/{self.owner}/{self.repo}/issues/{pull_number}/comments" ), json=dict(body=body), headers=headers, )
async def delete_label(self, label: str, pull_number: int) -> http.Response: headers = await get_headers(installation_id=self.installation_id) escaped_label = urllib.parse.quote(label) async with self.throttler: return await self.session.delete( conf.v3_url( f"/repos/{self.owner}/{self.repo}/issues/{pull_number}/labels/{escaped_label}" ), headers=headers, )
async def add_label(self, label: str, pull_number: int) -> http.Response: headers = await get_headers(installation_id=self.installation_id) async with self.throttler: return await self.session.post( conf.v3_url( f"/repos/{self.owner}/{self.repo}/issues/{pull_number}/labels" ), json=dict(labels=[label]), headers=headers, )
async def delete_branch(self, branch: str) -> http.Response: """ delete a branch by name """ headers = await get_headers(installation_id=self.installation_id) ref = urllib.parse.quote(f"heads/{branch}") async with self.throttler: return await self.session.delete( conf.v3_url(f"/repos/{self.owner}/{self.repo}/git/refs/{ref}"), headers=headers, )
async def get_login_for_install(*, installation_id: str) -> str: app_token = generate_jwt(private_key=conf.PRIVATE_KEY, app_identifier=conf.GITHUB_APP_ID) res = await http.get( conf.v3_url(f"/app/installations/{installation_id}"), headers=dict( Accept="application/vnd.github.machine-man-preview+json", Authorization=f"Bearer {app_token}", ), ) res.raise_for_status() return cast(str, res.json()["account"]["login"])
async def approve_pull_request(self, *, pull_number: int) -> http.Response: """ https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review """ headers = await get_headers(installation_id=self.installation_id) body = dict(event="APPROVE") async with self.throttler: return await self.session.post( conf.v3_url( f"/repos/{self.owner}/{self.repo}/pulls/{pull_number}/reviews" ), headers=headers, json=body, )
async def get_permissions_for_username(self, username: str) -> Permission: headers = await get_headers(installation_id=self.installation_id) async with self.throttler: res = await self.session.get( conf.v3_url( f"/repos/{self.owner}/{self.repo}/collaborators/{username}/permission" ), headers=headers, ) try: res.raise_for_status() return Permission(res.json()["permission"]) except (http.HTTPError, IndexError, TypeError, ValueError): logger.exception("couldn't fetch permissions for username %r", username) return Permission.NONE
async def create_notification( self, head_sha: str, message: str, summary: Optional[str] = None ) -> http.Response: headers = await get_headers(installation_id=self.installation_id) url = conf.v3_url(f"/repos/{self.owner}/{self.repo}/check-runs") body = dict( name=CHECK_RUN_NAME, head_sha=head_sha, status="completed", completed_at=datetime.now(timezone.utc).isoformat(), conclusion="neutral", output=dict(title=message, summary=summary or ""), ) async with self.throttler: return await self.session.post(url, headers=headers, json=body)
async def get_open_pull_requests( self, base: Optional[str] = None, head: Optional[str] = None ) -> Optional[List[GetOpenPullRequestsResponseSchema]]: """ https://developer.github.com/v3/pulls/#list-pull-requests """ log = self.log.bind(base=base, head=head) headers = await get_headers(session=self.session, installation_id=self.installation_id) params = dict(state="open", sort="updated", per_page="100") if base is not None: params["base"] = base if head is not None: params["head"] = head open_prs = [] page = None current_page = 0 while page != []: current_page += 1 if current_page > 20: log.info("hit pagination limit") break params["page"] = str(current_page) async with self.throttler: res = await self.session.get( conf.v3_url(f"/repos/{self.owner}/{self.repo}/pulls"), params=params, headers=headers, ) try: res.raise_for_status() except http.HTTPError: log.warning("problem finding prs", res=res, exc_info=True) return None page = res.json() open_prs += [ GetOpenPullRequestsResponseSchema.parse_obj(pr) for pr in page ] return open_prs
async def merge_pull_request( self, number: int, merge_method: str, commit_title: Optional[str], commit_message: Optional[str], ) -> http.Response: body = dict(merge_method=merge_method) # we must not pass the keys for commit_title or commit_message when they # are null because GitHub will error saying the title/message cannot be # null. When the keys are not passed, GitHub creates a title and # message. if commit_title is not None: body["commit_title"] = commit_title if commit_message is not None: body["commit_message"] = commit_message headers = await get_headers(installation_id=self.installation_id) url = conf.v3_url(f"/repos/{self.owner}/{self.repo}/pulls/{number}/merge") async with self.throttler: return await self.session.put(url, headers=headers, json=body)
async def get_pull_request(self, number: int) -> http.Response: headers = await get_headers(session=self.session, installation_id=self.installation_id) url = conf.v3_url(f"/repos/{self.owner}/{self.repo}/pulls/{number}") async with self.throttler: return await self.session.get(url, headers=headers)