diff --git a/bedevere/__main__.py b/bedevere/__main__.py index 3a3c0740..a16111d1 100644 --- a/bedevere/__main__.py +++ b/bedevere/__main__.py @@ -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 @@ -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) @@ -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) diff --git a/bedevere/stage.py b/bedevere/stage.py index f3914d9b..bf366ba7 100644 --- a/bedevere/stage.py +++ b/bedevere/stage.py @@ -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) diff --git a/bedevere/util.py b/bedevere/util.py index 7cb6c7a9..97ed6c6a 100644 --- a/bedevere/util.py +++ b/bedevere/util.py @@ -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. @@ -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] diff --git a/runtime.txt b/runtime.txt index a5da7cc4..1e480cee 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.10.5 +python-3.10.12 diff --git a/tests/test___main__.py b/tests/test___main__.py index e9b49172..9084683e 100644 --- a/tests/test___main__.py +++ b/tests/test___main__.py @@ -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() @@ -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 diff --git a/tests/test_stage.py b/tests/test_stage.py index 39cc449d..9d2b4838 100644 --- a/tests/test_stage.py +++ b/tests/test_stage.py @@ -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={}): @@ -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": [ { @@ -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": [ { diff --git a/tests/test_util.py b/tests/test_util.py index 3917dbe1..01dea940 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -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"