Skip to content

Commit 2d3f773

Browse files
authored
fix: correct label parsing for hyphenated GitHub usernames (#1061)
## Summary - Label parsing used `split("-")[-1]` which broke GitHub usernames containing hyphens (e.g., `Ahmad-Hafe` was parsed as `Hafe`), causing LGTM/approved/changes-requested labels to not be recognized - Fixed by using prefix-length slicing (`_label[len(PREFIX):]`) to correctly extract the full username - Switched substring `in` checks to `startswith` for consistency with the prefix-based extraction and other patterns in the file ## Bug Details The `can-be-merged` check counted only 1 LGTM instead of 2 when one reviewer had a hyphenated username. For example, label `lgtm-Ahmad-Hafe` was split as `["lgtm", "Ahmad", "Hafe"]` and only `"Hafe"` was extracted — which matched no reviewer. Affected all three label types: `lgtm-`, `approved-`, `changes-requested-`. Bug existed since commit c90382a (2023-05-24). ## Test plan - [x] Added tests for hyphenated usernames across all 3 label types (lgtm, approved, changes-requested) - [x] Added tests verifying substring labels are not falsely matched (startswith fix) - [x] All 1491 tests pass - [x] 90.62% coverage (meets 90% threshold) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved label parsing for pull request approvals to correctly recognize hyphenated usernames in LGTM, approval, and change request labels. * **Tests** * Added test coverage for label parsing with hyphenated usernames and prefix-matching validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 6c55487 commit 2d3f773

2 files changed

Lines changed: 139 additions & 9 deletions

File tree

webhook_server/libs/handlers/pull_request_handler.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,16 +1593,17 @@ async def _check_if_pr_approved(self, labels: list[str]) -> str:
15931593

15941594
if self.github_webhook.minimum_lgtm:
15951595
for _label in labels:
1596-
reviewer = _label.split(LABELS_SEPARATOR)[-1]
1597-
if LGTM_BY_LABEL_PREFIX.lower() in _label.lower() and reviewer in all_reviewers_without_pr_owner:
1598-
lgtm_count += 1
1599-
if reviewer in all_reviewers_without_pr_owner_and_lgtmed:
1600-
all_reviewers_without_pr_owner_and_lgtmed.remove(reviewer)
1596+
if _label.lower().startswith(LGTM_BY_LABEL_PREFIX.lower()):
1597+
reviewer = _label[len(LGTM_BY_LABEL_PREFIX) :]
1598+
if reviewer in all_reviewers_without_pr_owner:
1599+
lgtm_count += 1
1600+
if reviewer in all_reviewers_without_pr_owner_and_lgtmed:
1601+
all_reviewers_without_pr_owner_and_lgtmed.remove(reviewer)
16011602
self.logger.debug(f"{self.log_prefix} lgtm_count: {lgtm_count}")
16021603

16031604
for _label in labels:
1604-
if APPROVED_BY_LABEL_PREFIX.lower() in _label.lower():
1605-
approved_by.append(_label.split(LABELS_SEPARATOR)[-1])
1605+
if _label.lower().startswith(APPROVED_BY_LABEL_PREFIX.lower()):
1606+
approved_by.append(_label[len(APPROVED_BY_LABEL_PREFIX) :])
16061607
self.logger.debug(f"{self.log_prefix} approved_by: {approved_by}")
16071608

16081609
missing_approvers = list(set(self.owners_file_handler.all_pull_request_approvers.copy()))
@@ -1658,8 +1659,8 @@ def _check_labels_for_can_be_merged(self, labels: list[str]) -> str:
16581659
failure_output = ""
16591660

16601661
for _label in labels:
1661-
if CHANGED_REQUESTED_BY_LABEL_PREFIX.lower() in _label.lower():
1662-
change_request_user = _label.split(LABELS_SEPARATOR)[-1]
1662+
if _label.lower().startswith(CHANGED_REQUESTED_BY_LABEL_PREFIX.lower()):
1663+
change_request_user = _label[len(CHANGED_REQUESTED_BY_LABEL_PREFIX) :]
16631664
if change_request_user in self.owners_file_handler.all_pull_request_approvers:
16641665
failure_output += "PR has changed requests from approvers\n"
16651666
self.logger.debug(f"{self.log_prefix} Found changed request by {change_request_user}")

webhook_server/tests/test_pull_request_handler.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,6 +1680,64 @@ async def test_check_if_pr_approved_commented(self, pull_request_handler: PullRe
16801680
result = await pull_request_handler._check_if_pr_approved(labels=[f"{COMMENTED_BY_LABEL_PREFIX}reviewer1"])
16811681
assert result == "" # Empty string means no errors
16821682

1683+
@pytest.mark.asyncio
1684+
async def test_check_if_pr_approved_lgtm_hyphenated_username(
1685+
self, pull_request_handler: PullRequestHandler
1686+
) -> None:
1687+
"""Test that LGTM labels with hyphenated usernames are parsed correctly."""
1688+
hyphenated_user = "Ahmad-Hafe"
1689+
with (
1690+
patch.object(
1691+
pull_request_handler.owners_file_handler,
1692+
"owners_data_for_changed_files",
1693+
_owners_data_coroutine(),
1694+
),
1695+
patch.object(pull_request_handler.github_webhook, "minimum_lgtm", 1),
1696+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", []),
1697+
patch.object(pull_request_handler.owners_file_handler, "root_approvers", []),
1698+
patch.object(pull_request_handler.owners_file_handler, "root_reviewers", []),
1699+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_reviewers", [hyphenated_user]),
1700+
patch.object(pull_request_handler.github_webhook, "parent_committer", "someone-else"),
1701+
):
1702+
result = await pull_request_handler._check_if_pr_approved(
1703+
labels=[f"{LGTM_BY_LABEL_PREFIX}{hyphenated_user}"]
1704+
)
1705+
assert result == "" # Hyphenated username should be recognized as a valid reviewer
1706+
1707+
@pytest.mark.asyncio
1708+
async def test_check_if_pr_approved_approved_hyphenated_username(
1709+
self, pull_request_handler: PullRequestHandler
1710+
) -> None:
1711+
"""Test that approved labels with hyphenated usernames are parsed correctly."""
1712+
hyphenated_user = "Ahmad-Hafe"
1713+
with (
1714+
patch.object(
1715+
pull_request_handler.owners_file_handler,
1716+
"owners_data_for_changed_files",
1717+
_owners_data_coroutine(),
1718+
),
1719+
patch.object(pull_request_handler.github_webhook, "minimum_lgtm", 0),
1720+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", [hyphenated_user]),
1721+
patch.object(pull_request_handler.owners_file_handler, "root_approvers", [hyphenated_user]),
1722+
patch.object(pull_request_handler.owners_file_handler, "root_reviewers", []),
1723+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_reviewers", []),
1724+
):
1725+
result = await pull_request_handler._check_if_pr_approved(
1726+
labels=[f"{APPROVED_BY_LABEL_PREFIX}{hyphenated_user}"]
1727+
)
1728+
assert result == "" # Hyphenated username should be recognized as an approver
1729+
1730+
def test_check_labels_for_can_be_merged_changes_requested_hyphenated_username(
1731+
self, pull_request_handler: PullRequestHandler
1732+
) -> None:
1733+
"""Test that changes-requested labels with hyphenated usernames are parsed correctly."""
1734+
hyphenated_user = "Ahmad-Hafe"
1735+
with patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", [hyphenated_user]):
1736+
result = pull_request_handler._check_labels_for_can_be_merged(
1737+
labels=[f"{CHANGED_REQUESTED_BY_LABEL_PREFIX}{hyphenated_user}"]
1738+
)
1739+
assert "PR has changed requests from approvers" in result
1740+
16831741
def test_check_labels_for_can_be_merged_approved(self, pull_request_handler: PullRequestHandler) -> None:
16841742
# Mock the logic to return empty string (no errors) when appropriate
16851743
with patch.object(pull_request_handler, "_check_if_pr_approved", return_value=""):
@@ -1710,6 +1768,77 @@ def test_check_labels_for_can_be_merged_not_approved(self, pull_request_handler:
17101768
result = pull_request_handler._check_labels_for_can_be_merged(labels=["other-label"])
17111769
assert result == "" # Empty string means no errors
17121770

1771+
@pytest.mark.asyncio
1772+
async def test_check_if_pr_approved_lgtm_label_only_matches_prefix(
1773+
self, pull_request_handler: PullRequestHandler
1774+
) -> None:
1775+
"""Test that only labels starting with LGTM prefix are counted as LGTM."""
1776+
reviewer = "reviewer1"
1777+
with (
1778+
patch.object(
1779+
pull_request_handler.owners_file_handler,
1780+
"owners_data_for_changed_files",
1781+
_owners_data_coroutine(),
1782+
),
1783+
patch.object(pull_request_handler.github_webhook, "minimum_lgtm", 1),
1784+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", []),
1785+
patch.object(pull_request_handler.owners_file_handler, "root_approvers", []),
1786+
patch.object(pull_request_handler.owners_file_handler, "root_reviewers", []),
1787+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_reviewers", [reviewer]),
1788+
patch.object(pull_request_handler.github_webhook, "parent_committer", "someone-else"),
1789+
):
1790+
# Valid LGTM label starting with prefix should count
1791+
result = await pull_request_handler._check_if_pr_approved(labels=[f"{LGTM_BY_LABEL_PREFIX}{reviewer}"])
1792+
assert result == ""
1793+
1794+
# Label containing prefix in the middle should NOT count
1795+
result = await pull_request_handler._check_if_pr_approved(labels=[f"not-{LGTM_BY_LABEL_PREFIX}{reviewer}"])
1796+
assert "Missing lgtm from reviewers" in result
1797+
1798+
@pytest.mark.asyncio
1799+
async def test_check_if_pr_approved_approved_label_only_matches_prefix(
1800+
self, pull_request_handler: PullRequestHandler
1801+
) -> None:
1802+
"""Test that only labels starting with approved prefix are matched."""
1803+
approver = "approver1"
1804+
with (
1805+
patch.object(
1806+
pull_request_handler.owners_file_handler,
1807+
"owners_data_for_changed_files",
1808+
_owners_data_coroutine({"repo/OWNERS": {"approvers": [approver]}}),
1809+
),
1810+
patch.object(pull_request_handler.github_webhook, "minimum_lgtm", 0),
1811+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", [approver]),
1812+
patch.object(pull_request_handler.owners_file_handler, "root_approvers", []),
1813+
patch.object(pull_request_handler.owners_file_handler, "root_reviewers", []),
1814+
patch.object(pull_request_handler.owners_file_handler, "all_pull_request_reviewers", []),
1815+
):
1816+
result = await pull_request_handler._check_if_pr_approved(labels=[f"{APPROVED_BY_LABEL_PREFIX}{approver}"])
1817+
assert result == ""
1818+
1819+
result = await pull_request_handler._check_if_pr_approved(
1820+
labels=[f"not-{APPROVED_BY_LABEL_PREFIX}{approver}"]
1821+
)
1822+
assert "Missing approved from approvers" in result
1823+
1824+
def test_check_labels_for_can_be_merged_changes_requested_label_only_matches_prefix(
1825+
self, pull_request_handler: PullRequestHandler
1826+
) -> None:
1827+
"""Test that only labels starting with changes-requested prefix are matched."""
1828+
approver = "reviewer1"
1829+
with patch.object(pull_request_handler.owners_file_handler, "all_pull_request_approvers", [approver]):
1830+
# Valid changes-requested label should trigger the check
1831+
result = pull_request_handler._check_labels_for_can_be_merged(
1832+
labels=[f"{CHANGED_REQUESTED_BY_LABEL_PREFIX}{approver}"]
1833+
)
1834+
assert "PR has changed requests from approvers" in result
1835+
1836+
# Label containing prefix in the middle should NOT trigger
1837+
result = pull_request_handler._check_labels_for_can_be_merged(
1838+
labels=[f"not-{CHANGED_REQUESTED_BY_LABEL_PREFIX}{approver}"]
1839+
)
1840+
assert result == ""
1841+
17131842
@pytest.mark.asyncio
17141843
async def test_skip_if_pull_request_already_merged_merged(
17151844
self, pull_request_handler: PullRequestHandler, mock_pull_request: Mock

0 commit comments

Comments
 (0)