Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make bedevere work as a GitHub App #569

Merged
merged 1 commit into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Mariatta marked this conversation as resolved.
Show resolved Hide resolved
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