Skip to content

Commit 33b9964

Browse files
authored
Delete branches as an async task with the correct auth (#726)
1 parent 650d2a2 commit 33b9964

File tree

3 files changed

+124
-125
lines changed

3 files changed

+124
-125
lines changed

miss_islington/delete_branch.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
import asyncio
2-
3-
import gidgethub
41
import gidgethub.routing
2+
from kombu import exceptions as kombu_ex
3+
from redis import exceptions as redis_ex
54
import stamina
65

6+
from . import tasks
7+
78
router = gidgethub.routing.Router()
89

910

1011
@router.register("pull_request", action="closed")
11-
@stamina.retry(on=gidgethub.GitHubException, timeout=120)
1212
async def delete_branch(event, gh, *args, **kwargs):
1313
"""
1414
Delete the branch once miss-islington's PR is closed.
1515
"""
1616
if event.data["pull_request"]["user"]["login"] == "miss-islington":
1717
branch_name = event.data["pull_request"]["head"]["ref"]
18-
url = f"/repos/miss-islington/cpython/git/refs/heads/{branch_name}"
19-
if event.data["pull_request"]["merged"]:
20-
await gh.delete(url)
21-
else:
22-
# this is delayed to ensure that the bot doesn't remove the branch
23-
# if PR was closed and reopened to rerun checks (or similar)
24-
await asyncio.sleep(60)
25-
updated_data = await gh.getitem(event.data["pull_request"]["url"])
26-
if updated_data["state"] == "closed":
27-
await gh.delete(url)
18+
merged = event.data["pull_request"]["merged"]
19+
pr_url = event.data["pull_request"]["url"]
20+
installation_id = event.data["installation"]["id"]
21+
_queue_delete_task(branch_name, pr_url, merged, installation_id)
22+
23+
24+
@stamina.retry(on=(redis_ex.ConnectionError, kombu_ex.OperationalError), timeout=30)
25+
def _queue_delete_task(branch_name, pr_url, merged, installation_id):
26+
tasks.delete_branch_task.delay(
27+
branch_name,
28+
pr_url,
29+
merged,
30+
installation_id=installation_id
31+
)

miss_islington/tasks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,58 @@ async def backport_task_asyncio(
178178
cp.abort_cherry_pick()
179179

180180

181+
@app.task()
182+
def delete_branch_task(branch_name, pr_url, merged, *, installation_id):
183+
"""Delete a branch from the miss-islington/cpython fork."""
184+
loop = asyncio.get_event_loop()
185+
loop.run_until_complete(
186+
_delete_branch_task_asyncio(
187+
branch_name, pr_url, merged, installation_id=installation_id
188+
)
189+
)
190+
191+
192+
async def _delete_branch_task_asyncio(branch_name, pr_url, merged, *, installation_id):
193+
"""Delete a branch, with delayed deletion for non-merged PRs."""
194+
if not util.is_cpython_repo():
195+
if "cpython" in os.listdir("."):
196+
os.chdir("./cpython")
197+
else:
198+
print(f"Cannot delete branch: cpython repo not found. "
199+
f"pwd: {os.getcwd()}, listdir: {os.listdir('.')}")
200+
return
201+
202+
if merged:
203+
_git_delete_branch(branch_name)
204+
else:
205+
await asyncio.sleep(60)
206+
async with aiohttp.ClientSession() as session:
207+
gh = gh_aiohttp.GitHubAPI(session, "python/cpython", cache=cache)
208+
installation_access_token = await apps.get_installation_access_token(
209+
gh,
210+
installation_id=installation_id,
211+
app_id=os.environ.get("GH_APP_ID"),
212+
private_key=os.environ.get("GH_PRIVATE_KEY")
213+
)
214+
gh.oauth_token = installation_access_token["token"]
215+
updated_data = await gh.getitem(pr_url)
216+
if updated_data["state"] == "closed":
217+
_git_delete_branch(branch_name)
218+
219+
220+
def _git_delete_branch(branch_name):
221+
"""Delete a branch from the origin remote using git."""
222+
try:
223+
subprocess.check_output(
224+
["git", "push", "origin", "--delete", branch_name],
225+
stderr=subprocess.STDOUT
226+
)
227+
print(f"Deleted branch {branch_name}")
228+
except subprocess.CalledProcessError as e:
229+
print(f"Failed to delete branch {branch_name}: {e.output.decode()}")
230+
raise
231+
232+
181233
class InitRepoStep(bootsteps.StartStopStep):
182234
def start(self, c):
183235
print("Initialize the repository.")

tests/test_delete_branch.py

Lines changed: 54 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,102 @@
1-
import asyncio
1+
import os
2+
from unittest import mock
23

34
from gidgethub import sansio
5+
import pytest
46

5-
from miss_islington import delete_branch
7+
os.environ.setdefault("HEROKU_REDIS_MAROON_URL", "someurl")
68

7-
8-
class FakeGH:
9-
def __init__(self, *, getitem=None):
10-
self._getitem_return = getitem
11-
self.getitem_url = None
12-
self.post_data = None
13-
14-
async def getitem(self, url):
15-
self.getitem_url = url
16-
to_return = self._getitem_return[self.getitem_url]
17-
return to_return
18-
19-
async def delete(self, url):
20-
self.delete_url = url
9+
from miss_islington import delete_branch, tasks
2110

2211

23-
async def noop_sleep(delay, result=None):
12+
class FakeGH:
2413
pass
2514

2615

27-
async def test_branch_deleted_when_pr_merged():
16+
async def test_branch_deletion_queued_when_pr_merged():
2817
data = {
2918
"action": "closed",
3019
"pull_request": {
3120
"number": 5722,
3221
"user": {"login": "miss-islington"},
3322
"merged": True,
34-
"merged_by": {"login": "miss-islington"},
35-
"head": {"ref": "backport-17ab8f0-3.7"},
36-
},
37-
}
38-
event = sansio.Event(data, event="pull_request", delivery_id="1")
39-
40-
gh = FakeGH()
41-
await delete_branch.router.dispatch(event, gh)
42-
assert gh.post_data is None # does not leave a comment
43-
assert (
44-
gh.delete_url
45-
== f"/repos/miss-islington/cpython/git/refs/heads/{data['pull_request']['head']['ref']}"
46-
)
47-
48-
49-
async def test_branch_deleted_and_thank_committer():
50-
data = {
51-
"action": "closed",
52-
"pull_request": {
53-
"number": 5722,
54-
"user": {"login": "miss-islington"},
55-
"merged": True,
56-
"merged_by": {"login": "Mariatta"},
57-
"head": {"ref": "backport-17ab8f0-3.7"},
58-
},
59-
}
60-
event = sansio.Event(data, event="pull_request", delivery_id="1")
61-
62-
gh = FakeGH()
63-
await delete_branch.router.dispatch(event, gh)
64-
assert gh.post_data is None # does not leave a comment
65-
assert (
66-
gh.delete_url
67-
== f"/repos/miss-islington/cpython/git/refs/heads/{data['pull_request']['head']['ref']}"
68-
)
69-
70-
71-
async def test_branch_deleted_and_thanks():
72-
data = {
73-
"action": "closed",
74-
"pull_request": {
75-
"number": 5722,
76-
"user": {"login": "miss-islington"},
77-
"merged": True,
78-
"merged_by": {"login": "miss-islington"},
79-
"head": {"ref": "backport-17ab8f0-3.7"},
80-
},
81-
}
82-
event = sansio.Event(data, event="pull_request", delivery_id="1")
83-
84-
gh = FakeGH()
85-
await delete_branch.router.dispatch(event, gh)
86-
assert gh.post_data is None # does not leave a comment
87-
assert (
88-
gh.delete_url
89-
== f"/repos/miss-islington/cpython/git/refs/heads/{data['pull_request']['head']['ref']}"
90-
)
91-
92-
93-
async def test_branch_deleted_when_pr_closed(monkeypatch):
94-
data = {
95-
"action": "closed",
96-
"pull_request": {
97-
"number": 5722,
98-
"user": {"login": "miss-islington"},
99-
"merged": False,
100-
"merged_by": {"login": None},
10123
"head": {"ref": "backport-17ab8f0-3.7"},
10224
"url": "https://api.github.com/repos/python/cpython/pulls/5722",
10325
},
26+
"installation": {"id": 123},
10427
}
10528
event = sansio.Event(data, event="pull_request", delivery_id="1")
106-
getitem = {
107-
"https://api.github.com/repos/python/cpython/pulls/5722": {"state": "closed"},
108-
}
10929

110-
monkeypatch.setattr(asyncio, "sleep", noop_sleep)
111-
gh = FakeGH(getitem=getitem)
112-
await delete_branch.router.dispatch(event, gh)
113-
assert gh.post_data is None # does not leave a comment
114-
assert (
115-
gh.delete_url
116-
== f"/repos/miss-islington/cpython/git/refs/heads/{data['pull_request']['head']['ref']}"
117-
)
30+
gh = FakeGH()
31+
with mock.patch.object(tasks.delete_branch_task, "delay") as mock_delay:
32+
await delete_branch.router.dispatch(event, gh)
33+
mock_delay.assert_called_once_with(
34+
"backport-17ab8f0-3.7",
35+
"https://api.github.com/repos/python/cpython/pulls/5722",
36+
True,
37+
installation_id=123
38+
)
11839

11940

120-
async def test_branch_not_deleted_when_pr_closed_and_reopened(monkeypatch):
41+
async def test_branch_deletion_queued_when_pr_closed_not_merged():
12142
data = {
12243
"action": "closed",
12344
"pull_request": {
12445
"number": 5722,
12546
"user": {"login": "miss-islington"},
12647
"merged": False,
127-
"merged_by": {"login": None},
12848
"head": {"ref": "backport-17ab8f0-3.7"},
12949
"url": "https://api.github.com/repos/python/cpython/pulls/5722",
13050
},
51+
"installation": {"id": 456},
13152
}
13253
event = sansio.Event(data, event="pull_request", delivery_id="1")
133-
getitem = {
134-
"https://api.github.com/repos/python/cpython/pulls/5722": {"state": "opened"},
135-
}
13654

137-
monkeypatch.setattr(asyncio, "sleep", noop_sleep)
138-
gh = FakeGH(getitem=getitem)
139-
await delete_branch.router.dispatch(event, gh)
140-
assert gh.post_data is None # does not leave a comment
141-
assert not hasattr(gh, "delete_url")
55+
gh = FakeGH()
56+
with mock.patch.object(tasks.delete_branch_task, "delay") as mock_delay:
57+
await delete_branch.router.dispatch(event, gh)
58+
mock_delay.assert_called_once_with(
59+
"backport-17ab8f0-3.7",
60+
"https://api.github.com/repos/python/cpython/pulls/5722",
61+
False,
62+
installation_id=456
63+
)
14264

14365

144-
async def test_ignore_non_miss_islingtons_prs():
66+
async def test_ignore_non_miss_islington_prs():
14567
data = {
14668
"action": "closed",
14769
"pull_request": {
14870
"number": 5722,
14971
"user": {"login": "Mariatta"},
15072
"merged": True,
151-
"merged_by": {"login": "Mariatta"},
15273
"head": {"ref": "backport-17ab8f0-3.7"},
74+
"url": "https://api.github.com/repos/python/cpython/pulls/5722",
15375
},
76+
"installation": {"id": 123},
15477
}
15578
event = sansio.Event(data, event="pull_request", delivery_id="1")
79+
15680
gh = FakeGH()
157-
await delete_branch.router.dispatch(event, gh)
158-
assert gh.post_data is None # does not leave a comment
159-
assert not hasattr(gh, "delete_url")
81+
with mock.patch.object(tasks.delete_branch_task, "delay") as mock_delay:
82+
await delete_branch.router.dispatch(event, gh)
83+
mock_delay.assert_not_called()
84+
85+
86+
def test_git_delete_branch_success():
87+
with mock.patch("subprocess.check_output") as mock_subprocess:
88+
tasks._git_delete_branch("backport-17ab8f0-3.7")
89+
mock_subprocess.assert_called_once_with(
90+
["git", "push", "origin", "--delete", "backport-17ab8f0-3.7"],
91+
stderr=mock.ANY
92+
)
93+
94+
95+
def test_git_delete_branch_failure():
96+
with mock.patch("subprocess.check_output") as mock_subprocess:
97+
import subprocess
98+
mock_subprocess.side_effect = subprocess.CalledProcessError(
99+
1, "git", output=b"error: unable to delete"
100+
)
101+
with pytest.raises(subprocess.CalledProcessError):
102+
tasks._git_delete_branch("backport-17ab8f0-3.7")

0 commit comments

Comments
 (0)