forked from Gitlink/gitlink-cli
335 lines
9.3 KiB
Markdown
335 lines
9.3 KiB
Markdown
# 示例:PR 长期未活动催办工作流
|
||
|
||
> 本示例演示对长期未活动的 PR 执行催办流程。
|
||
> ⚠️ 与 Issue 不同,PR 端点暂不支持 label 操作,因此**只评论催办**,不打 stale 标签。
|
||
|
||
## 场景
|
||
|
||
- **仓库**:`Gitlink/forgeplus`
|
||
- **目标**:识别 60+ 天未活动的 open PR,评论催办;74+ 天的关闭
|
||
- **执行者**:Claude Code + 用户(人在环路)
|
||
|
||
---
|
||
|
||
## Step 0 — 准备环境
|
||
|
||
```powershell
|
||
cd D:\code\SE\Evolution_and_Maintenance_of_SE\Mission2\gitlink-cli
|
||
|
||
.\gitlink-cli.exe version
|
||
.\gitlink-cli.exe auth status
|
||
```
|
||
|
||
---
|
||
|
||
## Step 1 — 拉取 PR 列表
|
||
|
||
### 1.1 获取所有 open PR
|
||
|
||
```powershell
|
||
.\gitlink-cli.exe pr +list `
|
||
--owner Gitlink `
|
||
--repo forgeplus `
|
||
--state open `
|
||
--format json | Out-File -Encoding utf8 "$env:TEMP\prs-open.json"
|
||
```
|
||
|
||
### 1.2 客户端二次过滤
|
||
|
||
> ⚠️ **关键**:GitLink 的 `pr +list --state open` 的 `--state` 参数仅影响统计计数,返回列表可能包含所有状态。**必须**按 `pull_request_status == 0` 二次过滤。
|
||
|
||
```powershell
|
||
$raw = Get-Content "$env:TEMP\prs-open.json" -Raw | ConvertFrom-Json
|
||
|
||
# 二次过滤:仅保留真正 open 的 PR
|
||
$openPRs = $raw.data.pull_requests | Where-Object { $_.pull_request_status -eq 0 }
|
||
|
||
# 时间过滤:60+ 天未活动
|
||
$threshold = (Get-Date).AddDays(-60)
|
||
$stalePRs = $openPRs | Where-Object {
|
||
$updated = if ($_.updated_at) { [DateTime]::Parse($_.updated_at) } else { [DateTime]::Parse($_.created_at) }
|
||
$updated -lt $threshold
|
||
}
|
||
|
||
Write-Host "Total open PRs: $($openPRs.Count)"
|
||
Write-Host "Stale candidates (60+ days): $($stalePRs.Count)"
|
||
```
|
||
|
||
**示例输出**:
|
||
```
|
||
Total open PRs: 12
|
||
Stale candidates (60+ days): 4
|
||
```
|
||
|
||
---
|
||
|
||
## Step 2 — 逐个详情分析
|
||
|
||
对每个候选 PR:
|
||
|
||
```powershell
|
||
$results = @()
|
||
|
||
foreach ($pr in $stalePRs) {
|
||
# 拉取 PR 详情
|
||
$detail = (& .\gitlink-cli.exe pr +view `
|
||
--owner Gitlink --repo forgeplus `
|
||
--id $pr.pull_request_number `
|
||
--format json) | ConvertFrom-Json
|
||
|
||
# 计算天数
|
||
$lastActivity = if ($detail.data.updated_at) {
|
||
[DateTime]::Parse($detail.data.updated_at)
|
||
} else {
|
||
[DateTime]::Parse($detail.data.created_at)
|
||
}
|
||
$days = [int]((Get-Date) - $lastActivity).TotalDays
|
||
|
||
# AI 判断(PR 特化规则)
|
||
$analysis = ai_judge_pr_stale $detail
|
||
|
||
$results += [PSCustomObject]@{
|
||
Id = $pr.pull_request_number
|
||
Title = $pr.title
|
||
Author = $pr.user.login
|
||
Days = $days
|
||
Confidence = $analysis.confidence
|
||
Action = if ($days -ge 74) { "auto_close" } else { "mark_stale" }
|
||
Reason = $analysis.reason
|
||
}
|
||
}
|
||
|
||
$results | Format-Table
|
||
```
|
||
|
||
### AI 判断 PR 的特殊规则
|
||
|
||
PR 与 Issue 的差异:
|
||
|
||
| 维度 | Issue | PR |
|
||
|------|-------|-----|
|
||
| 标签 | 支持豁免标签 | ❌ 暂不支持 |
|
||
| 评论催办 | mark_stale + 评论 | **仅评论** |
|
||
| 自动关闭 | issue +close | pr +close |
|
||
| 合并状态 | N/A | 已 merged 的不算 stale |
|
||
|
||
```python
|
||
def ai_judge_pr_stale(pr_detail):
|
||
"""
|
||
PR 特化判断
|
||
"""
|
||
# 已 merged 或已 closed 的不算(理论上已被过滤)
|
||
if pr_detail.pull_request_status != 0:
|
||
return {"truly_stale": False, "exempt": True, "reason": "已 merged/closed"}
|
||
|
||
# 是否有冲突?
|
||
if pr_detail.conflict:
|
||
return {
|
||
"truly_stale": True,
|
||
"confidence": 0.9,
|
||
"reason": "存在冲突,可能需要 rebase"
|
||
}
|
||
|
||
# 是否等待 review?
|
||
if pr_detail.reviewers and not pr_detail.approved:
|
||
return {
|
||
"truly_stale": True,
|
||
"confidence": 0.75,
|
||
"reason": "等待 reviewer 回应"
|
||
}
|
||
|
||
# 作者活跃度
|
||
if pr_detail.user.login in repo_contributors:
|
||
return {
|
||
"truly_stale": True,
|
||
"confidence": 0.65,
|
||
"reason": "贡献者提交后未跟进"
|
||
}
|
||
|
||
return {
|
||
"truly_stale": True,
|
||
"confidence": 0.8,
|
||
"reason": "默认判断"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Step 3 — 展示报告
|
||
|
||
Claude Code 输出:
|
||
|
||
```
|
||
发现 4 个 60+ 天未活动的 PR:
|
||
|
||
┌──────┬────────────────────────────┬──────────┬────────┬─────────────┬────────────┐
|
||
│ # │ 标题 │ 作者 │ 天数 │ 置信度 │ 动作 │
|
||
├──────┼────────────────────────────┼──────────┼────────┼─────────────┼────────────┤
|
||
│ 8 │ feat: 新增搜索功能 │ contrib-a│ 68 │ 0.85 │ mark_stale │
|
||
│ 12 │ fix: 修复登录 bug │ newbie │ 92 │ 0.92 │ auto_close │
|
||
│ 15 │ docs: 更新 README │ user-1 │ 65 │ 0.70 │ mark_stale │
|
||
│ 21 │ refactor: 重构 API │ contrib-b│ 78 │ 0.88 │ auto_close │
|
||
└──────┴────────────────────────────┴──────────┴────────┴─────────────┴────────────┘
|
||
|
||
⚠️ 注意:PR 暂不支持 label 操作,将仅评论催办。
|
||
|
||
是否应用?[yes / 选择性 / 取消]
|
||
```
|
||
|
||
---
|
||
|
||
## Step 4 — 应用动作(用户确认后)
|
||
|
||
### 4.1 备份
|
||
|
||
```powershell
|
||
$ts = Get-Date -Format "yyyyMMddHHmmss"
|
||
.\gitlink-cli.exe pr +list `
|
||
--owner Gitlink --repo forgeplus `
|
||
--state open --format json |
|
||
Out-File -Encoding utf8 "$env:TEMP\before-pr-stale-$ts.json"
|
||
```
|
||
|
||
### 4.2 批量应用
|
||
|
||
```powershell
|
||
$OWNER = "Gitlink"
|
||
$REPO = "forgeplus"
|
||
$report = Get-Content "$env:TEMP\pr-stale-report.json" -Raw | ConvertFrom-Json
|
||
|
||
$toApply = $report.items | Where-Object {
|
||
$_.recommended_action -in @("mark_stale", "auto_close") -and
|
||
$_.ai_analysis.confidence -ge 0.6
|
||
}
|
||
|
||
foreach ($item in $toApply) {
|
||
$id = $item.number
|
||
$action = $item.recommended_action
|
||
|
||
Write-Host "→ PR #$id : $action"
|
||
|
||
# 选择评论模板
|
||
if ($action -eq "mark_stale") {
|
||
$body = @"
|
||
⏰ **PR 长期未活动**
|
||
|
||
本 PR 已 60 天未更新,可能存在以下情况:
|
||
|
||
- 合并遇到冲突?请 rebase 后重新推送
|
||
- 等待 review?可 @mention 相关维护者
|
||
- 不再需要?欢迎手动关闭
|
||
|
||
如果 **14 天内**没有新活动,将默认关闭。
|
||
|
||
> 🤖 由 gitlink-stale skill 自动生成。
|
||
"@
|
||
} else {
|
||
$body = @"
|
||
🔒 **PR 自动关闭(长期未活动)**
|
||
|
||
本 PR 已 74 天无活动,自动关闭。
|
||
|
||
- 如仍需合并,请 rebase 后重新打开
|
||
- 如有冲突,可重新发起 PR
|
||
|
||
> 🤖 由 gitlink-stale skill 自动关闭。
|
||
"@
|
||
}
|
||
|
||
# 1. 评论催办
|
||
& .\gitlink-cli.exe pr +comment `
|
||
--owner $OWNER --repo $REPO `
|
||
--id $id --body $body 2>&1 | Out-Null
|
||
|
||
# 2. 若 auto_close,关闭 PR
|
||
if ($action -eq "auto_close") {
|
||
& .\gitlink-cli.exe pr +close `
|
||
--owner $OWNER --repo $REPO `
|
||
--id $id 2>&1 | Out-Null
|
||
}
|
||
|
||
Start-Sleep -Milliseconds 500
|
||
}
|
||
|
||
Write-Host "✓ Batch applied"
|
||
```
|
||
|
||
---
|
||
|
||
## Step 5 — 验证
|
||
|
||
```powershell
|
||
# 检查 PR #8 是否已评论
|
||
.\gitlink-cli.exe pr +view `
|
||
--owner Gitlink --repo forgeplus `
|
||
--id 8 --format json |
|
||
ConvertFrom-Json |
|
||
Select-Object -ExpandProperty data |
|
||
Select-Object title, @{N="status";E={$_.pull_request_status}}, @{N="journals_count";E={$_.journals.Count}}
|
||
```
|
||
|
||
---
|
||
|
||
## 故障恢复
|
||
|
||
### 误关闭的 PR 恢复
|
||
|
||
```powershell
|
||
# 重新打开 PR(Raw API)
|
||
# 注意:GitLink PR 端点重新打开的 API 可能不完善
|
||
# 推荐做法:让作者重新发起 PR
|
||
```
|
||
|
||
### 评论失败
|
||
|
||
```powershell
|
||
# 现象:pr +comment 返回 404
|
||
# 原因:--id 用了内部 id 而非 pull_request_number
|
||
# 处理:确认 id 是网页 URL 中的 pull_request_number
|
||
```
|
||
|
||
---
|
||
|
||
## 与 Issue 处理的差异
|
||
|
||
| 维度 | Issue | PR |
|
||
|------|-------|-----|
|
||
| 标签 | 支持 stale/pinned 等 | ❌ 不支持 |
|
||
| 评论催办 | ✅ | ✅ |
|
||
| 自动关闭 | `issue +close --number N` | `pr +close --id N` |
|
||
| 客户端过滤 | 直接看 status_id | 必须看 pull_request_status(state 参数不可靠) |
|
||
| 重开 | PATCH status_id=1 | API 可能不完善 |
|
||
|
||
> 💡 **核心差异**:PR 没有 label 维度,所有"stale 状态"必须通过评论标题或正文中的 ⏰/🔒 emoji 表达。
|
||
|
||
---
|
||
|
||
## 关键检查点
|
||
|
||
- ✅ Step 1 完成后,按 `pull_request_status == 0` 二次过滤
|
||
- ✅ Step 2 PR 详情中检查是否有冲突
|
||
- ✅ Step 3 展示时明确告知用户"PR 不打标签,仅评论"
|
||
- ✅ Step 4 应用前备份 PR 列表
|
||
|
||
---
|
||
|
||
## 性能数据
|
||
|
||
| 阶段 | API 调用次数 | 耗时 |
|
||
|------|-------------|------|
|
||
| Step 1 列表 | 1 | 3s |
|
||
| Step 2 详情 | N × 1 | 8s |
|
||
| Step 4 评论+关闭 | N × 2 | 6s |
|
||
| **总计(4 个 PR)** | **13** | **~20s** |
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
PR stale 处理的核心要点:
|
||
|
||
1. ✅ **必过滤 `pull_request_status`** — `--state` 参数不可靠
|
||
2. ✅ **仅评论催办** — 不打标签
|
||
3. ✅ **AI 判断考虑 PR 特性** — 冲突、reviewer、合并状态
|
||
4. ✅ **关闭操作可逆性差** — 建议作者重新发起而非自动重开
|