Skip to content

Commit 26f1bbb

Browse files
zjwu0522claude
andauthored
✨ feat: obfuscate GitHub @ mentions to prevent notification spam (#229)
Co-authored-by: Claude <[email protected]>
1 parent f434315 commit 26f1bbb

File tree

2 files changed

+80
-5
lines changed

2 files changed

+80
-5
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ Tip: MCPMark supports **auto-resume**. When re-running, only unfinished tasks wi
152152

153153
You can also follow [Quickstart](docs/quickstart.md) for the shortest end-to-end path.
154154

155+
### Important Notice: GitHub Repository Privacy
156+
157+
> **Please ensure your evaluation repositories are set to PRIVATE.**
158+
159+
GitHub state templates are now automatically downloaded from our CDN during evaluation — no manual download is required. However, because these templates contain issues and pull requests from real open-source repositories, the recreation process includes `@username` mentions of the original authors.
160+
161+
**We have received feedback from original GitHub authors who were inadvertently notified** when evaluation repositories were created as public. To be a responsible member of the open-source community, we urge all users to:
162+
163+
1. **Always keep evaluation repositories private** during the evaluation process.
164+
2. **In the latest version**, we have added random suffixes to all `@username` mentions (e.g., `@user` becomes `@user_x7k2`) and implemented a safety check that prevents importing templates to public repositories.
165+
3. **If you are using an older version of MCPMark**, please either:
166+
- Pull the latest code immediately, or
167+
- Manually ensure all GitHub evaluation repositories are set to private.
168+
169+
Thank you for helping us maintain a respectful relationship with the open-source community.
170+
155171
---
156172

157173
## Results and metrics

src/mcp_services/github/github_state_manager.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,21 @@ def _push_repo(
253253
html_url = resp.json()["html_url"]
254254
logger.info("| [import] Target repository created: %s", html_url)
255255

256+
# Safety check: Prevent importing to public repositories
257+
# Public repos would send @ mention notifications to real users, causing spam
258+
if not private:
259+
error_msg = (
260+
"ERROR: Cannot import template to a public repository.\n\n"
261+
"Reason: The template contains @ mentions of real GitHub users from the original\n"
262+
"repository. Importing to a public repository would send notifications to these\n"
263+
"users, which is disruptive and inappropriate.\n\n"
264+
"Solution: Set private=True when calling _import_template_repo()."
265+
)
266+
logger.error(error_msg)
267+
# Clean up the created repo before raising
268+
self._delete_repository(owner, repo_name)
269+
raise RuntimeError(error_msg)
270+
256271
# Immediately disable GitHub Actions for ALL repositories to prevent any accidental triggers
257272
# We'll re-enable it later only for mcpmark-cicd
258273
logger.info(
@@ -317,7 +332,7 @@ def _create_comment(issue_number: int, body: str):
317332
def _create_issue(item: dict) -> Optional[int]:
318333
data = {
319334
"title": item["title"],
320-
"body": item.get("body", ""),
335+
"body": self._obfuscate_mentions(item.get("body", "")),
321336
"labels": item.get("labels", []),
322337
}
323338
r = self._request_with_retry(
@@ -337,7 +352,7 @@ def _create_issue(item: dict) -> Optional[int]:
337352
return new_no
338353

339354
def _create_pull(pr_itm: dict) -> Optional[int]:
340-
body = pr_itm.get("body", "")
355+
body = self._obfuscate_mentions(pr_itm.get("body", ""))
341356
if pr_itm.get("is_from_fork", False):
342357
fork_note = f"\n\n---\n_This PR was originally from a fork: **{pr_itm.get('fork_owner')}/{pr_itm.get('fork_repo')}** (branch: `{pr_itm['head']}`)_"
343358
body = body + fork_note if body else fork_note[2:]
@@ -366,7 +381,10 @@ def _create_pull(pr_itm: dict) -> Optional[int]:
366381
created_issues += 1
367382
for c in itm.get("comments", []):
368383
_create_comment(
369-
new_no, f"*Original author: @{c['user']}*\n\n{c['body']}"
384+
new_no,
385+
self._obfuscate_mentions(
386+
f"*Original author: @{c['user']}*\n\n{c['body']}"
387+
),
370388
)
371389
logger.info(
372390
"| [phase] Created %d out of %d issues", created_issues, len(issues_data)
@@ -382,12 +400,17 @@ def _create_pull(pr_itm: dict) -> Optional[int]:
382400
created_prs += 1
383401
for c in pr.get("comments", []):
384402
_create_comment(
385-
new_pr_no, f"*Original author: @{c['user']}*\n\n{c['body']}"
403+
new_pr_no,
404+
self._obfuscate_mentions(
405+
f"*Original author: @{c['user']}*\n\n{c['body']}"
406+
),
386407
)
387408
for rc in pr.get("review_comments", []):
388409
_create_comment(
389410
new_pr_no,
390-
f"*Original author: @{rc['user']}* (review)\n\n{rc['body']}",
411+
self._obfuscate_mentions(
412+
f"*Original author: @{rc['user']}* (review)\n\n{rc['body']}"
413+
),
391414
)
392415
else:
393416
skipped_prs += 1
@@ -523,6 +546,42 @@ def _delete_repository(self, owner: str, repo_name: str):
523546
else:
524547
logger.info(f"| Successfully deleted repository {owner}/{repo_name}")
525548

549+
def _obfuscate_mentions(self, text: str) -> str:
550+
"""
551+
Obfuscate @ mentions to prevent notifications to real users.
552+
553+
Replaces @username with @username_XXXX (random suffix) to ensure the mentioned
554+
user does not exist on GitHub. This prevents notification spam when importing
555+
templates that contain @ mentions from original repositories.
556+
557+
Args:
558+
text: The text content that may contain @ mentions
559+
560+
Returns:
561+
Text with obfuscated @ mentions
562+
"""
563+
import re
564+
import random
565+
import string
566+
567+
if not text:
568+
return text
569+
570+
# Pattern matches @username (GitHub usernames: alphanumeric, hyphens, max 39 chars)
571+
# Negative lookbehind (?<![a-zA-Z0-9]) ensures @ is not preceded by alphanumeric,
572+
# which excludes emails like [email protected]
573+
pattern = r"(?<![a-zA-Z0-9])@([a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?)"
574+
575+
def replace_mention(match):
576+
username = match.group(1)
577+
# Generate random 4-char suffix
578+
suffix = "".join(
579+
random.choices(string.ascii_lowercase + string.digits, k=4)
580+
)
581+
return f"@{username}_{suffix}"
582+
583+
return re.sub(pattern, replace_mention, text)
584+
526585
# ---------------------------------------------------------------------
527586
# Helper utilities (organisation vs user)
528587
# ---------------------------------------------------------------------

0 commit comments

Comments
 (0)