Skip to content

Commit a20ad99

Browse files
committed
github_sync: smarter target branch selection
Implement "sticky" target branch: if a series has an associated open PR without merge-conflict, then do not consider other target branches. The config allows for multiple target branches to be associated with the same tag, for example: "tag_to_branch_mapping": { "tag": ["target1", "target2"] } Whenever a series is being synced, KPD tries to apply it to each target branch in order, stopping at the first successful apply. Then it opens/updates a tracking PR, and closes all other PRs for this series. This works fine most of the time. However, this logic may lead to series suddenly switching branches. Example scenario: * v1 successfully applied to target1, pr1 is created for it * submitter receives CI report with v1 applied to target1 * v2 fails to apply to target1, but succeeds on target2 * KPD opens pr2 for target2, and closes pr1 * submitter receives CI report with v2 applied to target2 with no indication that something happened What should've happened in this case is pr1 labeled as "merge-conflict" and target2 not tried. The "stickiness" is implemented by checking existing PRs when determining the list of target branches to apply the series to. Signed-off-by: Ihor Solodrai <[email protected]>
1 parent 05bc4a7 commit a20ad99

File tree

3 files changed

+77
-9
lines changed

3 files changed

+77
-9
lines changed

kernel_patches_daemon/branch_worker.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ async def send_email(
351351
email_send_fail_counter.add(1)
352352

353353

354-
def _is_pr_flagged(pr: PullRequest) -> bool:
355-
for label in pr.get_labels():
356-
if MERGE_CONFLICT_LABEL == label.name:
354+
def pr_has_label(pr: PullRequest, label: str) -> bool:
355+
for l in pr.get_labels():
356+
if l.name == label:
357357
return True
358358
return False
359359

@@ -855,7 +855,7 @@ async def _comment_series_pr(
855855

856856
if pr:
857857
if (not has_merge_conflict) or (
858-
has_merge_conflict and not _is_pr_flagged(pr)
858+
has_merge_conflict and not pr_has_label(pr, MERGE_CONFLICT_LABEL)
859859
):
860860
if message:
861861
self._add_pull_request_comment(pr, message)
@@ -1109,7 +1109,7 @@ async def sync_checks(self, pr: PullRequest, series: Series) -> None:
11091109
pr.update()
11101110
# if it's merge conflict - report failure
11111111
ctx = BranchWorker.slugify_context(f"{CI_DESCRIPTION}-{self.repo_branch}")
1112-
if _is_pr_flagged(pr):
1112+
if pr_has_label(pr, MERGE_CONFLICT_LABEL):
11131113
await series.set_check(
11141114
status=Status.CONFLICT,
11151115
target_url=pr.html_url,

kernel_patches_daemon/github_sync.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
from github import Auth
1515
from github.PullRequest import PullRequest
1616
from kernel_patches_daemon.branch_worker import (
17+
MERGE_CONFLICT_LABEL,
1718
BranchWorker,
1819
parsed_pr_ref_ok,
20+
pr_has_label,
1921
same_series_different_target,
2022
parse_pr_ref,
2123
NewPRWithNoChangeException,
@@ -191,6 +193,28 @@ async def checkout_and_patch_safe(
191193
)
192194
return None
193195

196+
async def select_target_branches_for_subject(
197+
self, subject: Subject, tag_mapped_branches: List[str]
198+
) -> List[str]:
199+
if len(tag_mapped_branches) == 1:
200+
return tag_mapped_branches
201+
202+
# Check if a single relevant open PR without merge conflicts exists.
203+
# If yes, then pick it without trying other target branches.
204+
subject_pr_targets = []
205+
for branch in tag_mapped_branches:
206+
worker = self.workers[branch]
207+
subj_branch = await worker.subject_to_branch(subject)
208+
for pr in worker.prs.values():
209+
if pr.head.ref == subj_branch and not pr_has_label(pr, MERGE_CONFLICT_LABEL):
210+
subject_pr_targets.append(branch)
211+
212+
if len(subject_pr_targets) == 1:
213+
return subject_pr_targets
214+
215+
# If no sticky target is determined, then return all branches
216+
return tag_mapped_branches
217+
194218
async def sync_relevant_subject(self, subject: Subject) -> None:
195219
"""
196220
1. Get Subject's latest series
@@ -210,8 +234,11 @@ async def sync_relevant_subject(self, subject: Subject) -> None:
210234
)
211235
return
212236

213-
last_branch = mapped_branches[-1]
214-
for branch in mapped_branches:
237+
target_branches = await self.select_target_branches_for_subject(
238+
subject, mapped_branches
239+
)
240+
last_branch = target_branches[-1]
241+
for branch in target_branches:
215242
worker = self.workers[branch]
216243
# PR branch name == sid of the first known series
217244
pr_branch_name = await worker.subject_to_branch(subject)

tests/test_github_sync.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414

1515
from aioresponses import aioresponses
1616

17-
from kernel_patches_daemon.branch_worker import NewPRWithNoChangeException
17+
from kernel_patches_daemon.branch_worker import MERGE_CONFLICT_LABEL, NewPRWithNoChangeException
1818
from kernel_patches_daemon.config import KPDConfig, SERIES_TARGET_SEPARATOR
1919
from kernel_patches_daemon.github_sync import GithubSync
20-
from tests.common.patchwork_mock import init_pw_responses, load_test_data, PatchworkMock
20+
from tests.common.patchwork_mock import (
21+
init_pw_responses,
22+
load_test_data,
23+
PatchworkMock,
24+
)
2125

2226
TEST_BRANCH = "test-branch"
2327
TEST_BPF_NEXT_BRANCH = "test-bpf-next"
@@ -280,6 +284,43 @@ async def test_sync_relevant_subject_success_second_branch(self) -> None:
280284
list(self._gh.workers.values()), pr_mock
281285
)
282286

287+
def _setup_test_select_target_branches_for_subject(self):
288+
series_prefix = "series/123123"
289+
subject_mock = MagicMock()
290+
subject_mock.subject = "Test subject"
291+
subject_mock.branch = AsyncMock(return_value=series_prefix)
292+
293+
pr_mock = MagicMock()
294+
pr_mock.head.ref = f"{series_prefix}{SERIES_TARGET_SEPARATOR}{TEST_BPF_NEXT_BRANCH}"
295+
296+
worker_mock = self._gh.workers[TEST_BPF_NEXT_BRANCH]
297+
worker_mock.prs = {subject_mock.subject: pr_mock}
298+
299+
return subject_mock, pr_mock
300+
301+
async def test_select_target_branches_for_subject(self) -> None:
302+
subject_mock, _ = self._setup_test_select_target_branches_for_subject()
303+
mapped_branches = [TEST_BRANCH, TEST_BPF_NEXT_BRANCH]
304+
305+
selected_branches = await self._gh.select_target_branches_for_subject(
306+
subject_mock, mapped_branches
307+
)
308+
309+
self.assertEqual(selected_branches, [TEST_BPF_NEXT_BRANCH])
310+
311+
async def test_select_target_branches_for_subject_merge_conflict(self) -> None:
312+
subject_mock, pr_mock = self._setup_test_select_target_branches_for_subject()
313+
label = MagicMock()
314+
label.name = MERGE_CONFLICT_LABEL
315+
pr_mock.get_labels = MagicMock(return_value=[label])
316+
mapped_branches = [TEST_BRANCH, TEST_BPF_NEXT_BRANCH]
317+
318+
selected_branches = await self._gh.select_target_branches_for_subject(
319+
subject_mock, mapped_branches
320+
)
321+
322+
self.assertEqual(selected_branches, mapped_branches)
323+
283324
@aioresponses()
284325
async def test_sync_patches_pr_summary_success(self, m: aioresponses) -> None:
285326
"""

0 commit comments

Comments
 (0)