Skip to content

Commit 9010a0f

Browse files
authored
Make bedevere works as a GitHub App (#569)
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.
1 parent 249bab5 commit 9010a0f

File tree

7 files changed

+110
-16
lines changed

7 files changed

+110
-16
lines changed

bedevere/__main__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from gidgethub import aiohttp as gh_aiohttp
1111
from gidgethub import routing
1212
from gidgethub import sansio
13+
from gidgethub import apps
1314

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

@@ -35,6 +36,17 @@ async def main(request):
3536
gh = gh_aiohttp.GitHubAPI(session, "python/bedevere",
3637
oauth_token=oauth_token,
3738
cache=cache)
39+
40+
if event.data.get("installation"):
41+
# This path only works on GitHub App
42+
installation_id = event.data["installation"]["id"]
43+
installation_access_token = await apps.get_installation_access_token(
44+
gh,
45+
installation_id=installation_id,
46+
app_id=os.environ.get("GH_APP_ID"),
47+
private_key=os.environ.get("GH_PRIVATE_KEY")
48+
)
49+
gh.oauth_token = installation_access_token["token"]
3850
# Give GitHub some time to reach internal consistency.
3951
await asyncio.sleep(1)
4052
await router.dispatch(event, gh, session=session)
@@ -48,6 +60,12 @@ async def main(request):
4860
return web.Response(status=500)
4961

5062

63+
@router.register("installation", action="created")
64+
async def repo_installation_added(event, gh, *args, **kwargs):
65+
# installation_id = event.data["installation"]["id"]
66+
print(f"App installed by {event.data['installation']['account']['login']}, installation_id: {event.data['installation']['id']}")
67+
68+
5169
if __name__ == "__main__": # pragma: no cover
5270
app = web.Application()
5371
app.router.add_post("/", main)

bedevere/stage.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ async def new_commit_pushed(event, gh, *arg, **kwargs):
146146
if len(commits) > 0:
147147
# get the latest commit hash
148148
commit_hash = commits[-1]["id"]
149-
pr = await util.get_pr_for_commit(gh, commit_hash)
149+
repo_full_name = event.data["repository"]["full_name"]
150+
pr = await util.get_pr_for_commit(gh, commit_hash, repo_full_name)
151+
150152
for label in util.labels(pr):
151153
if label == "awaiting merge":
152154
issue = await util.issue_for_PR(gh, pr)

bedevere/util.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,15 @@ async def is_core_dev(gh, username):
187187
"""Check if the user is a CPython core developer."""
188188
org_teams = "/orgs/python/teams"
189189
team_name = "python core"
190-
async for team in gh.getiter(org_teams):
191-
if team["name"].lower() == team_name: # pragma: no branch
192-
break
193-
else:
194-
raise ValueError(f"{team_name!r} not found at {org_teams!r}")
190+
try:
191+
async for team in gh.getiter(org_teams):
192+
if team["name"].lower() == team_name: # pragma: no branch
193+
break
194+
else:
195+
raise ValueError(f"{team_name!r} not found at {org_teams!r}")
196+
except gidgethub.BadRequest as exc:
197+
# returns 403 error if the resource is not accessible by integration
198+
return False
195199
# The 'teams' object only provides a URL to a deprecated endpoint,
196200
# so manually construct the URL to the non-deprecated team membership
197201
# endpoint.
@@ -232,10 +236,12 @@ def no_labels(event_data):
232236
return False
233237

234238

235-
async def get_pr_for_commit(gh, sha):
239+
async def get_pr_for_commit(gh, sha, repo_full_name=None):
236240
"""Find the PR containing the specific commit hash."""
241+
if not repo_full_name:
242+
repo_full_name = "python/cpython"
237243
prs_for_commit = await gh.getitem(
238-
f"/search/issues?q=type:pr+repo:python/cpython+sha:{sha}"
244+
f"/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}"
239245
)
240246
if prs_for_commit["total_count"] > 0: # there should only be one
241247
return prs_for_commit["items"][0]

runtime.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
python-3.10.5
1+
python-3.10.12

tests/test___main__.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
from aiohttp import web
2-
import pytest
2+
3+
from unittest import mock
4+
35

46
from bedevere import __main__ as main
57

8+
from gidgethub import sansio
9+
10+
11+
app_installation_payload = {
12+
"installation":
13+
{
14+
"id": 123,
15+
"account": {"login": "mariatta"},
16+
}
17+
}
18+
619

720
async def test_ping(aiohttp_client):
821
app = web.Application()
@@ -36,3 +49,40 @@ async def test_failure(aiohttp_client):
3649
# Missing key headers.
3750
response = await client.post("/", headers={})
3851
assert response.status == 500
52+
53+
54+
@mock.patch("gidgethub.apps.get_installation_access_token")
55+
async def test_success_with_installation(get_access_token_mock, aiohttp_client):
56+
57+
get_access_token_mock.return_value = {'token': 'ghs_blablabla', 'expires_at': '2023-06-14T19:02:50Z'}
58+
app = web.Application()
59+
app.router.add_post("/", main.main)
60+
client = await aiohttp_client(app)
61+
headers = {"x-github-event": "project",
62+
"x-github-delivery": "1234"}
63+
# Sending a payload that shouldn't trigger any networking, but no errors
64+
# either.
65+
data = {"action": "created"}
66+
data.update(app_installation_payload)
67+
response = await client.post("/", headers=headers, json=data)
68+
assert response.status == 200
69+
70+
71+
class FakeGH:
72+
73+
def __init__(self):
74+
pass
75+
76+
77+
async def test_repo_installation_added(capfd):
78+
event_data = {
79+
"action": "created",
80+
}
81+
event_data.update(app_installation_payload)
82+
83+
event = sansio.Event(event_data, event='installation',
84+
delivery_id='1')
85+
gh = FakeGH()
86+
await main.router.dispatch(event, gh)
87+
out, err = capfd.readouterr()
88+
assert f"App installed by {event.data['installation']['account']['login']}, installation_id: {event.data['installation']['id']}" in out

tests/test_stage.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ async def getiter(self, url, url_vars={}):
2424
self.getiter_url = sansio.format_url(url, url_vars)
2525
to_iterate = self._getiter_return[self.getiter_url]
2626
for item in to_iterate:
27+
if isinstance(item, Exception):
28+
raise item
2729
yield item
2830

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

1101+
10991102
@pytest.mark.parametrize("issue_url_key", ["url", "issue_url"])
1100-
async def test_new_commit_pushed_to_approved_pr(issue_url_key):
1103+
@pytest.mark.parametrize("repo_full_name", ["mariatta/cpython", "python/cpython"])
1104+
async def test_new_commit_pushed_to_approved_pr(issue_url_key, repo_full_name):
11011105
# There is new commit on approved PR
11021106
username = "brettcannon"
11031107
sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9"
1104-
data = {"commits": [{"id": sha}]}
1108+
data = {"commits": [{"id": sha}],
1109+
"repository": {"full_name": repo_full_name},
1110+
}
11051111
event = sansio.Event(data, event="push", delivery_id="12345")
11061112
teams = [{"name": "python core", "id": 6}]
11071113
items = {
11081114
f"https://api.github.com/teams/6/memberships/{username}": "OK",
1109-
f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": {
1115+
f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": {
11101116
"total_count": 1,
11111117
"items": [
11121118
{
@@ -1169,14 +1175,18 @@ async def test_new_commit_pushed_to_approved_pr(issue_url_key):
11691175
)
11701176
}
11711177

1178+
11721179
@pytest.mark.parametrize("issue_url_key", ["url", "issue_url"])
1173-
async def test_new_commit_pushed_to_not_approved_pr(issue_url_key):
1180+
@pytest.mark.parametrize("repo_full_name", ["mariatta/cpython", "python/cpython"])
1181+
async def test_new_commit_pushed_to_not_approved_pr(issue_url_key, repo_full_name):
11741182
# There is new commit on approved PR
11751183
sha = "f2393593c99dd2d3ab8bfab6fcc5ddee540518a9"
1176-
data = {"commits": [{"id": sha}]}
1184+
data = {"commits": [{"id": sha}],
1185+
"repository": {"full_name": repo_full_name},
1186+
}
11771187
event = sansio.Event(data, event="push", delivery_id="12345")
11781188
items = {
1179-
f"https://api.github.com/search/issues?q=type:pr+repo:python/cpython+sha:{sha}": {
1189+
f"https://api.github.com/search/issues?q=type:pr+repo:{repo_full_name}+sha:{sha}": {
11801190
"total_count": 1,
11811191
"items": [
11821192
{

tests/test_util.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ async def test_is_core_dev():
9898
await util.is_core_dev(gh, "andrea")
9999

100100

101+
async def test_is_core_dev_resource_not_accessible():
102+
103+
gh = FakeGH(getiter={"https://api.github.com/orgs/python/teams": [gidgethub.BadRequest(
104+
status_code=http.HTTPStatus(403)
105+
)]})
106+
assert await util.is_core_dev(gh, "mariatta") is False
107+
108+
101109
def test_title_normalization():
102110
title = "abcd"
103111
body = "1234"

0 commit comments

Comments
 (0)