Skip to content

Commit 6532274

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 6532274

File tree

3 files changed

+84
-9
lines changed

3 files changed

+84
-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: 31 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,30 @@ 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(
210+
pr, MERGE_CONFLICT_LABEL
211+
):
212+
subject_pr_targets.append(branch)
213+
214+
if len(subject_pr_targets) == 1:
215+
return subject_pr_targets
216+
217+
# If no sticky target is determined, then return all branches
218+
return tag_mapped_branches
219+
194220
async def sync_relevant_subject(self, subject: Subject) -> None:
195221
"""
196222
1. Get Subject's latest series
@@ -210,8 +236,11 @@ async def sync_relevant_subject(self, subject: Subject) -> None:
210236
)
211237
return
212238

213-
last_branch = mapped_branches[-1]
214-
for branch in mapped_branches:
239+
target_branches = await self.select_target_branches_for_subject(
240+
subject, mapped_branches
241+
)
242+
last_branch = target_branches[-1]
243+
for branch in target_branches:
215244
worker = self.workers[branch]
216245
# PR branch name == sid of the first known series
217246
pr_branch_name = await worker.subject_to_branch(subject)

tests/test_github_sync.py

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

1515
from aioresponses import aioresponses
1616

17-
from kernel_patches_daemon.branch_worker import NewPRWithNoChangeException
17+
from kernel_patches_daemon.branch_worker import (
18+
MERGE_CONFLICT_LABEL,
19+
NewPRWithNoChangeException,
20+
)
1821
from kernel_patches_daemon.config import KPDConfig, SERIES_TARGET_SEPARATOR
1922
from kernel_patches_daemon.github_sync import GithubSync
20-
from tests.common.patchwork_mock import init_pw_responses, load_test_data, PatchworkMock
23+
from tests.common.patchwork_mock import (
24+
init_pw_responses,
25+
load_test_data,
26+
PatchworkMock,
27+
)
2128

2229
TEST_BRANCH = "test-branch"
2330
TEST_BPF_NEXT_BRANCH = "test-bpf-next"
@@ -280,6 +287,45 @@ async def test_sync_relevant_subject_success_second_branch(self) -> None:
280287
list(self._gh.workers.values()), pr_mock
281288
)
282289

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

0 commit comments

Comments
 (0)