Skip to content

Commit 40da435

Browse files
committed
branch_worker: PR comments pre-processing
The format of PR comments (which is github-style markdown) may not be suitable for plain-text email responses, depending on the use-case. Implement a mechanism for pre-processing the contents of PR comments before forwarding them with email. The function transforming the content string must be a method of BranchWorker class, and then its name can be specified (or not) in the pr_comments_forwarding of the KPD config. If pre-processing function is not specified, couldn't be found or fails, the comment text remains unchanged. Signed-off-by: Ihor Solodrai <[email protected]>
1 parent ef6131d commit 40da435

File tree

7 files changed

+222
-6
lines changed

7 files changed

+222
-6
lines changed

kernel_patches_daemon/branch_worker.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,18 @@
2828
from enum import Enum
2929
from pathlib import Path
3030
from subprocess import PIPE
31-
from typing import Any, Dict, Final, Generator, IO, List, Optional, Sequence, Tuple
31+
from typing import (
32+
Any,
33+
Callable,
34+
Dict,
35+
Final,
36+
Generator,
37+
IO,
38+
List,
39+
Optional,
40+
Sequence,
41+
Tuple,
42+
)
3243

3344
import dateutil.parser
3445
import git
@@ -1444,6 +1455,40 @@ async def submit_pr_summary(
14441455
)
14451456
pr_summary_report.add(1)
14461457

1458+
def ai_review_comment_preprocessor(self, body: str) -> str:
1459+
# find the first line starting with > and remove everyting prior
1460+
match = re.search(r"^>.*$", body, re.MULTILINE)
1461+
if match:
1462+
body = body[match.start() :]
1463+
# remove triple backticks
1464+
body = re.sub(r"^```\s*$\n?", "", body, flags=re.MULTILINE)
1465+
# remove In-Reply-To-Subject tag
1466+
body = re.sub(r"^In-Reply-To-Subject:.*$\n?", "", body, flags=re.MULTILINE)
1467+
return body
1468+
1469+
def pr_comment_body_to_email_body(
1470+
self, cfg: PRCommentsForwardingConfig, comment_body: str
1471+
) -> str:
1472+
func_name: Optional[str] = cfg.body_preprocessor_func
1473+
if func_name is None:
1474+
return comment_body
1475+
1476+
maybe_func = getattr(self, func_name, None)
1477+
if not callable(maybe_func):
1478+
return comment_body
1479+
1480+
func: Callable = maybe_func
1481+
try:
1482+
email_body = func(comment_body)
1483+
if not isinstance(email_body, str):
1484+
return comment_body
1485+
return email_body
1486+
except Exception as e:
1487+
logger.error(
1488+
f"Comment body preprocessor '{func_name}' failed with exception: {e}"
1489+
)
1490+
return comment_body
1491+
14471492
async def forward_pr_comments(self, pr: PullRequest, series: Series):
14481493
# The checks and local variables are needed to make type checker happy
14491494
if self.email_config is None:
@@ -1510,9 +1555,8 @@ async def forward_pr_comments(self, pr: PullRequest, series: Series):
15101555
patch_msg = parser.parsebytes(mbox, headersonly=True)
15111556

15121557
# and forward the target comment via email
1513-
sent_msg_id = await send_pr_comment_email(
1514-
email_cfg, patch_msg, comment.body
1515-
)
1558+
email_body: str = self.pr_comment_body_to_email_body(cfg, comment.body)
1559+
sent_msg_id = await send_pr_comment_email(email_cfg, patch_msg, email_body)
15161560
if sent_msg_id is not None:
15171561
logger.info(
15181562
f"Forwarded PR comment {comment.html_url} via email, Message-Id: {sent_msg_id}"

kernel_patches_daemon/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import os
1313
import re
1414
from dataclasses import dataclass
15-
from typing import Dict, List, Optional, Set
15+
from typing import Dict, List, Optional
1616

1717

1818
logger = logging.getLogger(__name__)
@@ -112,6 +112,7 @@ class PRCommentsForwardingConfig:
112112
commenter_allowlist: List[str]
113113
recipient_allowlist: List[re.Pattern]
114114
recipient_denylist: List[re.Pattern]
115+
body_preprocessor_func: Optional[str]
115116

116117
@classmethod
117118
def from_json(cls, json: Dict) -> "PRCommentsForwardingConfig":
@@ -126,6 +127,7 @@ def from_json(cls, json: Dict) -> "PRCommentsForwardingConfig":
126127
re.compile(pattern) for pattern in json.get("recipient_denylist", [])
127128
],
128129
commenter_allowlist=json.get("commenter_allowlist", []),
130+
body_preprocessor_func=json.get("body_preprocessor_func", None),
129131
)
130132

131133

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,3 @@ project-excludes = [
6868
"**/__pycache__",
6969
"**/*venv/**/*",
7070
]
71-
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
3+
```
4+
commit 16cbc4520eb13d2065d145c76b97fb93035d81e8
5+
Author: KaFai Wan <[email protected]>
6+
7+
bpf: Fix tnum_overlap to check for zero mask intersection
8+
9+
This patch adds an early return check in tnum_overlap() to detect
10+
non-overlapping tnums when their masks have no overlapping bits. The
11+
fix addresses a syzbot report where the verifier failed to recognize
12+
that two tnums couldn't represent the same value.
13+
14+
Reported-by: [email protected]
15+
16+
> diff --git a/kernel/bpf/tnum.c b/kernel/bpf/tnum.c
17+
> index f8e70e9c3..1a75b7c9a 100644
18+
> --- a/kernel/bpf/tnum.c
19+
> +++ b/kernel/bpf/tnum.c
20+
> @@ -163,6 +163,8 @@ bool tnum_overlap(struct tnum a, struct tnum b)
21+
> {
22+
> u64 mu;
23+
>
24+
> + if (a.mask && b.mask && !(a.mask & b.mask))
25+
> + return false;
26+
^^^^^
27+
28+
Does this logic correctly detect non-overlapping tnums? Consider this
29+
counter-example:
30+
31+
a = (value=0x5, mask=0xA) // binary: value=0101, mask=1010
32+
b = (value=0xA, mask=0x5) // binary: value=1010, mask=0101
33+
34+
The masks are disjoint: (0xA & 0x5) == 0, so the new check returns
35+
false (no overlap).
36+
37+
However, these tnums actually represent overlapping sets:
38+
a represents: {5, 7, 13, 15} // 0b0101, 0b0111, 0b1101, 0b1111
39+
b represents: {10, 11, 14, 15} // 0b1010, 0b1011, 0b1110, 0b1111
40+
41+
Both sets contain 15, so they do overlap. Can this cause incorrect
42+
verifier behavior when is_branch_taken() calls tnum_overlap() to
43+
determine if JEQ/JNE branches are reachable?
44+
45+
> mu = ~a.mask & ~b.mask;
46+
> return (a.value & mu) == (b.value & mu);
47+
> }
48+
49+
50+
```
51+
52+
---
53+
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
54+
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
55+
56+
In-Reply-To-Subject: `bpf: Fix tnum_overlap to check for zero mask intersection`
57+
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/18880108453
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
> diff --git a/kernel/bpf/tnum.c b/kernel/bpf/tnum.c
2+
> index f8e70e9c3..1a75b7c9a 100644
3+
> --- a/kernel/bpf/tnum.c
4+
> +++ b/kernel/bpf/tnum.c
5+
> @@ -163,6 +163,8 @@ bool tnum_overlap(struct tnum a, struct tnum b)
6+
> {
7+
> u64 mu;
8+
>
9+
> + if (a.mask && b.mask && !(a.mask & b.mask))
10+
> + return false;
11+
^^^^^
12+
13+
Does this logic correctly detect non-overlapping tnums? Consider this
14+
counter-example:
15+
16+
a = (value=0x5, mask=0xA) // binary: value=0101, mask=1010
17+
b = (value=0xA, mask=0x5) // binary: value=1010, mask=0101
18+
19+
The masks are disjoint: (0xA & 0x5) == 0, so the new check returns
20+
false (no overlap).
21+
22+
However, these tnums actually represent overlapping sets:
23+
a represents: {5, 7, 13, 15} // 0b0101, 0b0111, 0b1101, 0b1111
24+
b represents: {10, 11, 14, 15} // 0b1010, 0b1011, 0b1110, 0b1111
25+
26+
Both sets contain 15, so they do overlap. Can this cause incorrect
27+
verifier behavior when is_branch_taken() calls tnum_overlap() to
28+
determine if JEQ/JNE branches are reachable?
29+
30+
> mu = ~a.mask & ~b.mask;
31+
> return (a.value & mu) == (b.value & mu);
32+
> }
33+
34+
35+
---
36+
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
37+
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
38+
39+
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/18880108453

tests/test_branch_worker.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1671,6 +1671,7 @@ async def test_forward_pr_comments(self, m: aioresponses) -> None:
16711671
commenter_allowlist=["test_user"],
16721672
recipient_denylist=[re.compile(".*@vger.kernel.org")],
16731673
recipient_allowlist=[],
1674+
body_preprocessor_func=None,
16741675
)
16751676
)
16761677

@@ -1724,3 +1725,76 @@ async def test_forward_pr_comments(self, m: aioresponses) -> None:
17241725
)
17251726
self.assertEqual(mock_comment.body, body)
17261727
self.assertEqual(f"<{mbox_msgid}>", in_reply_to)
1728+
1729+
1730+
class TestPRCommentBodyPreprocessor(unittest.TestCase):
1731+
def setUp(self) -> None:
1732+
patcher = patch("kernel_patches_daemon.github_connector.Github")
1733+
self._gh_mock = patcher.start()
1734+
self.addCleanup(patcher.stop)
1735+
self._bw = BranchWorkerMock()
1736+
1737+
def test_ai_review_comment_preprocessor(self):
1738+
comment_body = read_test_data_file(
1739+
"test_ai_review_comment_preprocessor/comment-body.md"
1740+
)
1741+
expected_email_body = read_test_data_file(
1742+
"test_ai_review_comment_preprocessor/expected-email-body.txt"
1743+
)
1744+
email_body = self._bw.ai_review_comment_preprocessor(comment_body)
1745+
print(email_body)
1746+
self.assertEqual(email_body, expected_email_body)
1747+
1748+
def test_pr_comment_body_to_email_body_with_no_preprocessor(self):
1749+
cfg = PRCommentsForwardingConfig(
1750+
enabled=True,
1751+
always_reply_to_author=False,
1752+
always_cc=[],
1753+
commenter_allowlist=[],
1754+
recipient_allowlist=[],
1755+
recipient_denylist=[],
1756+
body_preprocessor_func=None,
1757+
)
1758+
comment_body = read_test_data_file(
1759+
"test_ai_review_comment_preprocessor/comment-body.md"
1760+
)
1761+
email_body = self._bw.pr_comment_body_to_email_body(cfg, comment_body)
1762+
# Expect no change
1763+
self.assertEqual(email_body, comment_body)
1764+
1765+
def test_pr_comment_body_to_email_body_with_valid_preprocessor(self):
1766+
cfg = PRCommentsForwardingConfig(
1767+
enabled=True,
1768+
always_reply_to_author=False,
1769+
always_cc=[],
1770+
commenter_allowlist=[],
1771+
recipient_allowlist=[],
1772+
recipient_denylist=[],
1773+
body_preprocessor_func="ai_review_comment_preprocessor",
1774+
)
1775+
comment_body = read_test_data_file(
1776+
"test_ai_review_comment_preprocessor/comment-body.md"
1777+
)
1778+
expected_email_body = read_test_data_file(
1779+
"test_ai_review_comment_preprocessor/expected-email-body.txt"
1780+
)
1781+
email_body = self._bw.pr_comment_body_to_email_body(cfg, comment_body)
1782+
self.assertEqual(email_body, expected_email_body)
1783+
1784+
def test_pr_comment_body_to_email_body_with_invalid_preprocessor(self):
1785+
# Setup: create config with non-existent preprocessor function
1786+
cfg = PRCommentsForwardingConfig(
1787+
enabled=True,
1788+
always_reply_to_author=False,
1789+
always_cc=[],
1790+
commenter_allowlist=[],
1791+
recipient_allowlist=[],
1792+
recipient_denylist=[],
1793+
body_preprocessor_func="nonexistent_function",
1794+
)
1795+
comment_body = read_test_data_file(
1796+
"test_ai_review_comment_preprocessor/comment-body.md"
1797+
)
1798+
email_body = self._bw.pr_comment_body_to_email_body(cfg, comment_body)
1799+
# Expect no change
1800+
self.assertEqual(email_body, comment_body)

tests/test_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ def test_valid(self) -> None:
179179
commenter_allowlist=["kpd-bot[bot]"],
180180
recipient_denylist=[re.compile(".*@vger.kernel.org")],
181181
recipient_allowlist=[],
182+
body_preprocessor_func=None,
182183
),
183184
),
184185
tag_to_branch_mapping={"tag": ["app_auth_key_path"]},

0 commit comments

Comments
 (0)