From 9f0973f80a2e5a60292202b403521365eb8df3ac Mon Sep 17 00:00:00 2001 From: camelliamc <16583354+camelliamc@user.noreply.gitee.com> Date: Fri, 10 Jul 2026 10:49:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=B8=A4=E6=9D=A1=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8C=96=E5=B7=A5=E4=BD=9C=E6=B5=81=EF=BC=9A=E7=A4=BE?= =?UTF-8?q?=E5=8C=BA=E8=BF=90=E8=90=A5=E8=87=AA=E5=8A=A8=E5=8C=96=20+=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E7=9C=8B=E9=97=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工作流一(社区运营自动化): - Webhook 监听 Issue 创建 → 自动关键词分类 → 打标签 + 分配责任人 - 修复 assigner_ids 字段名(原 assigned_to_id 已被后端静默忽略) - 修复标签 ID 从项目 issue_tags API 动态获取(原 tracker ID 与 issue_tag ID 不匹配) - 新增 Windows PowerShell 版本脚本 工作流二(代码质量看门人): - Webhook 监听 PR 创建 → 数据采集 → AI 代码审查 → 本地 CI → 冲突检测 → 自动合并 - 新增 02a-pr-gatekeeper.sh 核心编排脚本(603 行) - 新增 Windows PowerShell 版本 - Webhook 监听器增加 pull_request 事件路由 --- shortcuts/issue/batch.go | 11 +- workflows/01-community-ops.ps1 | 186 +++++++ workflows/01a-issue-triage.ps1 | 58 ++- workflows/01a-issue-triage.sh | 42 +- workflows/01a-webhook-listener.py | 101 +++- workflows/02-code-quality-gatekeeper.ps1 | 567 +++++++++++++++++++++ workflows/02-code-quality-gatekeeper.sh | 6 +- workflows/02a-pr-gatekeeper.ps1 | 413 ++++++++++++++++ workflows/02a-pr-gatekeeper.sh | 603 +++++++++++++++++++++++ 9 files changed, 1934 insertions(+), 53 deletions(-) create mode 100644 workflows/02-code-quality-gatekeeper.ps1 create mode 100644 workflows/02a-pr-gatekeeper.ps1 create mode 100644 workflows/02a-pr-gatekeeper.sh diff --git a/shortcuts/issue/batch.go b/shortcuts/issue/batch.go index 5fa4a2f..54aef1e 100644 --- a/shortcuts/issue/batch.go +++ b/shortcuts/issue/batch.go @@ -372,7 +372,7 @@ func runBatchAssign(ctx *common.RuntimeContext) error { summary.Results = append(summary.Results, result) continue } - if err := updateIssueField(ctx, number, map[string]interface{}{"assigned_to_id": assigneeID}); err != nil { + if err := updateIssueField(ctx, number, map[string]interface{}{"assigner_ids": []interface{}{assigneeID}}); err != nil { result.Status = "failed" result.Error = err.Error() summary.Failed++ @@ -413,7 +413,12 @@ func runBatchLabel(ctx *common.RuntimeContext) error { return err } label := ctx.Arg("label") - trackerID, err := parseTracker(label) + + tags, err := resolveIssueTags(ctx) + if err != nil { + return fmt.Errorf("cannot resolve issue tags: %w", err) + } + tagID, err := parseLabel(label, tags) if err != nil { return err } @@ -444,7 +449,7 @@ func runBatchLabel(ctx *common.RuntimeContext) error { summary.Results = append(summary.Results, result) continue } - if err := updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []int{trackerID}}); err != nil { + if err := updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []int{tagID}}); err != nil { result.Status = "failed" result.Error = err.Error() summary.Failed++ diff --git a/workflows/01-community-ops.ps1 b/workflows/01-community-ops.ps1 index 4a968ed..f498864 100644 --- a/workflows/01-community-ops.ps1 +++ b/workflows/01-community-ops.ps1 @@ -191,6 +191,190 @@ $docSection = if ($docLines.Count -gt 0) { ($docLines -join "`n") } else { "_无 $otherSection = if ($otherLines.Count -gt 0) { ($otherLines -join "`n") } else { "_无_" } Log-Ok "Issues in period: $issCount" +# -- 动态收集标签 ID 映射 (与 01a-issue-triage.ps1 一致) -- +Log-Step "Discovering label IDs..." +$tagIdMap = @{} + +# 优先从项目 issue_tags API 获取 +$tagsApi = Invoke-GL api,GET,"/v1/$Owner/$Repo/issue_tags" +if ($tagsApi) { + try { + $tData = ($tagsApi | ConvertFrom-Json).data + $tagList = if ($tData.issue_tags) { @($tData.issue_tags) } elseif ($tData -is [array]) { @($tData) } else { @() } + foreach ($t in $tagList) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } catch {} +} + +# 补充:从已有 issue 的 tags 字段收集 +foreach ($state in @("open","closed")) { + $sample = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,50 + if ($sample) { + try { + $sData = ($sample | ConvertFrom-Json).data + $sIssues = if ($sData.issues) { @($sData.issues) } elseif ($sData -is [array]) { @($sData) } else { @() } + foreach ($iss in $sIssues) { + if ($iss.tags) { + foreach ($t in $iss.tags) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } + } + } catch {} + } +} +$tagFound = @() +foreach ($k in $tagIdMap.Keys) { $tagFound += "$k($($tagIdMap[$k]))" } +if ($tagFound.Count -gt 0) { Log-Ok "Found tags: $($tagFound -join ' ')" } else { Log-Warn "No tags found in repo" } + +# -- 打标签 & 分配责任人 (自动分类后的实际写入) -- +# 收集需要处理的 Issue 信息 (编号, issue ID, 标签名, 作者) +$triageItems = @() +$seenTriage = @{} + +foreach ($state in @("open","closed")) { + $issJson2 = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,100 + if (-not $issJson2) { continue } + try { + $issData2 = ($issJson2 | ConvertFrom-Json).data + $issues2 = if ($issData2.issues) { @($issData2.issues) } elseif ($issData2 -is [array]) { @($issData2) } else { @() } + foreach ($iss in $issues2) { + $num = if ($iss.project_issues_index) { $iss.project_issues_index } elseif ($iss.number) { $iss.number } else { "?" } + if ($num -eq "?" -or $seenTriage.ContainsKey($num)) { continue } + $created = if ($iss.created_at) { $iss.created_at } else { "" } + $closed = if ($iss.closed_at) { $iss.closed_at } else { "" } + $inPeriod = $false + if ($created) { try { if (([DateTime]$created) -ge $periodStart) { $inPeriod = $true } } catch {} } + if (-not $inPeriod -and $closed) { try { if (([DateTime]$closed) -ge $periodStart) { $inPeriod = $true } } catch {} } + if (-not $inPeriod) { continue } + + $seenTriage[$num] = $true + $title2 = if ($iss.subject) { $iss.subject } elseif ($iss.title) { $iss.title } else { "" } + $desc2 = if ($iss.description) { $iss.description } else { "" } + $author2 = if ($iss.author.login) { $iss.author.login } elseif ($iss.author.username) { $iss.author.username } else { "" } + $issueId2 = if ($iss.id) { $iss.id } else { $num } + + # 检查是否已有标签 + $hasLabel = $false + if ($iss.tags -and @($iss.tags).Count -gt 0) { $hasLabel = $true } + + # 关键词分类 + $combined2 = "$title2 $desc2".ToLower() + $chosenLabel = "" + if ($combined2 -match '(?i)bug|error|crash|fault|fix|缺陷|错误|异常|崩溃|修复|故障') { + $chosenLabel = "缺陷" + } elseif ($combined2 -match '(?i)feature|enhancement|add|新增|建议|功能|特性|新功能|支持|request') { + $chosenLabel = "功能" + } elseif ($combined2 -match '(?i)doc|readme|guide|wiki|tutorial|文档|说明|教程|手册') { + $chosenLabel = "文档" + } elseif ($combined2 -match '(?i)test|测试|用例|覆盖|验证') { + $chosenLabel = "测试" + } elseif ($combined2 -match '(?i)duplicate|重复|重复的') { + $chosenLabel = "重复" + } elseif ($combined2 -match '(?i)question|疑问|不确定|讨论|澄清|是否|可否') { + $chosenLabel = "疑问" + } elseif ($combined2 -match '(?i)help|协助|帮助|协作|请求帮助|互助') { + $chosenLabel = "协助" + } elseif ($combined2 -match '(?i)postpone|wontfix|暂缓|搁置|低优|不重要|不紧急|暂不|delay') { + $chosenLabel = "搁置" + } elseif ($combined2 -match '(?i)task|todo|任务|待办|计划|安排') { + $chosenLabel = "任务" + } elseif ($combined2 -match '(?i)support|兼容|环境|依赖|平台|适配') { + $chosenLabel = "支持" + } + + # 检查是否已有负责人 + $hasAssignee = $false + if ($iss.assigned_to_id -and "$($iss.assigned_to_id)" -ne "0" -and "$($iss.assigned_to_id)" -ne "") { $hasAssignee = $true } + if ($iss.assigned_to -and $iss.assigned_to.login) { $hasAssignee = $true } + + if ($chosenLabel -or -not $hasAssignee) { + $triageItems += @{ num=$num; id=$issueId2; title=$title2; desc=$desc2; author=$author2; label=$chosenLabel; hasLabel=$hasLabel; hasAssignee=$hasAssignee } + } + } + } catch {} +} + +$labelOk = 0; $labelFail = 0; $labelSkip = 0 +$assignOk = 0; $assignFail = 0; $assignSkip = 0 + +if ($triageItems.Count -gt 0) { + Log-Step "Applying labels and assignees to $($triageItems.Count) issue(s)..." + foreach ($item in $triageItems) { + # -- 打标签 -- + if (-not $item.label) { + $labelSkip++ + } elseif ($item.hasLabel) { + Log-Info " #$($item.num) already has label, skipping" + $labelSkip++ + } elseif ($DryRun) { + Log-Warn " [DRY RUN] Would tag #$($item.num) with '$($item.label)'" + $labelSkip++ + } else { + $tgtId = $tagIdMap[$item.label] + if (-not $tgtId) { + Log-Warn " #$($item.num): label '$($item.label)' not found in repo tags — create on website first" + $labelFail++ + } else { + $bodyJson = "{`"issue_tag_ids`":[$tgtId]}" + $tagResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$($item.id)",--body,$bodyJson + if ($tagResult) { + try { + $tagOkFlag = (($tagResult | ConvertFrom-Json).ok -eq $true) + } catch { $tagOkFlag = $false } + if ($tagOkFlag) { Log-Ok " #$($item.num) tagged: $($item.label)"; $labelOk++ } else { Log-Warn " #$($item.num) tag failed"; $labelFail++ } + } else { Log-Warn " #$($item.num) tag failed (no response)"; $labelFail++ } + } + } + + # -- 分配责任人 (默认分配给 Issue 作者) -- + if ($item.hasAssignee) { + $assignSkip++ + } elseif (-not $item.author -or $item.author -eq "?") { + Log-Info " #$($item.num): no author info, skipping assign" + $assignSkip++ + } elseif ($DryRun) { + Log-Warn " [DRY RUN] Would assign #$($item.num) to @$($item.author)" + $assignSkip++ + } else { + # Resolve login name to numeric user ID + $authorID = $null + $isNum = $false + try { [void][int]$item.author; $isNum = $true } catch {} + if ($isNum) { + $authorID = $item.author + } else { + $userJson = Invoke-GL api,GET,"/users/$($item.author)" + if ($userJson) { + try { + $userData = ($userJson | ConvertFrom-Json).data + if ($userData.id) { $authorID = [int]$userData.id } + elseif ($userData.user_id) { $authorID = [int]$userData.user_id } + } catch {} + } + } + if (-not $authorID) { + Log-Warn " #$($item.num): cannot resolve user ID for '@$($item.author)'" + $assignFail++ + } else { + $bodyJson = "{`"assigner_ids`":[$authorID]}" + $assignResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$($item.id)",--body,$bodyJson + if ($assignResult) { + try { + $assignOkFlag = (($assignResult | ConvertFrom-Json).ok -eq $true) + } catch { $assignOkFlag = $false } + if ($assignOkFlag) { Log-Ok " #$($item.num) assigned to @$($item.author) (ID:$authorID)"; $assignOk++ } else { Log-Warn " #$($item.num) assign failed"; $assignFail++ } + } else { Log-Warn " #$($item.num) assign failed (no response)"; $assignFail++ } + } + } + } + Log-Ok "Labels: $labelOk applied, $labelFail failed, $labelSkip skipped" + Log-Ok "Assign: $assignOk applied, $assignFail failed, $assignSkip skipped" +} else { + Log-Info "No new issues require triage in this period" +} + # -- 贡献者汇总 -- $allContribs = @($prContributors.Keys; $issContributors.Keys) | Select-Object -Unique | Sort-Object $contribText = if ($allContribs.Count -gt 0) { ($allContribs | ForEach-Object { "- @$_" }) -join "`n" } else { "(无活跃贡献者)" } @@ -311,5 +495,7 @@ Log-Title "Complete" Write-Host " Release: $newVersion" -ForegroundColor Green Write-Host " PRs merged: $prCount" -ForegroundColor Green Write-Host " Issues: $issCount" -ForegroundColor Green +Write-Host " Labels: $labelOk applied / $labelFail failed / $labelSkip skipped" -ForegroundColor Green +Write-Host " Assigned: $assignOk applied / $assignFail failed / $assignSkip skipped" -ForegroundColor Green Write-Host " Contributors: $($allContribs.Count)" -ForegroundColor Green Write-Host " Wiki: $wikiTitle" -ForegroundColor Green diff --git a/workflows/01a-issue-triage.ps1 b/workflows/01a-issue-triage.ps1 index 4ed5b04..b93c285 100644 --- a/workflows/01a-issue-triage.ps1 +++ b/workflows/01a-issue-triage.ps1 @@ -56,6 +56,19 @@ Log-Ok "Issue `"$issueTitle`" by @$issueAuthor" Log-Step "Discovering label IDs..." $tagIdMap = @{} +# 优先从项目 issue_tags API 获取 +$tagsApi = Invoke-GL api,GET,"/v1/$Owner/$Repo/issue_tags" +if ($tagsApi) { + try { + $tData = ($tagsApi | ConvertFrom-Json).data + $tagList = if ($tData.issue_tags) { @($tData.issue_tags) } elseif ($tData -is [array]) { @($tData) } else { @() } + foreach ($t in $tagList) { + if ($t.id -and $t.name) { $tagIdMap[$t.name] = $t.id } + } + } catch {} +} + +# 补充:从已有 issue 的 tags 字段收集 foreach ($state in @("open","closed")) { $sample = Invoke-GL issue,+list,--owner,$Owner,--repo,$Repo,--state,$state,--limit,50 if ($sample) { @@ -127,11 +140,8 @@ if (-not $chosenLabel) { } else { Log-Step "Tagging with '$chosenLabel' (ID:$tgtId)..." - $curTagsJson = if ($issueData.tags) { - ($issueData.tags | ForEach-Object { "{`"id`":$($_.id),`"name`":`"$($_.name)`"" }) -join "," | ForEach-Object { "[$_]" } - } else { "[]" } - - $bodyJson = "{`"tags`":[{`"id`":$tgtId,`"name`":`"$chosenLabel`"}]}" + # GitLink v1 API 使用 issue_tag_ids (整数ID数组),不是 tags (对象数组) + $bodyJson = "{`"issue_tag_ids`":[$tgtId]}" $tagResult = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson if ($tagResult) { @@ -152,12 +162,40 @@ if (-not $targetAssignee) { } elseif ($DryRun) { Log-Warn "[DRY RUN] Would assign @$targetAssignee" } else { - $bodyJson = "{`"subject`":`"$($issueTitle -replace '"','\"')`",`"description`":`"$($issueDesc -replace '"','\"')`",`"assigned_to_id`":`"$targetAssignee`"}" - $result = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson - if ($result -and ((($result | ConvertFrom-Json).ok -eq $true))) { - Log-Ok "Assigned: @$targetAssignee" + # assigned_to_id 需要数字用户 ID,不是登录名 + # 先尝试把输入当纯数字;否则通过 /users/{login} 查询 + $assigneeID = $null + $isNumeric = $false + try { [void][int]$targetAssignee; $isNumeric = $true } catch {} + if ($isNumeric) { + $assigneeID = $targetAssignee } else { - Log-Warn "Assign failed" + Log-Info "Resolving user ID for '$targetAssignee'..." + $userJson = Invoke-GL api,GET,"/users/$targetAssignee" + if ($userJson) { + try { + $userData = ($userJson | ConvertFrom-Json).data + if ($userData.id) { + $assigneeID = [int]$userData.id + } elseif ($userData.user_id) { + $assigneeID = [int]$userData.user_id + } + } catch {} + } + } + + if (-not $assigneeID) { + Log-Warn "Cannot resolve user ID for '$targetAssignee' — skipping assign" + } else { + $escTitle = $issueTitle -replace '"','\"' + $escDesc = $issueDesc -replace '"','\"' + $bodyJson = "{`"subject`":`"$escTitle`",`"description`":`"$escDesc`",`"assigner_ids`":[$assigneeID]}" + $result = Invoke-GL api,PATCH,"/v1/$Owner/$Repo/issues/$issueId",--body,$bodyJson + if ($result -and ((($result | ConvertFrom-Json).ok -eq $true))) { + Log-Ok "Assigned: @$targetAssignee (ID:$assigneeID)" + } else { + Log-Warn "Assign failed" + } } } diff --git a/workflows/01a-issue-triage.sh b/workflows/01a-issue-triage.sh index 989f539..2381e06 100644 --- a/workflows/01a-issue-triage.sh +++ b/workflows/01a-issue-triage.sh @@ -60,8 +60,15 @@ log_ok "Issue \"$ISSUE_TITLE\" by @$ISSUE_AUTHOR" # ── Phase 2: 动态收集标签 ID 映射 ───────────────────────────────── log_step "Discovering label IDs..." -# 从已有 issue 的 tags 字段收集 name→ID 映射 ID_MAP_FILE=$(mktemp) + +# 优先从项目 issue_tags API 获取 +TAGS_JSON=$(gl_run api GET "/v1/$OWNER/$REPO/issue_tags" 2>/dev/null || true) +if [[ -n "$TAGS_JSON" ]]; then + echo "$TAGS_JSON" | jq -r '(.data.issue_tags // .data // [])[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true +fi + +# 补充:从已有 issue 的 tags 字段收集(覆盖 issue_tags API 未返回的情况) for state in open closed; do SAMPLE=$(gl_run issue +list --owner "$OWNER" --repo "$REPO" --state "$state" --limit 50 2>/dev/null || true) echo "$SAMPLE" | jq -r '(.data.issues // .data // [])[]?.tags[]? | "\(.id)|\(.name)"' 2>/dev/null >> "$ID_MAP_FILE" || true @@ -136,13 +143,9 @@ else else log_step "Tagging with '$CHOSEN_LABEL' (ID:$TGT_ID)..." - # 获取当前 tags,追加新标签(去重) - CUR_TAGS=$(echo "$ISSUE_JSON" | jq -c '[.data.tags[]? | {id:.id, name:.name}]' 2>/dev/null || echo "[]") - NEW_TAGS=$(echo "$CUR_TAGS" | jq -c --argjson nt "{\"id\":$TGT_ID,\"name\":\"$CHOSEN_LABEL\"}" \ - '. + [$nt] | unique_by(.id)' 2>/dev/null) TAG_RESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" \ - --body "{\"tags\":$NEW_TAGS}" 2>&1) || true + --body "{\"issue_tag_ids\":[$TGT_ID]}" 2>&1) || true if echo "$TAG_RESP" | jq -e '.ok == true' &>/dev/null; then log_ok "Tagged: $CHOSEN_LABEL" @@ -162,13 +165,28 @@ if [[ -z "$TARGET_ASSIGNEE" ]]; then elif [[ "$DRY_RUN" == "true" ]]; then log_warn "[DRY RUN] Would assign @$TARGET_ASSIGNEE" else - BODY="{\"assigned_to_id\":\"$TARGET_ASSIGNEE\"}" - ARESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" --body "$BODY" 2>&1) || true - if echo "$ARESP" | jq -e '.ok == true' &>/dev/null; then - log_ok "Assigned: @$TARGET_ASSIGNEE" + # Resolve login name to numeric user ID + ASSIGNEE_ID="" + if [[ "$TARGET_ASSIGNEE" =~ ^[0-9]+$ ]]; then + ASSIGNEE_ID="$TARGET_ASSIGNEE" else - ERR2=$(echo "$ARESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown") - log_warn "Assign failed: $ERR2" + USER_JSON=$(gl_run api GET "/users/$TARGET_ASSIGNEE" 2>/dev/null || true) + if [[ -n "$USER_JSON" ]]; then + ASSIGNEE_ID=$(echo "$USER_JSON" | jq -r '.data.id // .data.user_id // ""' 2>/dev/null || echo "") + fi + fi + + if [[ -z "$ASSIGNEE_ID" ]]; then + log_warn "Cannot resolve user ID for '$TARGET_ASSIGNEE' — skipping assign" + else + BODY="{\"assigner_ids\":[$ASSIGNEE_ID]}" + ARESP=$(gl_run api PATCH "/v1/$OWNER/$REPO/issues/$ISSUE_NUMBER" --body "$BODY" 2>&1) || true + if echo "$ARESP" | jq -e '.ok == true' &>/dev/null; then + log_ok "Assigned: @$TARGET_ASSIGNEE (ID:$ASSIGNEE_ID)" + else + ERR2=$(echo "$ARESP" | jq -r '.error.message // "unknown"' 2>/dev/null || echo "unknown") + log_warn "Assign failed: $ERR2" + fi fi fi diff --git a/workflows/01a-webhook-listener.py b/workflows/01a-webhook-listener.py index 2a3da6f..4db54c6 100644 --- a/workflows/01a-webhook-listener.py +++ b/workflows/01a-webhook-listener.py @@ -22,6 +22,7 @@ PORT = int(os.environ.get("WEBHOOK_PORT", "8080")) SECRET = os.environ.get("WEBHOOK_SECRET", "") SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TRIAGE_SCRIPT = os.path.join(SCRIPT_DIR, "01a-issue-triage.sh") +GATEKEEPER_SCRIPT = os.path.join(SCRIPT_DIR, "02a-pr-gatekeeper.sh") LOG_DIR = os.path.join(SCRIPT_DIR, "webhook-logs") os.makedirs(LOG_DIR, exist_ok=True) @@ -60,24 +61,34 @@ def verify_signature(body: bytes, signature_header: str) -> bool: def extract_issue_number(payload: dict) -> str: """从 webhook payload 提取 Issue 编号""" - # GitLink 格式 issue = payload.get("issue", {}) if issue: num = issue.get("project_issues_index") or issue.get("number") or issue.get("id") if num: log(f"Extracted issue number: #{num}", "OK") return str(num) - - # 直接格式 if "number" in payload: return str(payload["number"]) if "id" in payload: return str(payload["id"]) - log("Could not extract issue number from payload", "WARN") return None +def extract_pr_number(payload: dict) -> str: + """从 webhook payload 提取 PR 编号""" + pr = payload.get("pull_request", {}) or payload.get("pullrequest", {}) + if pr: + num = pr.get("number") or pr.get("pull_request_number") or pr.get("id") + if num: + log(f"Extracted PR number: #{num}", "OK") + return str(num) + if "pull_request_number" in payload: + return str(payload["pull_request_number"]) + log("Could not extract PR number from payload", "WARN") + return None + + def extract_repo_info(payload: dict): """从 payload 提取 owner/repo""" repo = payload.get("repository", {}) @@ -116,6 +127,34 @@ def run_triage_async(issue_number: str, owner: str = "", repo: str = ""): t.start() +def run_gatekeeper_async(pr_number: str, owner: str = "", repo: str = ""): + """异步调用 PR gatekeeper 脚本""" + def _run(): + cmd = ["bash", GATEKEEPER_SCRIPT, "--pr-id", pr_number] + if owner: + cmd += ["--owner", owner] + if repo: + cmd += ["--repo", repo] + + log(f"Dispatching gatekeeper: {' '.join(cmd)}", "INFO") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, cwd=SCRIPT_DIR) + outfile = os.path.join(LOG_DIR, f"gatekeeper-{pr_number}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log") + with open(outfile, "w", encoding="utf-8") as f: + f.write(f"=== STDOUT ===\n{result.stdout}\n=== STDERR ===\n{result.stderr}\n") + if result.returncode == 0: + log(f"Gatekeeper PR #{pr_number} completed successfully -> {outfile}", "OK") + else: + log(f"Gatekeeper PR #{pr_number} failed (exit={result.returncode}) -> {outfile}", "ERR") + except subprocess.TimeoutExpired: + log(f"Gatekeeper PR #{pr_number} TIMEOUT after 300s", "ERR") + except Exception as e: + log(f"Gatekeeper PR #{pr_number} error: {e}", "ERR") + + t = threading.Thread(target=_run, daemon=True) + t.start() + + class WebhookHandler(BaseHTTPRequestHandler): def log_message(self, format, *args): log(f"{self.client_address[0]} - {format % args}", "INFO") @@ -129,7 +168,7 @@ h1{{color:#333}}.ok{{color:green;font-weight:bold}}code{{background:#f0f0f0;padd
✓ Running
-Listening for issue events at /webhook
Listening for issue and pull_request events at /webhook
{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Port: {PORT} | HMAC: {'enabled' if SECRET else 'disabled'}