feat(issue): add batch comment shortcut with dry-run support #14
12
README.md
12
README.md
|
|
@ -165,10 +165,16 @@ gitlink-cli issue +list --owner Gitlink --repo forgeplus
|
|||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: Login failed" -b "Steps to reproduce..."
|
||||
|
||||
# View an issue
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -n 123
|
||||
|
||||
# Close an issue
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -n 123
|
||||
|
||||
# Preview batch comment without changing data
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 -b "Please confirm whether this issue is still relevant." --dry-run
|
||||
|
||||
# Batch add a comment before closing stale issues
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv -b "This issue is inactive. Please reply if it is still relevant."
|
||||
|
||||
# Preview batch close without changing data
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
|
|
@ -177,7 +183,7 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# Add a comment
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed"
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -n 123 -b "Fixed"
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
|
|
|||
|
|
@ -165,10 +165,16 @@ gitlink-cli issue +list --owner Gitlink --repo forgeplus
|
|||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" -b "复现步骤..."
|
||||
|
||||
# 查看 Issue
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -n 123
|
||||
|
||||
# 关闭 Issue
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -n 123
|
||||
|
||||
# 预览批量评论,不修改数据
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 123,124 -b "请确认该 Issue 是否仍需处理。" --dry-run
|
||||
|
||||
# 从 CSV 文件批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv -b "该 Issue 长期无更新,如仍需处理请回复。"
|
||||
|
||||
# 预览批量关闭,不修改数据
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
|
|
@ -177,7 +183,7 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -n 123 -b "已修复"
|
||||
```
|
||||
|
||||
### Pull Request
|
||||
|
|
|
|||
|
|
@ -12,20 +12,20 @@ import (
|
|||
|
||||
const closedIssueStatusID = 5
|
||||
|
||||
type batchCloseResult struct {
|
||||
type batchOperationResult struct {
|
||||
Number string `json:"number" yaml:"number"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Status string `json:"status" yaml:"status"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type batchCloseSummary struct {
|
||||
Repository string `json:"repository" yaml:"repository"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Results []batchCloseResult `json:"results" yaml:"results"`
|
||||
type batchOperationSummary struct {
|
||||
Repository string `json:"repository" yaml:"repository"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Total int `json:"total" yaml:"total"`
|
||||
Succeeded int `json:"succeeded" yaml:"succeeded"`
|
||||
Failed int `json:"failed" yaml:"failed"`
|
||||
Results []batchOperationResult `json:"results" yaml:"results"`
|
||||
}
|
||||
|
||||
func newBatchCloseShortcut() *common.Shortcut {
|
||||
|
|
@ -55,15 +55,15 @@ func runBatchClose(ctx *common.RuntimeContext) error {
|
|||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
summary := batchCloseSummary{
|
||||
summary := batchOperationSummary{
|
||||
Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
DryRun: dryRun,
|
||||
Total: len(numbers),
|
||||
Results: make([]batchCloseResult, 0, len(numbers)),
|
||||
Results: make([]batchOperationResult, 0, len(numbers)),
|
||||
}
|
||||
|
||||
for _, number := range numbers {
|
||||
result := batchCloseResult{Number: number, Action: "close"}
|
||||
result := batchOperationResult{Number: number, Action: "close"}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
|
|
@ -108,6 +108,85 @@ func closeIssue(ctx *common.RuntimeContext, number string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func newBatchCommentShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-comment",
|
||||
Description: "Add a comment to multiple issues by issue numbers or a CSV file",
|
||||
Flags: []common.Flag{
|
||||
{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers from the web URL, for example: 1,2,3"},
|
||||
{Name: "from", Usage: "Read issue numbers from a CSV file. Supports a number/issue_number/project_issues_index column or first column without header"},
|
||||
{Name: "body", Short: "b", Usage: "Comment body", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the issues that would be commented without changing them", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchComment,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchComment(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ctx.RequireArg("body")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(numbers) == 0 {
|
||||
return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
summary := batchOperationSummary{
|
||||
Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
DryRun: dryRun,
|
||||
Total: len(numbers),
|
||||
Results: make([]batchOperationResult, 0, len(numbers)),
|
||||
}
|
||||
|
||||
for _, number := range numbers {
|
||||
result := batchOperationResult{Number: number, Action: "comment"}
|
||||
if dryRun {
|
||||
result.Status = "planned"
|
||||
summary.Succeeded++
|
||||
summary.Results = append(summary.Results, result)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := commentIssue(ctx, number, body); err != nil {
|
||||
result.Status = "failed"
|
||||
result.Error = err.Error()
|
||||
summary.Failed++
|
||||
} else {
|
||||
result.Status = "commented"
|
||||
summary.Succeeded++
|
||||
}
|
||||
summary.Results = append(summary.Results, result)
|
||||
}
|
||||
|
||||
if err := ctx.OutputData(summary); err != nil {
|
||||
return err
|
||||
}
|
||||
if summary.Failed > 0 {
|
||||
return fmt.Errorf("%d of %d issue(s) failed to comment", summary.Failed, summary.Total)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func commentIssue(ctx *common.RuntimeContext, number, body string) error {
|
||||
payload := map[string]interface{}{
|
||||
"notes": body,
|
||||
}
|
||||
if _, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), payload); err != nil {
|
||||
return fmt.Errorf("comment issue: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func collectIssueNumbers(numbersValue, csvPath string) ([]string, error) {
|
||||
numbers, err := parseIssueNumbers(numbersValue)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ type existingIssue struct {
|
|||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
newBatchCloseShortcut(),
|
||||
newBatchCommentShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List issues",
|
||||
|
|
|
|||
|
|
@ -131,6 +131,64 @@ func TestBatchClosePreservesCurrentDescription(t *testing.T) {
|
|||
assertEqual(t, updatePayload["status_id"], float64(5))
|
||||
}
|
||||
|
||||
func TestBatchCommentPostsNotesToEachIssue(t *testing.T) {
|
||||
posted := map[string]string{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/42/journals.json":
|
||||
posted["42"] = decodeJSON(t, r)["notes"].(string)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/issues/43/journals.json":
|
||||
posted["43"] = decodeJSON(t, r)["notes"].(string)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "batch-comment", map[string]string{
|
||||
"numbers": "42,43",
|
||||
"body": "stale issue reminder",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-comment shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertEqual(t, posted["42"], "stale issue reminder")
|
||||
assertEqual(t, posted["43"], "stale issue reminder")
|
||||
}
|
||||
|
||||
func TestBatchCommentDryRunDoesNotPost(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "batch-comment", map[string]string{
|
||||
"numbers": "42,43",
|
||||
"body": "stale issue reminder",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-comment dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCommentRequiresBody(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("missing body should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runIssueShortcut(t, server, "batch-comment", map[string]string{
|
||||
"numbers": "42",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("batch-comment without body expected an error")
|
||||
}
|
||||
}
|
||||
|
||||
func runIssueShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findIssueShortcut(t, name)
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ skills/
|
|||
|-------|------|----------|
|
||||
| **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` |
|
||||
| **gitlink-repo** | 仓库管理 | `repo +list`, `repo +create`, `repo +info`, `repo +fork` |
|
||||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` |
|
||||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-comment`, `issue +batch-close` |
|
||||
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +review` |
|
||||
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +protect` |
|
||||
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +view` |
|
||||
|
|
@ -162,13 +162,16 @@ gitlink-cli repo +info --owner wbtiger --repo gitlink-cli
|
|||
gitlink-cli issue +create -t "Bug: 登录失败" -b "复现步骤..."
|
||||
|
||||
# 查看 Issue
|
||||
gitlink-cli issue +view -i 123
|
||||
gitlink-cli issue +view -n 123
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment -i 123 -b "已修复"
|
||||
gitlink-cli issue +comment -n 123 -b "已修复"
|
||||
|
||||
# 关闭 Issue
|
||||
gitlink-cli issue +close -i 123
|
||||
gitlink-cli issue +close -n 123
|
||||
|
||||
# 预览批量评论 Issue
|
||||
gitlink-cli issue +batch-comment --numbers 123,124 -b "请确认该 Issue 是否仍需处理。" --dry-run
|
||||
|
||||
# 预览批量关闭 Issue
|
||||
gitlink-cli issue +batch-close --numbers 123,124 --dry-run
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-issue
|
||||
version: 2.0.0
|
||||
description: "Issue 管理:创建、查看、更新、关闭/批量关闭 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。"
|
||||
description: "Issue 管理:创建、查看、更新、评论/批量评论、关闭/批量关闭 Issue。当用户需要操作 GitLink Issue 时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -25,8 +25,9 @@ metadata:
|
|||
| `issue +view` | Issue 详情 | 否(公开项目) |
|
||||
| `issue +update` | 更新 Issue | 是 |
|
||||
| `issue +close` | 关闭 Issue | 是 |
|
||||
| `issue +batch-close` | 批量关闭 Issue,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
| `issue +comment` | 添加评论 | 是 |
|
||||
| `issue +batch-comment` | 批量添加评论,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
| `issue +batch-close` | 批量关闭 Issue,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
|
||||
## 使用示例
|
||||
|
||||
|
|
@ -46,6 +47,12 @@ gitlink-cli issue +update --number 4 --title "新标题" --body "更新描述"
|
|||
# 关闭 Issue
|
||||
gitlink-cli issue +close --number 4
|
||||
|
||||
# 预览批量评论 Issue,不修改数据
|
||||
gitlink-cli issue +batch-comment --owner myuser --repo myrepo --numbers 123,124 --body "请确认该 Issue 是否仍需处理。" --dry-run
|
||||
|
||||
# 从 CSV 文件批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner myuser --repo myrepo --from issues.csv --body "该 Issue 长期无更新,如仍需处理请回复。"
|
||||
|
||||
# 预览批量关闭 Issue,不修改数据
|
||||
gitlink-cli issue +batch-close --owner myuser --repo myrepo --numbers 123,124 --dry-run
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
# issue +batch-comment
|
||||
|
||||
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../../gitlink-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
批量给多个 Issue 添加评论。支持直接传入 Issue 编号列表、从 CSV 读取 Issue 编号,以及用 `--dry-run` 安全预览。
|
||||
|
||||
> **Issue 编号说明:** `--numbers` 使用的是网页 URL 中可见的 Issue 编号,即 v1 API 的 `project_issues_index`,不是数据库内部 ID。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# 预览,不修改数据
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 42,43 --body "请确认该 Issue 是否仍需处理。" --dry-run
|
||||
|
||||
# 按 Issue 编号批量添加评论
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --numbers 42,43 --body "该 Issue 长期无更新,如仍需处理请回复。"
|
||||
|
||||
# 从 CSV 文件读取 Issue 编号
|
||||
gitlink-cli issue +batch-comment --owner Gitlink --repo forgeplus --from issues.csv --body "该 Issue 长期无更新,如仍需处理请回复。"
|
||||
```
|
||||
|
||||
## CSV 格式
|
||||
|
||||
CSV 文件可以包含 `number`、`issue_number` 或 `project_issues_index` 列:
|
||||
|
||||
```csv
|
||||
number,title
|
||||
42,stale issue
|
||||
43,duplicate issue
|
||||
```
|
||||
|
||||
如果没有表头,则默认第一列是 Issue 编号:
|
||||
|
||||
```csv
|
||||
42,stale issue
|
||||
43,duplicate issue
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--numbers, -n` | 否 | 逗号分隔的 Issue 编号,例如 `1,2,3` |
|
||||
| `--from` | 否 | 包含 Issue 编号的 CSV 文件 |
|
||||
| `--body, -b` | 是 | 评论内容 |
|
||||
| `--dry-run` | 否 | 仅预览计划操作,不添加评论 |
|
||||
| `--owner` | 否 | 仓库所有者(自动从 git remote 解析) |
|
||||
| `--repo` | 否 | 仓库名称(自动从 git remote 解析) |
|
||||
| `--format` | 否 | 输出格式: `json`/`table`/`yaml` |
|
||||
| `--debug` | 否 | 开启调试输出 |
|
||||
|
||||
`--numbers` 和 `--from` 至少提供一个。两者同时提供时,会按顺序合并并去重。
|
||||
|
||||
## 输出
|
||||
|
||||
命令会输出批量操作汇总:
|
||||
|
||||
```json
|
||||
{
|
||||
"repository": "Gitlink/forgeplus",
|
||||
"dry_run": true,
|
||||
"total": 2,
|
||||
"succeeded": 2,
|
||||
"failed": 0,
|
||||
"results": [
|
||||
{"number": "42", "action": "comment", "status": "planned"},
|
||||
{"number": "43", "action": "comment", "status": "planned"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
每个 Issue 使用 v1 评论接口:
|
||||
|
||||
```text
|
||||
POST /v1/{owner}/{repo}/issues/{number}/journals
|
||||
Body: { "notes": <comment body> }
|
||||
```
|
||||
|
||||
## Workflow
|
||||
|
||||
1. 与用户确认目标仓库、要评论的 Issue 编号和评论内容。
|
||||
2. 先执行 `--dry-run` 并展示计划结果。
|
||||
3. 用户确认后,再执行不带 `--dry-run` 的命令。
|
||||
4. 汇报成功数量、失败数量和失败原因。
|
||||
|
||||
> [!CAUTION]
|
||||
> 不带 `--dry-run` 是 **写操作**,执行前必须确认用户意图。
|
||||
|
||||
## References
|
||||
|
||||
- [gitlink-issue](../SKILL.md)
|
||||
- [gitlink-shared](../../gitlink-shared/SKILL.md)
|
||||
Loading…
Reference in New Issue