Skip to content

Commit

Permalink
Make bedevere works as a GitHub App (#569)
Browse files Browse the repository at this point in the history
Instead of using Oauth Token, use the GitHub App Installation Access Token.
If the "installation" dict is passed in the webhook event, use it.

Made some changes to accommodate testing in personal CPython fork.
  • Loading branch information
Mariatta committed Sep 11, 2023
1 parent 249bab5 commit 9010a0f
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 16 deletions.
18 changes: 18 additions & 0 deletions bedevere/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from gidgethub import aiohttp as gh_aiohttp
from gidgethub import routing
from gidgethub import sansio
from gidgethub import apps

from . import backport, gh_issue, close_pr, filepaths, news, stage

Expand All @@ -35,6 +36,17 @@ async def main(request):
gh = gh_aiohttp.GitHubAPI(session, "python/bedevere",
oauth_token=oauth_token,
cache=cache)

if event.data.get("installation"):
# This path only works on GitHub App
installation_id = event.data["installation"]["id"]
installation_access_token = await apps.get_installation_access_token(
gh,
installation_id=installation_id,
app_id=os.environ.get("GH_APP_ID"),
private_key=os.environ.get("GH_PRIVATE_KEY")
)
gh.oauth_token = installation_access_token["token"]
# Give GitHub some time to reach internal consistency.
await asyncio.sleep(1)
await router.dispatch(event, gh, session=session)
Expand All @@ -48,6 +60,12 @@ async def main(request):
return web.Response(status=500)


@router.register("installation", action="created")
async def repo_installation_added(event, gh, *args, **kwargs):
# installation_id = event.data["installation"]["id"]
print(f"App installed by {event.data['installation']['account']['login']}, installation_id: {event.data['installation']['id']}")


if __name__ == "__main__": # pragma: no cover
app = web.Application()
app.router.add_post("/", main)
Expand Down
4 changes: 3 additions & 1 deletion bedevere/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ async def new_commit_pushed(event, gh, *arg, **kwargs):
if len(commits) > 0:
# get the latest commit hash
commit_hash = commits[-1]["id"]
pr = await util.get_pr_for_commit(gh, commit_hash)
repo_full_name = event.data["repository"]["full_name"]
pr = await util.get_pr_for_commit(gh, commit_hash, repo_full_name)

for label in util.labels(pr):
if label == "awaiting merge":
issue = await util.issue_for_PR(gh, pr)
Expand Down
20 changes: 13 additions & 7 deletions bedevere/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,15 @@ async def is_core_dev(gh, username):
"""Check if the user is a CPython core developer."""
org_teams = "/orgs/python/teams"
team_name = "python core"
async for team in gh.getiter(org_teams):
if team["name"].lower() == team_name: # pragma: no branch
break
else:
raise ValueError(f"{team_name!r} not found at {org_teams!r}")
try:
async for team in gh.getiter(org_teams):
if team["name"].lower() == team_name: # pragma: no branch
break
else:
raise ValueError(f"{team_name!r} not found at {org_teams!r}")
except gidgethub.BadRequest as exc:
# returns 403 error if the resource is not accessible by integration
return False
# The 'teams' object only provides a URL to a deprecated endpoint,
# so manually construct the URL to the non-deprecated team membership
# endpoint.
Expand Down Expand Up @@ -232,10 +236,12 @@ def no_labels(event_data):
return False


async def get_pr_for_commit(gh, sha):
async def get_pr_for_commit(gh, sha, repo_full_name=None):
"""Find the PR containing the specific commit hash."""
if not repo_full_name:
repo_full_name = "python/cpython"
prs_for_commit = await gh.getitem(
f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}"
f"/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}"
)
if prs_for_commit["total_count"] > 0: # there should only be one
return prs_for_commit["items"][0]
Expand Down
2 changes: 1 addition & 1 deletion runtime.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-3.10.5
python-3.10.12
52 changes: 51 additions & 1 deletion tests/test___main__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
from aiohttp import web
import pytest

from unittest import mock


from bedevere import __main__ as main

from gidgethub import sansio


app_installation_payload = {
"installation":
{
"id": 123,
"account": {"login": "mariatta"},
}
}


async def test_ping(aiohttp_client):
app = web.Application()
Expand Down Expand Up @@ -36,3 +49,40 @@ async def test_failure(aiohttp_client):
# Missing key headers.
response = await client.post("/", headers={})
assert response.status == 500


@mock.patch("gidgethub.apps.get_installation_access_token")
async def test_success_with_installation(get_access_token_mock, aiohttp_client):

get_access_token_mock.return_value = {'token': 'ghs_blablabla', 'expires_at': '2023-06-14T19:02:50Z'}
app = web.Application()
app.router.add_post("/", main.main)
client = await aiohttp_client(app)
headers = {"x-github-event": "project",
"x-github-delivery": "1234"}
# Sending a payload that shouldn't trigger any networking, but no errors
# either.
data = {"action": "created"}
data.update(app_installation_payload)
response = await client.post("/", headers=headers, json=data)
assert response.status == 200


class FakeGH:

def __init__(self):
pass


async def test_repo_installation_added(capfd):
event_data = {
"action": "created",
}
event_data.update(app_installation_payload)

event = sansio.Event(event_data, event='installation',
delivery_id='1')
gh = FakeGH()
await main.router.dispatch(event, gh)
out, err = capfd.readouterr()
assert f"App installed by {event.data['installation']['account']['login']}, installation_id: {event.data['installation']['id']}" in out
22 changes: 16 additions & 6 deletions tests/test_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ async def getiter(self, url, url_vars={}):
self.getiter_url = sansio.format_url(url, url_vars)
to_iterate = self._getiter_return[self.getiter_url]
for item in to_iterate:
if isinstance(item, Exception):
raise item
yield item

async def getitem(self, url, url_vars={}):
Expand Down Expand Up @@ -1096,17 +1098,21 @@ async def test_awaiting_label_not_removed_when_pr_not_merged(label):
await awaiting.router.dispatch(event, gh)
assert gh.delete_url is None


@pytest.mark.parametrize("issue_url_key", ["url", "issue_url"])
async def test_new_commit_pushed_to_approved_pr(issue_url_key):
@pytest.mark.parametrize("repo_full_name", ["mariatta/cpython", "python/cpython"])
async def test_new_commit_pushed_to_approved_pr(issue_url_key, repo_full_name):
# There is new commit on approved PR
username = "brettcannon"
sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9"
data = {"commits": [{"id": sha}]}
data = {"commits": [{"id": sha}],
"repository": {"full_name": repo_full_name},
}
event = sansio.Event(data, event="push", delivery_id="12345")
teams = [{"name": "python core", "id": 6}]
items = {
f"https://api.github.com/teams/6/memberships/{username}": "OK",
f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": {
f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": {
"total_count": 1,
"items": [
{
Expand Down Expand Up @@ -1169,14 +1175,18 @@ async def test_new_commit_pushed_to_approved_pr(issue_url_key):
)
}


@pytest.mark.parametrize("issue_url_key", ["url", "issue_url"])
async def test_new_commit_pushed_to_not_approved_pr(issue_url_key):
@pytest.mark.parametrize("repo_full_name", ["mariatta/cpython", "python/cpython"])
async def test_new_commit_pushed_to_not_approved_pr(issue_url_key, repo_full_name):
# There is new commit on approved PR
sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9"
data = {"commits": [{"id": sha}]}
data = {"commits": [{"id": sha}],
"repository": {"full_name": repo_full_name},
}
event = sansio.Event(data, event="push", delivery_id="12345")
items = {
f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": {
f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": {
"total_count": 1,
"items": [
{
Expand Down
8 changes: 8 additions & 0 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ async def test_is_core_dev():
await util.is_core_dev(gh, "andrea")


async def test_is_core_dev_resource_not_accessible():

gh = FakeGH(getiter={"https://api.github.com/orgs/python/teams": [gidgethub.BadRequest(
status_code=http.HTTPStatus(403)
)]})
assert await util.is_core_dev(gh, "mariatta") is False


def test_title_normalization():
title = "abcd"
body = "1234"
Expand Down

0 comments on commit 9010a0f

Please sign in to comment.