From 5f2dba49742a4db9475b05157c1fcaa3797ff9d6 Mon Sep 17 00:00:00 2001 From: NeeNe <26158277@qq.com> Date: Thu, 2 Jul 2026 14:07:55 +0800 Subject: [PATCH] feat(label): add guarded batch label shortcuts --- README.md | 15 +- README.zh-CN.md | 15 +- doc/changes/label-batch-safety.md | 77 ++++++ shortcuts/label/label.go | 257 +++++++++++++++++++- shortcuts/label/label_test.go | 196 ++++++++++++++- skills/gitlink-issue-tag/SKILL.md | 168 ++++++------- skills/gitlink-label/SKILL.md | 39 ++- skills/gitlink-stale-issue-manager/SKILL.md | 30 +-- 8 files changed, 669 insertions(+), 128 deletions(-) create mode 100644 doc/changes/label-batch-safety.md diff --git a/README.md b/README.md index e5e4318..8e42821 100644 --- a/README.md +++ b/README.md @@ -392,8 +392,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something # Update a label (unspecified fields are preserved) gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" -# Delete a label -gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 +# Delete a label safely +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes + +# Batch create labels safely +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes + +# Batch delete labels safely +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes ``` ### Pull Requests diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..eaeb79e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -402,8 +402,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "功能缺 # 更新标签(未指定的字段会被保留) gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" -# 删除标签 -gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 +# 安全删除标签 +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes + +# 安全批量创建标签 +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能' --dry-run +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能' --yes + +# 安全批量删除标签 +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes ``` ### Pull Request diff --git a/doc/changes/label-batch-safety.md b/doc/changes/label-batch-safety.md new file mode 100644 index 0000000..406fdd3 --- /dev/null +++ b/doc/changes/label-batch-safety.md @@ -0,0 +1,77 @@ +# Label batch safety shortcuts + +## Background + +The label shortcut group already supported listing, creating, updating, and +deleting issue labels. Deletion executed immediately, and larger taxonomy setup +or cleanup workflows still required repeated manual commands or Raw API calls. + +This change adds safer destructive operations and first-class batch helpers. + +## New and changed shortcuts + +- `label +delete` now supports `--dry-run` and requires `--yes` for real + deletion. +- `label +batch-create` creates multiple labels from a semicolon-separated + `name:color:description` list. +- `label +batch-delete` deletes multiple labels from a comma-separated ID list. + +## Safety model + +Destructive or multi-write commands should be previewed first: + +```bash +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run +``` + +After confirmation, pass `--yes`: + +```bash +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes +``` + +## Parsing rules + +- `label +batch-create --labels` uses semicolons between labels and colons + inside each label spec: `name:color:description`. +- Missing colors default to `#1E90FF`. +- Colors are validated as `#RGB` or `#RRGGBB` before any API call. +- `label +batch-delete --ids` accepts comma-separated positive integer IDs and + removes duplicates before making requests. + +## Documentation updates + +- README and README.zh-CN include safe delete and batch examples. +- `skills/gitlink-label` documents the new shortcuts and safety model. +- `skills/gitlink-issue-tag` now recommends label shortcuts instead of Raw API + calls for common label workflows. +- `skills/gitlink-stale-issue-manager` uses `label +batch-create` for stale + label bootstrap steps. + +## Tests + +Unit tests cover: + +- single delete dry-run and `--yes` confirmation; +- batch-create dry-run/default preview and real API calls; +- batch-delete dry-run/default preview, de-duplication, and real API calls; +- parser validation for label specs, colors, and ID lists; +- partial batch failure reporting. + +Suggested verification: + +```bash +go test ./shortcuts/label ./shortcuts +``` + +Full project verification: + +```bash +go test ./... +``` diff --git a/shortcuts/label/label.go b/shortcuts/label/label.go index 2b6a298..e38794e 100644 --- a/shortcuts/label/label.go +++ b/shortcuts/label/label.go @@ -75,25 +75,60 @@ func Shortcuts() []*common.Shortcut { Description: "Delete an issue label", Flags: []common.Flag{ {Name: "id", Short: "i", Usage: "Label ID", Required: true}, + {Name: "dry-run", Usage: "Preview the delete request without removing the label", Bool: true, Default: "false"}, + {Name: "yes", Usage: "Confirm label deletion", Bool: true, Default: "false"}, }, - Run: func(ctx *common.RuntimeContext) error { - if err := ctx.ResolveOwnerRepo(); err != nil { - return err - } - id, err := ctx.RequireArg("id") - if err != nil { - return err - } - env, err := ctx.CallAPI("DELETE", labelItemPath(ctx, id), nil) - if err != nil { - return err - } - return ctx.Output(env) + Run: runDelete, + }, + { + Name: "batch-create", + Description: "Batch create issue labels from semicolon-separated specs", + Flags: []common.Flag{ + {Name: "labels", Usage: "Semicolon-separated label specs: name:color:description", Required: true}, + {Name: "dry-run", Usage: "Preview labels without creating them", Bool: true, Default: "false"}, + {Name: "yes", Usage: "Confirm real batch label creation", Bool: true, Default: "false"}, }, + Run: runBatchCreate, + }, + { + Name: "batch-delete", + Description: "Batch delete issue labels by IDs", + Flags: []common.Flag{ + {Name: "ids", Usage: "Comma-separated label IDs", Required: true}, + {Name: "dry-run", Usage: "Preview labels without deleting them", Bool: true, Default: "false"}, + {Name: "yes", Usage: "Confirm real batch label deletion", Bool: true, Default: "false"}, + }, + Run: runBatchDelete, }, } } +type labelSpec struct { + Name string `json:"name" yaml:"name"` + Description string `json:"description" yaml:"description"` + Color string `json:"color" yaml:"color"` +} + +type labelBatchRequest struct { + Method string `json:"method" yaml:"method"` + Path string `json:"path" yaml:"path"` + Body map[string]interface{} `json:"body,omitempty" yaml:"body,omitempty"` +} + +type labelBatchPreview struct { + Repository string `json:"repository" yaml:"repository"` + DryRun bool `json:"dry_run" yaml:"dry_run"` + Action string `json:"action" yaml:"action"` + Requests []labelBatchRequest `json:"requests" yaml:"requests"` +} + +type labelBatchResult struct { + Request labelBatchRequest `json:"request" yaml:"request"` + OK bool `json:"ok" yaml:"ok"` + Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` + Error string `json:"error,omitempty" yaml:"error,omitempty"` +} + func runCreate(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { return err @@ -163,6 +198,131 @@ func runUpdate(ctx *common.RuntimeContext) error { return ctx.Output(env) } +func runDelete(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + path := labelItemPath(ctx, id) + if ctx.Arg("dry-run") == "true" { + return ctx.OutputData(labelBatchPreview{ + Repository: repositoryName(ctx), + DryRun: true, + Action: "delete_label", + Requests: []labelBatchRequest{{ + Method: "DELETE", + Path: path, + }}, + }) + } + if ctx.Arg("yes") != "true" { + return fmt.Errorf("label delete is destructive; run with --dry-run first, then pass --yes to confirm") + } + env, err := ctx.CallAPI("DELETE", path, nil) + if err != nil { + return err + } + return ctx.Output(env) +} + +func runBatchCreate(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + specs, err := parseLabelSpecs(ctx.Arg("labels")) + if err != nil { + return err + } + requests := make([]labelBatchRequest, 0, len(specs)) + for _, spec := range specs { + requests = append(requests, labelBatchRequest{ + Method: "POST", + Path: labelPath(ctx), + Body: map[string]interface{}{ + "name": spec.Name, + "description": spec.Description, + "color": spec.Color, + }, + }) + } + if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" { + return ctx.OutputData(labelBatchPreview{ + Repository: repositoryName(ctx), + DryRun: true, + Action: "batch_create_labels", + Requests: requests, + }) + } + return runLabelBatchRequests(ctx, "batch_create_labels", requests) +} + +func runBatchDelete(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + ids, err := parseLabelIDList(ctx.Arg("ids")) + if err != nil { + return err + } + requests := make([]labelBatchRequest, 0, len(ids)) + for _, id := range ids { + requests = append(requests, labelBatchRequest{ + Method: "DELETE", + Path: labelItemPath(ctx, id), + }) + } + if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" { + return ctx.OutputData(labelBatchPreview{ + Repository: repositoryName(ctx), + DryRun: true, + Action: "batch_delete_labels", + Requests: requests, + }) + } + return runLabelBatchRequests(ctx, "batch_delete_labels", requests) +} + +func runLabelBatchRequests(ctx *common.RuntimeContext, action string, requests []labelBatchRequest) error { + results := make([]labelBatchResult, 0, len(requests)) + succeeded := 0 + failed := 0 + for _, request := range requests { + env, err := ctx.CallAPI(request.Method, request.Path, request.Body) + result := labelBatchResult{Request: request} + if err != nil { + result.OK = false + result.Error = err.Error() + failed++ + } else { + result.OK = env.OK + result.Data = env.Data + if env.OK { + succeeded++ + } else { + failed++ + } + } + results = append(results, result) + } + if err := ctx.OutputData(map[string]interface{}{ + "repository": repositoryName(ctx), + "action": action, + "count": len(requests), + "succeeded": succeeded, + "failed": failed, + "results": results, + }); err != nil { + return err + } + if failed > 0 { + return fmt.Errorf("%d of %d label request(s) failed", failed, len(requests)) + } + return nil +} + // fetchLabel looks up a single label by id from the list endpoint. GitLink does // not expose a single-label GET, so we page through the list and match by id. // A nil result (label not found) is not an error: the caller falls back to the @@ -200,6 +360,77 @@ func labelItemPath(ctx *common.RuntimeContext, id string) string { return fmt.Sprintf("%s/%s", labelPath(ctx), url.PathEscape(id)) } +func parseLabelSpecs(raw string) ([]labelSpec, error) { + if strings.TrimSpace(raw) == "" { + return nil, fmt.Errorf("--labels is required") + } + parts := strings.Split(raw, ";") + specs := make([]labelSpec, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + fields := strings.SplitN(part, ":", 3) + name := strings.TrimSpace(fields[0]) + if name == "" { + return nil, fmt.Errorf("invalid label spec %q: name is required", part) + } + color := defaultLabelColor + if len(fields) > 1 && strings.TrimSpace(fields[1]) != "" { + color = strings.TrimSpace(fields[1]) + } + if err := validateColor(color); err != nil { + return nil, err + } + description := "" + if len(fields) > 2 { + description = strings.TrimSpace(fields[2]) + } + specs = append(specs, labelSpec{ + Name: name, + Description: description, + Color: color, + }) + } + if len(specs) == 0 { + return nil, fmt.Errorf("--labels must contain at least one label spec") + } + return specs, nil +} + +func parseLabelIDList(raw string) ([]string, error) { + if strings.TrimSpace(raw) == "" { + return nil, fmt.Errorf("--ids is required") + } + parts := strings.Split(raw, ",") + ids := make([]string, 0, len(parts)) + seen := map[string]bool{} + for _, part := range parts { + id := strings.TrimSpace(part) + if id == "" { + continue + } + n, err := strconv.Atoi(id) + if err != nil || n <= 0 { + return nil, fmt.Errorf("invalid label id %q: use positive integer IDs", id) + } + if seen[id] { + continue + } + seen[id] = true + ids = append(ids, id) + } + if len(ids) == 0 { + return nil, fmt.Errorf("--ids must contain at least one label ID") + } + return ids, nil +} + +func repositoryName(ctx *common.RuntimeContext) string { + return fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo) +} + func validateColor(color string) error { if !hexColorPattern.MatchString(color) { return fmt.Errorf("invalid --color value %q: use a hex color like #1E90FF or #abc", color) diff --git a/shortcuts/label/label_test.go b/shortcuts/label/label_test.go index 66bd5dc..1962435 100644 --- a/shortcuts/label/label_test.go +++ b/shortcuts/label/label_test.go @@ -143,11 +143,157 @@ func TestLabelDelete(t *testing.T) { }) defer server.Close() - if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7"}); err != nil { + if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7", "yes": "true"}); err != nil { t.Fatalf("delete shortcut failed: %v", err) } } +func TestLabelDeleteDryRunDoesNotCallAPI(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("delete dry-run should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7", "dry-run": "true"}); err != nil { + t.Fatalf("delete dry-run failed: %v", err) + } +} + +func TestLabelDeleteRequiresYes(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("delete without --yes should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7"}) + if err == nil { + t.Fatal("expected delete to require --yes") + } +} + +func TestLabelBatchCreateDryRunDoesNotCallAPI(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("batch-create dry-run should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runLabelShortcut(t, server, "batch-create", map[string]string{ + "labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features", + "dry-run": "true", + }) + if err != nil { + t.Fatalf("batch-create dry-run failed: %v", err) + } +} + +func TestLabelBatchCreateDefaultsToDryRunWithoutYes(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("batch-create without --yes should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + err := runLabelShortcut(t, server, "batch-create", map[string]string{ + "labels": "bug:#ee0701:Bug fixes", + }) + if err != nil { + t.Fatalf("batch-create default dry-run failed: %v", err) + } +} + +func TestLabelBatchCreateWithYesCallsEndpoints(t *testing.T) { + var payloads []map[string]interface{} + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "POST", "/v1/owner/repo/issue_tags.json") + payloads = append(payloads, decodeJSON(t, r)) + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + err := runLabelShortcut(t, server, "batch-create", map[string]string{ + "labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features", + "yes": "true", + }) + if err != nil { + t.Fatalf("batch-create failed: %v", err) + } + if len(payloads) != 2 { + t.Fatalf("got %d payloads, want 2", len(payloads)) + } + assertEqual(t, payloads[0]["name"], "bug") + assertEqual(t, payloads[0]["color"], "#ee0701") + assertEqual(t, payloads[0]["description"], "Bug fixes") + assertEqual(t, payloads[1]["name"], "feature") +} + +func TestLabelBatchCreateReportsPartialFailure(t *testing.T) { + calls := 0 + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + assertRequest(t, r, "POST", "/v1/owner/repo/issue_tags.json") + calls++ + if calls == 2 { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("server error")) + return + } + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + err := runLabelShortcut(t, server, "batch-create", map[string]string{ + "labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features", + "yes": "true", + }) + if err == nil { + t.Fatal("expected partial failure to return an error") + } + if calls != 2 { + t.Fatalf("got %d calls, want 2", calls) + } +} + +func TestLabelBatchDeleteDryRunDoesNotCallAPI(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("batch-delete dry-run should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8", "dry-run": "true"}); err != nil { + t.Fatalf("batch-delete dry-run failed: %v", err) + } +} + +func TestLabelBatchDeleteDefaultsToDryRunWithoutYes(t *testing.T) { + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("batch-delete without --yes should not call API, got: %s %s", r.Method, r.URL.Path) + }) + defer server.Close() + + if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8"}); err != nil { + t.Fatalf("batch-delete default dry-run failed: %v", err) + } +} + +func TestLabelBatchDeleteWithYesCallsEndpoints(t *testing.T) { + var paths []string + server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != "DELETE" { + t.Fatalf("got method %s, want DELETE", r.Method) + } + paths = append(paths, r.URL.Path) + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + }) + defer server.Close() + + if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8,7", "yes": "true"}); err != nil { + t.Fatalf("batch-delete failed: %v", err) + } + if len(paths) != 2 { + t.Fatalf("got %d calls, want 2", len(paths)) + } + assertEqual(t, paths[0], "/v1/owner/repo/issue_tags/7.json") + assertEqual(t, paths[1], "/v1/owner/repo/issue_tags/8.json") +} + func TestValidateColor(t *testing.T) { valid := []string{"#1E90FF", "#abc", "#ABCDEF", "#000"} for _, c := range valid { @@ -170,6 +316,54 @@ func TestLabelIDString(t *testing.T) { assertEqual(t, labelIDString(nil), "") } +func TestParseLabelSpecs(t *testing.T) { + specs, err := parseLabelSpecs("bug:#ee0701:Bug fixes; docs::Documentation") + if err != nil { + t.Fatalf("parseLabelSpecs failed: %v", err) + } + if len(specs) != 2 { + t.Fatalf("got %d specs, want 2", len(specs)) + } + assertEqual(t, specs[0].Name, "bug") + assertEqual(t, specs[0].Color, "#ee0701") + assertEqual(t, specs[0].Description, "Bug fixes") + assertEqual(t, specs[1].Name, "docs") + assertEqual(t, specs[1].Color, defaultLabelColor) + assertEqual(t, specs[1].Description, "Documentation") +} + +func TestParseLabelSpecsRejectsInvalidInput(t *testing.T) { + invalid := []string{"", ":#ee0701:missing name", "bug:red:bad color"} + for _, value := range invalid { + if _, err := parseLabelSpecs(value); err == nil { + t.Fatalf("expected %q to be invalid", value) + } + } +} + +func TestParseLabelIDList(t *testing.T) { + ids, err := parseLabelIDList("7, 8,7, 9") + if err != nil { + t.Fatalf("parseLabelIDList failed: %v", err) + } + want := []string{"7", "8", "9"} + if len(ids) != len(want) { + t.Fatalf("got %v, want %v", ids, want) + } + for i := range want { + assertEqual(t, ids[i], want[i]) + } +} + +func TestParseLabelIDListRejectsInvalidInput(t *testing.T) { + invalid := []string{"", "0", "-1", "abc", "7,abc"} + for _, value := range invalid { + if _, err := parseLabelIDList(value); err == nil { + t.Fatalf("expected %q to be invalid", value) + } + } +} + func runLabelShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { t.Helper() shortcut := findLabelShortcut(t, name) diff --git a/skills/gitlink-issue-tag/SKILL.md b/skills/gitlink-issue-tag/SKILL.md index 7f5b775..64c61a3 100644 --- a/skills/gitlink-issue-tag/SKILL.md +++ b/skills/gitlink-issue-tag/SKILL.md @@ -5,14 +5,14 @@ description: "项目标记管理:查看、创建、修改、删除 GitLink 仓 metadata: requires: bins: ["gitlink-cli"] - cliHelp: "gitlink-cli api --help" + cliHelp: "gitlink-cli label --help" --- # gitlink-tag(项目标记管理) **CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。** **CRITICAL — 所有写入/删除操作前,务必先确认用户意图。** -**CRITICAL — 项目标记通过 `gitlink-cli api` 操作,无需本地 git 命令。** +**CRITICAL — 项目标记优先通过 `gitlink-cli label` 快捷命令操作,无需本地 git 命令。** > **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) @@ -23,22 +23,25 @@ metadata: 1. **查看标记** — 列出仓库所有项目标记,支持关键词搜索和精简模式 2. **创建标记** — 创建新标记,设置名称、描述和颜色 3. **修改标记** — 修改已有标记的名称、描述或颜色 -4. **删除标记** — 删除不再需要的标记 +4. **删除标记** — 安全删除不再需要的标记 +5. **批量操作** — 批量创建/删除标记,支持 dry-run 预览和 `--yes` 确认 --- -## API 能力说明 +## Shortcut 能力说明 -GitLink API 对项目标记(issue_tags)的完整支持: +GitLink CLI 对项目标记(issue_tags)的完整支持: -| 操作 | HTTP 方法 | API 路径 | 说明 | -|------|-----------|---------|------| -| 查询标记列表 | GET | `/v1/{owner}/{repo}/issue_tags` | 支持 keyword/only_name/sort_by/sort_direction/limit/page 参数 | -| 创建标记 | POST | `/v1/{owner}/{repo}/issue_tags` | 请求体:{name, description, color} | -| 修改标记 | PATCH | `/v1/{owner}/{repo}/issue_tags/{id}` | 请求体:{name, description, color},路径参数 id 为标记 ID | -| 删除标记 | DELETE | `/v1/{owner}/{repo}/issue_tags/{id}` | 路径参数 id 为标记 ID | +| 操作 | Shortcut | 说明 | +|------|----------|------| +| 查询标记列表 | `label +list` | 支持 keyword/only_name/sort_by/sort_direction 参数 | +| 创建标记 | `label +create` | 参数:name、description、color | +| 修改标记 | `label +update` | 按 ID 修改,未指定字段会保留 | +| 删除标记 | `label +delete` | 按 ID 删除,支持 `--dry-run` 和 `--yes` | +| 批量创建 | `label +batch-create` | 解析 `name:color:description` 列表,支持 `--dry-run` 和 `--yes` | +| 批量删除 | `label +batch-delete` | 解析逗号分隔 ID,支持 `--dry-run` 和 `--yes` | -> **核心原则:** 所有操作均通过 `gitlink-cli api` 调用,无需本地 git 命令。 +> **核心原则:** 优先使用 `gitlink-cli label`。Raw API 仅在快捷命令无法覆盖的调试场景使用。 --- @@ -48,7 +51,7 @@ GitLink API 对项目标记(issue_tags)的完整支持: ```bash # 获取项目标记完整列表(含描述、颜色、关联 Issue 数量等) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json ``` **返回数据结构**: @@ -71,7 +74,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json ```bash # 搜索名称中包含关键词的标记 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json +gitlink-cli label +list --owner --repo --keyword bug --format json ``` **支持的查询参数**: @@ -87,7 +90,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format j ```bash # 仅返回名称和 ID(适用于选择标记、快速浏览等场景) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json +gitlink-cli label +list --owner --repo --only-name true --format json ``` **返回示例**: @@ -107,13 +110,13 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --forma ```bash # 按 Issue 数量倒序排列(找出最常用的标记) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=desc' --format json +gitlink-cli label +list --owner --repo --sort-by issues_count --sort-direction desc --format json # 按创建时间正序排列(最早创建的排前面) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=created_on&order_direction=asc' --format json +gitlink-cli label +list --owner --repo --sort-by created_on --sort-direction asc --format json # 按更新时间倒序排列(最近更新的排前面) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&order_direction=desc' --format json +gitlink-cli label +list --owner --repo --sort-by updated_on --sort-direction desc --format json ``` --- @@ -124,7 +127,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&ord ```bash # 创建一个项目标记 -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"测试11","description":"111","color":"#54ff85"}' --format json +gitlink-cli label +create --owner --repo -n "测试11" -d "111" -c "#54ff85" --format json ``` **请求体参数**: @@ -165,21 +168,22 @@ AI 创建标记时,如用户未指定颜色,可按标记用途推荐默认 ```bash # 创建标记后,通过关键词搜索确认 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=测试11' --format json +gitlink-cli label +list --owner --repo --keyword "测试11" --format json ``` ### 2.4 批量创建标记 -当用户需要一次创建多个标记时,逐个调用创建 API: +当用户需要一次创建多个标记时,优先使用批量快捷命令: ```bash -# 批量创建标记(逐个调用) -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"bug","description":"Bug 修复","color":"#ee0701"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"feature","description":"新功能","color":"#0075ca"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"documentation","description":"文档相关","color":"#0075ca"}' --format json +# 批量创建标记,先 dry-run 再确认 +gitlink-cli label +batch-create --owner --repo \ + --labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能;documentation:#0075ca:文档相关' --dry-run +gitlink-cli label +batch-create --owner --repo \ + --labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能;documentation:#0075ca:文档相关' --yes # 验证创建结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json +gitlink-cli label +list --owner --repo --only-name true --format json ``` > **⚠️ 批量创建前,先查询现有标记,避免创建重复名称的标记。** @@ -194,11 +198,11 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --forma ```bash # Step 1:查询标记列表,获取目标标记的 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # 在返回结果中找到目标标记的 id 字段 # Step 2:使用 ID 修改标记 -gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"测试11","description":"1112","color":"#54ff85"}' --format json +gitlink-cli label +update --owner --repo -i -n "测试11" -d "1112" -c "#54ff85" --format json ``` **请求体参数**(与创建相同): @@ -224,40 +228,40 @@ gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"测试11" ```bash # 查询获取 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=旧名称' --format json +gitlink-cli label +list --owner --repo --keyword "旧名称" --format json # 假设返回 id=5 # 修改名称(保持描述和颜色不变) -gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/5 --body '{"name":"新名称","description":"原描述","color":"#ee0701"}' --format json +gitlink-cli label +update --owner --repo -i 5 -n "新名称" --format json ``` **修改标记颜色**: ```bash # 查询获取 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json +gitlink-cli label +list --owner --repo --keyword bug --format json # 假设返回 id=3 # 修改颜色(保持名称和描述不变) -gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/3 --body '{"name":"bug","description":"Bug 修复","color":"#ff0000"}' --format json +gitlink-cli label +update --owner --repo -i 3 -c "#ff0000" --format json ``` **修改标记描述**: ```bash # 查询获取 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=feature' --format json +gitlink-cli label +list --owner --repo --keyword feature --format json # 假设返回 id=7 # 修改描述 -gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/7 --body '{"name":"feature","description":"新的功能需求描述","color":"#0075ca"}' --format json +gitlink-cli label +update --owner --repo -i 7 -d "新的功能需求描述" --format json ``` ### 3.3 修改后验证 ```bash # 修改后查询确认变更已生效 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后的名称' --format json +gitlink-cli label +list --owner --repo --keyword "修改后的名称" --format json ``` > **⚠️ 修改标记名称后,已关联该标记的 Issue 会自动更新为新名称。** @@ -272,11 +276,12 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后的名 ```bash # Step 1:查询标记列表,获取目标标记的 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要删除的标记名' --format json +gitlink-cli label +list --owner --repo --keyword "要删除的标记名" --format json # 在返回结果中找到目标标记的 id 字段 -# Step 2:删除标记 -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json +# Step 2:先预览删除,再确认执行 +gitlink-cli label +delete --owner --repo -i --dry-run +gitlink-cli label +delete --owner --repo -i --yes ``` **返回示例**: @@ -292,7 +297,7 @@ gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json ```bash # Step 1:查看所有标记,确认要删除的目标 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # Step 2:记录目标标记的 ID 和关联 Issue 数量 # 假设目标标记 id=5, name="deprecated", issues_count=3 @@ -301,10 +306,11 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json # ⚠️ 删除标记后,关联的 Issue 将失去该标记 # Step 4:执行删除 -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/5 --format json +gitlink-cli label +delete --owner --repo -i 5 --dry-run +gitlink-cli label +delete --owner --repo -i 5 --yes # Step 5:验证删除结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=deprecated' --format json +gitlink-cli label +list --owner --repo --keyword deprecated --format json # total_count 应为 0 ``` @@ -312,18 +318,17 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=deprecated' --f ```bash # Step 1:查询所有标记 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # Step 2:AI 根据用户意图筛选要删除的标记,列出 ID 列表 # 假设要删除 id=3, id=5, id=8 -# Step 3:逐个删除 -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/3 --format json -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/5 --format json -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/8 --format json +# Step 3:批量删除,先 dry-run 再确认 +gitlink-cli label +batch-delete --owner --repo --ids 3,5,8 --dry-run +gitlink-cli label +batch-delete --owner --repo --ids 3,5,8 --yes # Step 4:验证删除结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json ``` > **⚠️ 批量删除是危险操作,必须先列出待删除标记清单让用户确认后再执行。** @@ -336,7 +341,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json ```bash # 获取所有标记及关联 Issue 数量 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=asc' --format json +gitlink-cli label +list --owner --repo --sort-by issues_count --sort-direction asc --format json ``` **AI 分析规则**: @@ -355,14 +360,10 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&o ```bash # 基础标记集(适用于大多数项目) -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"bug","description":"Bug 修复或问题报告","color":"#ee0701"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"feature","description":"新功能需求","color":"#0075ca"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"enhancement","description":"功能优化或改进","color":"#5319e7"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"documentation","description":"文档相关","color":"#0075ca"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"good-first-issue","description":"适合新贡献者的问题","color":"#008672"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"help-wanted","description":"需要帮助的问题","color":"#008672"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"question","description":"使用疑问","color":"#fbca04"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"wontfix","description":"不会处理的问题","color":"#fbca04"}' --format json +gitlink-cli label +batch-create --owner --repo --dry-run \ + --labels 'bug:#ee0701:Bug 修复或问题报告;feature:#0075ca:新功能需求;enhancement:#5319e7:功能优化或改进;documentation:#0075ca:文档相关;good-first-issue:#008672:适合新贡献者的问题;help-wanted:#008672:需要帮助的问题;question:#fbca04:使用疑问;wontfix:#fbca04:不会处理的问题' +gitlink-cli label +batch-create --owner --repo --yes \ + --labels 'bug:#ee0701:Bug 修复或问题报告;feature:#0075ca:新功能需求;enhancement:#5319e7:功能优化或改进;documentation:#0075ca:文档相关;good-first-issue:#008672:适合新贡献者的问题;help-wanted:#008672:需要帮助的问题;question:#fbca04:使用疑问;wontfix:#fbca04:不会处理的问题' ``` ### 5.3 合并相似标记 @@ -371,7 +372,7 @@ gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"wontfix","desc ```bash # Step 1:查询所有标记,AI 识别相似标记对 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # Step 2:假设 "bug"(id=3) 和 "defect"(id=7) 需要合并,保留 "bug" @@ -381,23 +382,24 @@ gitlink-cli issue +list --state open --owner --repo --format json # AI 筛选标记为 "defect" 的 Issue,将其改为 "bug" # Step 4:删除 "defect" 标记 -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/7 --format json +gitlink-cli label +delete --owner --repo -i 7 --dry-run +gitlink-cli label +delete --owner --repo -i 7 --yes # Step 5:验证 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json ``` ### 5.4 查询参数组合使用 ```bash # 搜索关键词 + 仅返回名称 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug&only_name=true' --format json +gitlink-cli label +list --owner --repo --keyword bug --only-name true --format json # 按 Issue 数量倒序 + 仅返回名称(快速查看热门标记) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=desc&only_name=true' --format json +gitlink-cli label +list --owner --repo --sort-by issues_count --sort-direction desc --only-name true --format json # 按更新时间倒序(查看最近活跃的标记) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&order_direction=desc' --format json +gitlink-cli label +list --owner --repo --sort-by updated_on --sort-direction desc --format json ``` --- @@ -408,57 +410,58 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&ord ```bash # Step 1:获取项目标记完整列表 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # Step 2(可选):搜索特定标记 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json +gitlink-cli label +list --owner --repo --keyword bug --format json # Step 3(可选):查看精简列表 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json +gitlink-cli label +list --owner --repo --only-name true --format json ``` ### 6.2 创建标记流程 ```bash # Step 1:查看现有标记,避免重复 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json +gitlink-cli label +list --owner --repo --only-name true --format json # Step 2:创建标记 -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"新标记","description":"标记描述","color":"#54ff85"}' --format json +gitlink-cli label +create --owner --repo -n "新标记" -d "标记描述" -c "#54ff85" --format json # Step 3:验证创建结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=新标记' --format json +gitlink-cli label +list --owner --repo --keyword "新标记" --format json ``` ### 6.3 修改标记流程 ```bash # Step 1:查询目标标记,获取 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要修改的标记' --format json +gitlink-cli label +list --owner --repo --keyword "要修改的标记" --format json # 记录目标标记的 id # Step 2:修改标记(使用 ID) -gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"修改后名称","description":"修改后描述","color":"#ff0000"}' --format json +gitlink-cli label +update --owner --repo -i -n "修改后名称" -d "修改后描述" -c "#ff0000" --format json # Step 3:验证修改结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后名称' --format json +gitlink-cli label +list --owner --repo --keyword "修改后名称" --format json ``` ### 6.4 删除标记流程 ```bash # Step 1:查询目标标记,获取 ID 和关联 Issue 数量 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要删除的标记' --format json +gitlink-cli label +list --owner --repo --keyword "要删除的标记" --format json # 记录目标标记的 id 和 issues_count # Step 2:确认删除意图(⚠️ 如 issues_count > 0 需特别提醒) # ⚠️ 删除标记后,关联的 Issue 将失去该标记 # Step 3:删除标记 -gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json +gitlink-cli label +delete --owner --repo -i --dry-run +gitlink-cli label +delete --owner --repo -i --yes # Step 4:验证删除结果 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json ``` --- @@ -553,9 +556,10 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json 用户:"帮我的新仓库创建一套 Issue 标签" AI 执行: -1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json +1. gitlink-cli label +list --owner --repo --only-name true --format json → 确认当前标记列表为空 -2. 逐个创建基础标记(bug/feature/enhancement/documentation/good-first-issue/help-wanted/question/wontfix) +2. gitlink-cli label +batch-create --owner --repo --labels '<基础标记集>' --dry-run +3. 用户确认后执行 gitlink-cli label +batch-create --owner --repo --labels '<基础标记集>' --yes 3. 验证创建结果 4. 输出创建报告 ``` @@ -566,9 +570,9 @@ AI 执行: 用户:"把 bug 标签的颜色改成红色" AI 执行: -1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json +1. gitlink-cli label +list --owner --repo --keyword bug --format json → 获取 id 和当前信息 -2. gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"bug","description":"原描述","color":"#ee0701"}' --format json +2. gitlink-cli label +update --owner --repo -i -c "#ee0701" --format json 3. 验证修改结果 4. 输出修改报告 ``` @@ -579,10 +583,11 @@ AI 执行: 用户:"删除没有关联任何 Issue 的标签" AI 执行: -1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=asc' --format json +1. gitlink-cli label +list --owner --repo --sort-by issues_count --sort-direction asc --format json 2. AI 筛选 issues_count == 0 的标记 3. 列出待删除标记清单,请用户确认 -4. 确认后逐个删除 +4. 先执行 gitlink-cli label +batch-delete --owner --repo --ids --dry-run +5. 用户确认后执行 gitlink-cli label +batch-delete --owner --repo --ids --yes 5. 输出删除报告 ``` @@ -592,7 +597,7 @@ AI 执行: 用户:"检查有没有重复的标签" AI 执行: -1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +1. gitlink-cli label +list --owner --repo --format json 2. AI 分析语义相似的标记对(如 bug/defect、feature/enhancement) 3. 列出建议合并的标记对,请用户确认 4. 执行合并(迁移 Issue 标记 → 删除冗余标记) @@ -612,4 +617,3 @@ AI 执行: - ⚠️ **标记名称唯一**:同一仓库下标记名称不能重复 - ✅ **排序参数**:`sort_by` 支持 `updated_on`、`created_on`、`issues_count`,`sort_direction` 支持 `desc`、`asc` - ✅ **所有操作通过 gitlink-cli api**:无需本地 git 命令,所有增删查改均通过 API 完成 - diff --git a/skills/gitlink-label/SKILL.md b/skills/gitlink-label/SKILL.md index 45ee27b..378bd5e 100644 --- a/skills/gitlink-label/SKILL.md +++ b/skills/gitlink-label/SKILL.md @@ -1,7 +1,7 @@ --- name: gitlink-label -version: 1.0.0 -description: "Issue label management: list, create, update, and delete GitLink issue labels (项目标记). Triggered when a user needs to manage labels, set up a triage taxonomy, or tag issues." +version: 2.0.0 +description: "Issue label management: list, create, update, delete, and batch manage GitLink issue labels (项目标记). Triggered when a user needs to manage labels, set up a triage taxonomy, or tag issues." metadata: requires: bins: ["gitlink-cli"] @@ -11,7 +11,7 @@ metadata: # gitlink-label **CRITICAL**: Read [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) before starting. It covers authentication, permissions, global flags, and GitLink API behavior. -**CRITICAL**: Confirm user intent before running write or destructive operations such as `+create`, `+update`, or `+delete`. +**CRITICAL**: Confirm user intent before running write or destructive operations such as `+create`, `+update`, `+delete`, `+batch-create`, or `+batch-delete`. **CRITICAL**: Use `gitlink-cli` for GitLink resources. Do not use GitHub-only tools such as `gh`. ## Shortcuts @@ -21,7 +21,9 @@ metadata: | `label +list` | List issue labels | Read | | `label +create` | Create an issue label | Write | | `label +update` | Update a label, preserving unspecified fields | Write | -| `label +delete` | Delete an issue label | Destructive | +| `label +delete` | Delete an issue label with dry-run/yes protection | Destructive | +| `label +batch-create` | Batch create issue labels from `name:color:description` specs | Write | +| `label +batch-delete` | Batch delete issue labels by IDs with dry-run/yes protection | Destructive | ## Examples @@ -38,8 +40,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something # Update only the color; name and description are preserved gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" -# Delete a label -gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 +# Delete a label safely +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run +gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes + +# Batch create labels safely +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run +gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \ + --labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes + +# Batch delete labels safely +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run +gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes ``` ## Parameters @@ -49,23 +62,27 @@ gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 | `+list` | `--keyword`, `--only-name`, `--sort-by` (updated_on / created_on / issues_count), `--sort-direction` (asc / desc) | | `+create` | `--name` (required), `--description`, `--color` (hex, default `#1E90FF`) | | `+update` | `--id` (required) plus at least one of `--name`, `--description`, `--color` | -| `+delete` | `--id` (required) | +| `+delete` | `--id` (required), `--dry-run`, `--yes` | +| `+batch-create` | `--labels` (semicolon-separated `name:color:description` specs), `--dry-run`, `--yes` | +| `+batch-delete` | `--ids` (comma-separated label IDs), `--dry-run`, `--yes` | ## API Notes - Labels map to the GitLink "项目标记" / `issue_tags` API: `/api/v1/{owner}/{repo}/issue_tags`. - `--color` must be a hex value (`#RGB` or `#RRGGBB`); it is validated client-side before the API call. - `+update` first fetches the label's current values from the list endpoint and merges the requested changes, so fields you do not pass are preserved (the API requires `name`, `description`, and `color` together). +- `+delete`, `+batch-create`, and `+batch-delete` should be run with `--dry-run` first, then repeated with `--yes` after confirmation. +- `+batch-create` defaults missing colors to `#1E90FF`; duplicate IDs in `+batch-delete` are de-duplicated client-side. - To attach a label to an issue, pass its id via the issue update API field `issue_tag_ids` (see `gitlink-issue`); use `label +list --only-name true` to resolve label ids quickly. ## Typical workflow: bootstrap a triage taxonomy ```bash # Create a consistent label set for issue triage -gitlink-cli label +create -n bug -c "#D73A4A" -d "Confirmed defect" -gitlink-cli label +create -n enhancement -c "#A2EEEF" -d "Feature request" -gitlink-cli label +create -n question -c "#D876E3" -d "Needs clarification" -gitlink-cli label +create -n security -c "#B60205" -d "Security-sensitive" +gitlink-cli label +batch-create --dry-run \ + --labels 'bug:#D73A4A:Confirmed defect;enhancement:#A2EEEF:Feature request;question:#D876E3:Needs clarification;security:#B60205:Security-sensitive' +gitlink-cli label +batch-create --yes \ + --labels 'bug:#D73A4A:Confirmed defect;enhancement:#A2EEEF:Feature request;question:#D876E3:Needs clarification;security:#B60205:Security-sensitive' # Verify the taxonomy gitlink-cli label +list --only-name true --format json diff --git a/skills/gitlink-stale-issue-manager/SKILL.md b/skills/gitlink-stale-issue-manager/SKILL.md index 431a6f1..c7c7a91 100644 --- a/skills/gitlink-stale-issue-manager/SKILL.md +++ b/skills/gitlink-stale-issue-manager/SKILL.md @@ -123,22 +123,17 @@ gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals?category=comment&pa ```bash # Step 1:获取仓库现有标签列表 -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # Step 2:检查目标标签是否已存在(AI 在返回结果中查找) -# 如果目标标签不存在,则创建: - -# 创建"迟缓"标签(30-59天) -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"迟缓","description":"近期活动频率明显下降,需关注但尚未停滞","color":"#fbca04"}' --format json - -# 创建"不活跃"标签(60-89天) -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"不活跃","description":"长期无更新或互动,可能已失去推进动力","color":"#d93f0b"}' --format json - -# 创建"过期"标签(≥90天) -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"过期","description":"已超出合理响应周期,建议关闭或重新评估","color":"#b60205"}' --format json +# 如果目标标签不存在,则批量创建。先 dry-run,再在用户确认后 --yes: +gitlink-cli label +batch-create --owner --repo \ + --labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --dry-run +gitlink-cli label +batch-create --owner --repo \ + --labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --yes # Step 3:重新获取标签列表,确认标签 ID -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json ``` **标签与过期等级对应关系**: @@ -428,11 +423,12 @@ gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals --format json # Step 5(执行 — 用户确认后): # a. 预创建标签(确保标签存在) -gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json +gitlink-cli label +list --owner --repo --format json # 检查迟缓/不活跃/过期标签是否存在,不存在则创建: -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"迟缓","description":"近期活动频率明显下降,需关注但尚未停滞","color":"#fbca04"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"不活跃","description":"长期无更新或互动,可能已失去推进动力","color":"#d93f0b"}' --format json -gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"过期","description":"已超出合理响应周期,建议关闭或重新评估","color":"#b60205"}' --format json +gitlink-cli label +batch-create --owner --repo \ + --labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --dry-run +gitlink-cli label +batch-create --owner --repo \ + --labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --yes # b. 对 30-59 天 Issue:打"迟缓"标签 + 发提醒评论 gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[]}' --format json @@ -559,4 +555,4 @@ AI 应解析为: - ✅ **关闭操作不可逆**:虽然维护者可以重新打开,但评论通知已发出,应谨慎 - ✅ **建议定期执行**:推荐每周执行一次,保持 Issue 列表健康 - ⚠️ **标签操作**:打标签通过 `gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[]}'` 完成,`issue_tag_ids` 为完整替换,需包含已有标签 ID -- ⚠️ **标签预创建**:打标签前必须先查询标签列表,确认目标标签存在,不存在则先通过 `POST /v1/:owner/:repo/issue_tags` 创建 +- ⚠️ **标签预创建**:打标签前必须先用 `gitlink-cli label +list` 查询标签列表,确认目标标签存在;不存在则先用 `gitlink-cli label +batch-create --dry-run` 预览并在确认后 `--yes` 创建