Skip to content

Commit b460366

Browse files
authored
fix scan join-issues bug for vteam (#2)
- mcpp-community/OpenOrg#1 Signed-off-by: SPeak Shen <speakshen@163.com>
1 parent f6a63aa commit b460366

File tree

4 files changed

+131
-0
lines changed

4 files changed

+131
-0
lines changed

src/components/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@ python src/components/scan_join_issues.py
1616

1717
**Configuration:** `config/join-config.yml`
1818

19+
**Approval Mode:**
20+
- When a team is configured with `mode: approval`, the user will only be added after approvals are found.
21+
- Approvals are detected from issue comments that contain approval keywords, or an optional approval label.
22+
- `reviewers.users`: All specified users must approve.
23+
- `reviewers.teams`: Each team requires approval from at least one member.
24+
25+
Example:
26+
```yaml
27+
teams:
28+
vteam:
29+
mode: approval
30+
team_slug: vteam
31+
reviewers:
32+
users: ["sunrisepeak"]
33+
teams: ["coreteam"]
34+
approval_keywords: ["/approve", "approve", "lgtm"]
35+
approval_label: "approved"
36+
```
37+
1938
### 2. task_checker.py
2039
2140
Scans for task issues with priority labels (P0/P1/P2) and sends reminders if they haven't been updated within configured timeouts.

src/components/scan_join_issues.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,87 @@
1414
is_org_member,
1515
add_user_to_team,
1616
is_user_in_team,
17+
list_issue_comments,
1718
list_open_join_issues,
1819
update_issue_title,
1920
)
2021
from libs.utils import load_simple_yaml
2122

23+
def _normalize_list(value):
24+
if not value:
25+
return []
26+
if isinstance(value, list):
27+
return [str(v).strip() for v in value if str(v).strip()]
28+
return [str(value).strip()]
29+
30+
def _is_approval_comment(body, keywords):
31+
if not body:
32+
return False
33+
text = body.lower()
34+
for kw in keywords:
35+
if kw and kw.lower() in text:
36+
return True
37+
return False
38+
39+
def _is_issue_approved(token, repo, org, issue, team_cfg, team_slug, verbose=False):
40+
reviewers = team_cfg.get("reviewers") or {}
41+
required_users = _normalize_list(reviewers.get("users"))
42+
required_teams = _normalize_list(reviewers.get("teams"))
43+
44+
approval_label = team_cfg.get("approval_label") or team_cfg.get("approved_label")
45+
if approval_label and has_label(issue, approval_label):
46+
return True
47+
48+
approval_keywords = _normalize_list(team_cfg.get("approval_keywords"))
49+
if not approval_keywords:
50+
approval_keywords = ["/approve", "approve", "approved", "lgtm", "同意", "批准", "通过", "已批准"]
51+
52+
if not required_users and not required_teams:
53+
if verbose:
54+
print(" ⊘ 跳过: 未配置审核人/团队")
55+
return False
56+
57+
try:
58+
comments = list_issue_comments(token, repo, issue["number"])
59+
except RuntimeError as e:
60+
if verbose:
61+
print(f" ✗ 获取评论失败: {e}")
62+
return False
63+
64+
approvals = set()
65+
for c in comments:
66+
body = c.get("body", "")
67+
if not _is_approval_comment(body, approval_keywords):
68+
continue
69+
author = c.get("user", {}).get("login", "")
70+
if author:
71+
approvals.add(author)
72+
73+
# Check required teams approvals (need at least one member approval per team)
74+
team_member_cache = {}
75+
for team in required_teams:
76+
approved_by_team = False
77+
for approver in approvals:
78+
cache_key = (team, approver)
79+
if cache_key not in team_member_cache:
80+
team_member_cache[cache_key] = is_user_in_team(token, org, team, approver)
81+
if team_member_cache[cache_key]:
82+
approved_by_team = True
83+
break
84+
if not approved_by_team:
85+
if verbose:
86+
print(f" ⊘ 跳过: 缺少团队 @{org}/{team} 成员的批准")
87+
return False
88+
89+
# Check required users approvals
90+
for user in required_users:
91+
if user not in approvals:
92+
if verbose:
93+
print(f" ⊘ 跳过: 缺少审核人 @{user} 的批准")
94+
return False
95+
96+
return True
97+
2298
def scan(verbose=False):
2399
# Load configuration first
24100
config_path = Path(__file__).parent.parent / "config" / "join-config.yml"
@@ -57,6 +133,7 @@ def scan(verbose=False):
57133
"title_updated": 0,
58134
"not_member_yet": 0,
59135
"reminder_sent": 0,
136+
"waiting_approval": 0,
60137
"team_added": 0,
61138
"team_add_failed": 0,
62139
"completed": 0,
@@ -92,6 +169,7 @@ def scan(verbose=False):
92169

93170
team_cfg = teams_cfg[target]
94171
team_slug = team_cfg.get("team_slug", "") or ""
172+
team_mode = team_cfg.get("mode", "auto")
95173

96174
if verbose:
97175
print(f" 目标团队: {target} (team_slug={team_slug})")
@@ -114,6 +192,14 @@ def scan(verbose=False):
114192
if verbose:
115193
print(f" ✓ @{author} 已是组织成员")
116194

195+
# If approval required, ensure it is approved before adding to team
196+
if team_mode == "approval" and team_slug:
197+
if not _is_issue_approved(token, repo, org, it, team_cfg, team_slug, verbose=verbose):
198+
summary["waiting_approval"] += 1
199+
if verbose:
200+
print(f" ⊘ 跳过: @{author} 等待审核\n")
201+
continue
202+
117203
# If needs team, ensure team membership
118204
if team_slug:
119205
if not is_user_in_team(token, org, team_slug, author):

src/libs/github_client.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,27 @@ def is_user_in_team(token, org, team_slug, username):
155155
code, payload = gh("GET", f"orgs/{org}/teams/{team_slug}/memberships/{username}", token)
156156
return code == 200 and payload and payload.get("state") in ("active", "pending")
157157

158+
def list_issue_comments(token, repo, issue_number, per_page=100):
159+
"""
160+
List comments for a GitHub issue.
161+
162+
Args:
163+
token: GitHub API token
164+
repo: Repository in "owner/name" format
165+
issue_number: Issue number
166+
per_page: Number of results per page (max 100)
167+
168+
Returns:
169+
List of comment objects
170+
171+
Raises:
172+
RuntimeError: If the request fails
173+
"""
174+
code, payload = gh("GET", f"repos/{repo}/issues/{issue_number}/comments?per_page={per_page}", token)
175+
if code != 200:
176+
raise RuntimeError(f"Failed to list issue comments: {code} {payload}")
177+
return payload or []
178+
158179
def search_issues(token, query, per_page=100):
159180
"""
160181
Search issues using GitHub search API.

src/libs/summary_reporter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def _format_join_issues_summary(self, summary: Dict[str, Any], include_details:
136136
completed = summary.get("completed", 0)
137137
skipped = summary.get("skipped", 0)
138138
not_member = summary.get("not_member_yet", 0)
139+
waiting_approval = summary.get("waiting_approval", 0)
139140

140141
# 简要统计
141142
lines.append(f"- **待处理请求:** {total}")
@@ -145,6 +146,8 @@ def _format_join_issues_summary(self, summary: Dict[str, Any], include_details:
145146
lines.append(f"- **标题已更新:** {summary.get('title_updated', 0)}")
146147
lines.append(f"- **等待加入组织:** {not_member}")
147148
lines.append(f" - 发送提醒: {summary.get('reminder_sent', 0)}")
149+
if waiting_approval > 0:
150+
lines.append(f"- **等待审核:** {waiting_approval}")
148151
lines.append(f"- **已添加到团队:** {summary.get('team_added', 0)}")
149152

150153
team_failed = summary.get("team_add_failed", 0)
@@ -156,6 +159,8 @@ def _format_join_issues_summary(self, summary: Dict[str, Any], include_details:
156159
# 简化模式:只显示关键指标
157160
if not_member > 0:
158161
lines.append(f"- **等待加入组织:** {not_member} (已发送 {summary.get('reminder_sent', 0)} 条提醒)")
162+
if waiting_approval > 0:
163+
lines.append(f"- **等待审核:** {waiting_approval}")
159164
if skipped > 0:
160165
lines.append(f"- **跳过:** {skipped}")
161166

0 commit comments

Comments
 (0)