Example #1
0
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
Example #2
0
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
Example #3
0
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 {}
Example #5
0
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 {}