diff --git a/bot/code_review_bot/github.py b/bot/code_review_bot/github.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/bot/code_review_bot/report/__init__.py b/bot/code_review_bot/report/__init__.py index f1d5294ee..ed79bb1ec 100644 --- a/bot/code_review_bot/report/__init__.py +++ b/bot/code_review_bot/report/__init__.py @@ -4,6 +4,7 @@ import structlog +from code_review_bot.report.github import GithubReporter from code_review_bot.report.lando import LandoReporter from code_review_bot.report.mail import MailReporter from code_review_bot.report.mail_builderrors import BuildErrorsReporter @@ -22,6 +23,7 @@ def get_reporters(configuration): "mail": MailReporter, "build_error": BuildErrorsReporter, "phabricator": PhabricatorReporter, + "github": GithubReporter, } out = {} diff --git a/bot/code_review_bot/report/github.py b/bot/code_review_bot/report/github.py new file mode 100644 index 000000000..1e55a605d --- /dev/null +++ b/bot/code_review_bot/report/github.py @@ -0,0 +1,45 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from code_review_bot.report.base import Reporter +from code_review_bot.sources.github import GithubClient + + +class GithubReporter(Reporter): + # Auth to Github using a configuration (from Taskcluster secret) + + def __init__(self, configuration={}, *args, **kwargs): + if kwargs.get("api") is not None: + api_url = kwargs["api"] + else: + api_url = "https://api.github.com/" + + # Setup github App secret from the configuration + self.github_client = GithubClient( + api_url=api_url, + client_id=configuration.get("app_client_id"), + pem_file_path=configuration.get("app_pem_file"), + ) + + self.analyzers_skipped = configuration.get("analyzers_skipped", []) + assert isinstance( + self.analyzers_skipped, list + ), "analyzers_skipped must be a list" + + def publish(self, issues, revision, task_failures, notices, reviewers): + """ + Publish issues on a Github pull request. + """ + raise NotImplementedError + + @property + def github_jwt_token(self): + # Use the GitHub App's private key to create a JWT (JSON Web Token). + # Exchange the JWT for an installation access token via the GitHub API. + raise NotImplementedError + + def comment(self, *, owner, repo, issue_number, message): + self.github_client.make_request( + "post", f"repos/{owner}/{repo}/issues/{issue_number}/comments", json=message + ) diff --git a/bot/code_review_bot/sources/github.py b/bot/code_review_bot/sources/github.py new file mode 100644 index 000000000..90f400dc2 --- /dev/null +++ b/bot/code_review_bot/sources/github.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import time +from urllib.parse import urljoin + +import jwt +import requests + + +class GithubClient: + def __init__(self, api_url: str, client_id: str, pem_file_path: str): + self.api_url = api_url + self.client_id = client_id + self._jwt = self.generate_jwt(pem_file_path) + + def generate_jwt(self, pem_file_path): + with open(pem_file_path) as f: + signing_key = f.read() + client_id = self.client_id + + # https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app#example-using-python-to-generate-a-jwt + return jwt.encode( + { + # Issued at time + "iat": int(time.time()), + # JWT expiration time (10 minutes maximum) + "exp": int(time.time()) + 600, + # GitHub App's client ID + "iss": client_id, + }, + signing_key, + algorithm="RS256", + ) + + def make_request(self, method, path, *, headers={}, **kwargs): + headers["Authorization"] = f"Bearer {self._jwt}" + headers["Accept"] = "application/vnd.github+json" + + url = urljoin(self.api_url, path) + resp = requests[method]( + url, + headers=headers, + **kwargs, + ) + resp.raise_for_status() + return resp.json() diff --git a/bot/github_comment.py b/bot/github_comment.py new file mode 100755 index 000000000..1ed0ea147 --- /dev/null +++ b/bot/github_comment.py @@ -0,0 +1,46 @@ +#! python +import sys + +from code_review_bot.report.github import GithubReporter + + +def get_configuration(): + """ + Example of code review reporter configuration to publish on github API. + + bot: + REPORTERS: + - reporter: github + app_client_id: xxxxxxxxxxxxxxxxxxxx + app_pem_file: path/to.pem + """ + # Handle the GitHub app secret as the single script argument + _, *args = sys.argv + assert ( + len(args) == 2 + ), "Please run this script with a App client ID and the path to Github private key (`.pem` file)." + app_client_id, app_secret, *_ = args + assert len(app_client_id) == 20, "Github App client ID should be 20 characters." + return { + "reporter": "github", + "app_pem_file": app_secret, + } + + +def main(): + """ + Initialize a Github reporter and publish a simple comment on a defined issue + """ + print("Initializing Github reporter") + reporter = GithubReporter(get_configuration()) + print("Publishing a comment to https://github.com/vrigal/test-dev-mozilla/pull/1") + reporter.comment( + owner="vrigal", + repo="test-dev-mozilla", + issue_number=1, + message="test message", + ) + + +if __name__ == "__main__": + main() diff --git a/bot/requirements.txt b/bot/requirements.txt index 9d51590d1..7ef7ba525 100644 --- a/bot/requirements.txt +++ b/bot/requirements.txt @@ -1,5 +1,6 @@ aiohttp<4 influxdb==5.3.2 +jwt==1.4.0 libmozdata==0.2.12 python-hglib==2.6.2 pyyaml==6.0.3