diff --git a/.github/actions/rtc-auto-bounty/award_rtc.py b/.github/actions/rtc-auto-bounty/award_rtc.py index 5320c5519..5bb32c4b6 100644 --- a/.github/actions/rtc-auto-bounty/award_rtc.py +++ b/.github/actions/rtc-auto-bounty/award_rtc.py @@ -54,13 +54,37 @@ # Accepted forms: # wallet: RTCxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Wallet: RTCxxxx... -# wallet: my-github-username +# payout wallet: RTCxxxx... +# payout address if accepted: RTCxxxx... +# RTC wallet: RTCxxxx... +# miner ID for payout if accepted: contributor-id # .rtc-wallet: RTCxxxx... +_RTC_ADDRESS_RE = re.compile(r"\bRTC[0-9a-fA-F]{40}\b") + _WALLET_RE = re.compile( - r"(?:^|\n)\s*(?:wallet|\.rtc-wallet)\s*:\s*(\S+)\s*(?:\n|$)", - re.IGNORECASE, + r"""(?:^|\n)\s* + (?: + wallet + | \.rtc-wallet + | payout\s+wallet + | payout\s+address(?:\s+if\s+accepted)? + | rtc\s+wallet + ) + \s*:\s*(\S+)\s*(?:\n|$) + """, + re.IGNORECASE | re.VERBOSE, ) +_MINER_ID_RE = re.compile( + r"""(?:^|\n)\s* + miner[_\s]+id(?:\s+for\s+payout(?:\s+if\s+accepted)?)? + \s*:\s*(\S+)\s*(?:\n|$) + """, + re.IGNORECASE | re.VERBOSE, +) + +_RTC_CONTEXT_LABELS = ("payout", "wallet", "address") + # Payment-amount override in the PR body (owner can specify a custom amount). # bounty: 100 RTC # bounty: 75.5 RTC @@ -170,11 +194,27 @@ def validate(self) -> Optional[str]: # --------------------------------------------------------------------------- +def _clean_wallet_candidate(value: str) -> str: + return value.strip().rstrip(",.;").strip("`") + + def resolve_wallet_from_pr_body(pr_body: str) -> Optional[str]: - """Extract wallet address from a ``wallet: `` directive in the PR body.""" + """Extract wallet or lazy-pay miner ID from a PR body directive.""" match = _WALLET_RE.search(pr_body) if match: - return match.group(1).strip().rstrip(",") + return _clean_wallet_candidate(match.group(1)) + + match = _MINER_ID_RE.search(pr_body) + if match: + return _clean_wallet_candidate(match.group(1)) + + for line in pr_body.splitlines(): + lowered = line.lower() + if not any(label in lowered for label in _RTC_CONTEXT_LABELS): + continue + rtc_match = _RTC_ADDRESS_RE.search(line) + if rtc_match: + return rtc_match.group(0) return None diff --git a/.github/actions/rtc-auto-bounty/test_award_rtc.py b/.github/actions/rtc-auto-bounty/test_award_rtc.py index dea39c468..f7cadd378 100644 --- a/.github/actions/rtc-auto-bounty/test_award_rtc.py +++ b/.github/actions/rtc-auto-bounty/test_award_rtc.py @@ -70,6 +70,56 @@ def test_github_username_as_wallet(self): body = "wallet: some-contributor\n" self.assertEqual(resolve_wallet_from_pr_body(body), "some-contributor") + def test_payout_wallet_directive(self): + wallet = "RTC" + "a" * 40 + body = f"Fixes payout routing\n\nPayout wallet: {wallet}\n" + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + + def test_payout_address_directive(self): + wallet = "RTC" + "b" * 40 + body = f"Payout address: {wallet}\n" + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + + def test_payout_address_if_accepted_directive(self): + wallet = "RTC" + "c" * 40 + body = f"Payout address if accepted: {wallet}\n" + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + + def test_rtc_wallet_directive_variants(self): + wallet = "RTC" + "d" * 40 + samples = [ + f"RTC Wallet: {wallet}\n", + f"RTC wallet: {wallet}\n", + ] + for body in samples: + with self.subTest(body=body): + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + + def test_labeled_rtc_address_line_without_exact_directive(self): + wallet = "RTC" + "e" * 40 + body = f"Accepted payout destination -> {wallet}\n" + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + + def test_unlabeled_rtc_address_is_ignored(self): + wallet = "RTC" + "f" * 40 + body = f"Implementation note: {wallet}\n" + self.assertIsNone(resolve_wallet_from_pr_body(body)) + + def test_lazy_pay_miner_id_variants(self): + samples = { + "miner ID for payout if accepted: github:alice\n": "github:alice", + "miner ID: github:bob\n": "github:bob", + "miner_id: lazy-pay-carol\n": "lazy-pay-carol", + } + for body, expected in samples.items(): + with self.subTest(body=body): + self.assertEqual(resolve_wallet_from_pr_body(body), expected) + + def test_strips_markdown_backticks_and_sentence_punctuation(self): + wallet = "RTC" + "1" * 40 + body = f"Payout wallet: `{wallet}`.\n" + self.assertEqual(resolve_wallet_from_pr_body(body), wallet) + class TestResolveWalletFromFile(unittest.TestCase): """Test reading wallet from .rtc-wallet file."""