From 132151d68e10cab7e3b8eaa7d10e011a881c5f8b Mon Sep 17 00:00:00 2001 From: NeeNe <26158277@qq.com> Date: Thu, 2 Jul 2026 13:10:51 +0800 Subject: [PATCH] feat(pm): add project management shortcuts --- README.md | 21 +++++ README.zh-CN.md | 20 +++++ doc/changes/pm-shortcuts.md | 60 ++++++++++++++ internal/i18n/locales/en-US.json | 8 ++ internal/i18n/locales/zh-CN.json | 8 ++ shortcuts/pm/pm.go | 71 +++++++++++++++++ shortcuts/pm/pm_test.go | 133 +++++++++++++++++++++++++++++++ shortcuts/register.go | 3 + shortcuts/register_test.go | 2 +- skills/README.md | 2 +- skills/gitlink-pm/SKILL.md | 55 ++++++++----- 11 files changed, 363 insertions(+), 20 deletions(-) create mode 100644 doc/changes/pm-shortcuts.md create mode 100644 shortcuts/pm/pm.go create mode 100644 shortcuts/pm/pm_test.go diff --git a/README.md b/README.md index e5e4318..682d6f7 100644 --- a/README.md +++ b/README.md @@ -508,6 +508,26 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run ``` +### Project Management + +`pm` surfaces GitLink project-management data for dashboards, sprints, weekly +issues, labels, pipelines, and action runs. PM commands require the numeric +GitLink project ID. + +```bash +# Query PM dashboards and sprint issues +gitlink-cli pm +dashboards --project-id 123 --limit 20 +gitlink-cli pm +sprint-issues --project-id 123 --page 1 --limit 20 + +# Query weekly issues and PM issue tags +gitlink-cli pm +weekly-issues --project-id 123 +gitlink-cli pm +issue-tags --project-id 123 + +# Query PM pipelines and action runs +gitlink-cli pm +pipelines --project-id 123 +gitlink-cli pm +action-runs --project-id 123 +``` + ### Ignore File Templates ```bash @@ -779,6 +799,7 @@ gitlink-cli/ │ ├── org/ # Organization shortcuts │ ├── ci/ # CI shortcuts │ ├── pipeline/ # Pipeline shortcuts +│ ├── pm/ # Project management shortcuts │ ├── search/ # Search shortcuts │ ├── user/ # User shortcuts │ └── register.go # Registration entry point diff --git a/README.zh-CN.md b/README.zh-CN.md index 6a8879d..b36a649 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -486,6 +486,25 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run ``` +### 项目管理 + +`pm` 暴露 GitLink 项目管理数据,覆盖看板、Sprint、周报、标签、流水线和 Action 运行记录。 +PM 命令需要传入数字项目 ID。 + +```bash +# 查询 PM 看板和 Sprint Issue +gitlink-cli pm +dashboards --project-id 123 --limit 20 +gitlink-cli pm +sprint-issues --project-id 123 --page 1 --limit 20 + +# 查询周报 Issue 和 PM Issue 标签 +gitlink-cli pm +weekly-issues --project-id 123 +gitlink-cli pm +issue-tags --project-id 123 + +# 查询 PM 流水线和 Action 运行记录 +gitlink-cli pm +pipelines --project-id 123 +gitlink-cli pm +action-runs --project-id 123 +``` + ### 忽略文件模板 ```bash @@ -651,6 +670,7 @@ gitlink-cli/ │ ├── org/ # 组织 shortcuts │ ├── ci/ # CI shortcuts │ ├── pipeline/ # Pipeline shortcuts +│ ├── pm/ # 项目管理 shortcuts │ ├── search/ # 搜索 shortcuts │ ├── user/ # 用户 shortcuts │ └── register.go # 注册入口 diff --git a/doc/changes/pm-shortcuts.md b/doc/changes/pm-shortcuts.md new file mode 100644 index 0000000..aa05e5b --- /dev/null +++ b/doc/changes/pm-shortcuts.md @@ -0,0 +1,60 @@ +# PM shortcuts + +## Background + +The `gitlink-pm` Skill documented GitLink project-management workflows but had +to call Raw API paths under `/pm/...` directly. That made Sprint reports, weekly +issue summaries, board inspection, and PM pipeline checks less discoverable for +users and agents. + +This change adds a first-class `pm` shortcut group for read-only PM data. + +## New shortcuts + +- `pm +dashboards` lists PM dashboards for a project. +- `pm +sprint-issues` lists Sprint issues. +- `pm +weekly-issues` lists weekly issues. +- `pm +issue-tags` lists PM issue tags. +- `pm +pipelines` lists PM pipelines. +- `pm +action-runs` lists PM action run records. + +All commands require `--project-id` and accept `--page`/`--limit`. + +## Examples + +```bash +gitlink-cli pm +dashboards --project-id 123 --limit 20 +gitlink-cli pm +sprint-issues --project-id 123 --page 1 --limit 20 +gitlink-cli pm +weekly-issues --project-id 123 +gitlink-cli pm +issue-tags --project-id 123 +gitlink-cli pm +pipelines --project-id 123 +gitlink-cli pm +action-runs --project-id 123 +``` + +## Documentation updates + +- README and README.zh-CN include PM usage examples. +- `skills/gitlink-pm` now prefers `pm +...` shortcuts instead of Raw API calls. +- The Skills overview lists the new PM commands. + +## Tests + +Unit tests cover: + +- all PM shortcut endpoint mappings; +- `project_id`, `page`, and `limit` query construction; +- default pagination; +- required `--project-id` validation; +- HTTP error propagation. + +Suggested verification: + +```bash +go test ./shortcuts/pm ./shortcuts +``` + +Full project verification: + +```bash +go test ./... +``` diff --git a/internal/i18n/locales/en-US.json b/internal/i18n/locales/en-US.json index 0739395..e420abe 100644 --- a/internal/i18n/locales/en-US.json +++ b/internal/i18n/locales/en-US.json @@ -52,6 +52,13 @@ "cmd.org.list.short": "List organizations", "cmd.org.members.short": "List organization members", "cmd.org.short": "Organization operations", + "cmd.pm.action_runs.short": "List PM action run records", + "cmd.pm.dashboards.short": "List PM dashboards", + "cmd.pm.issue_tags.short": "List PM issue tags", + "cmd.pm.pipelines.short": "List PM pipelines", + "cmd.pm.short": "Project management operations", + "cmd.pm.sprint_issues.short": "List PM sprint issues", + "cmd.pm.weekly_issues.short": "List PM weekly issues", "cmd.pr.close.short": "Close a pull request", "cmd.pr.comment.short": "Add a comment to a pull request", "cmd.pr.create.short": "Create a pull request", @@ -178,6 +185,7 @@ "flag.org.name": "Organization name", "flag.owner": "Repository owner (auto-detected from git remote)", "flag.page": "Page number", + "flag.pm.project_id": "GitLink project ID", "flag.pr.assignee_id": "Assignee user ID", "flag.pr.base": "Target branch", "flag.pr.body": "PR description", diff --git a/internal/i18n/locales/zh-CN.json b/internal/i18n/locales/zh-CN.json index 2e6fc4d..b95f503 100644 --- a/internal/i18n/locales/zh-CN.json +++ b/internal/i18n/locales/zh-CN.json @@ -52,6 +52,13 @@ "cmd.org.list.short": "列出组织", "cmd.org.members.short": "列出组织成员", "cmd.org.short": "组织操作", + "cmd.pm.action_runs.short": "列出 PM Action 运行记录", + "cmd.pm.dashboards.short": "列出 PM 看板", + "cmd.pm.issue_tags.short": "列出 PM Issue 标签", + "cmd.pm.pipelines.short": "列出 PM 流水线", + "cmd.pm.short": "项目管理操作", + "cmd.pm.sprint_issues.short": "列出 PM Sprint Issue", + "cmd.pm.weekly_issues.short": "列出 PM 周报 Issue", "cmd.pr.close.short": "关闭拉取请求", "cmd.pr.comment.short": "给拉取请求添加评论", "cmd.pr.create.short": "创建拉取请求", @@ -178,6 +185,7 @@ "flag.org.name": "组织名称", "flag.owner": "仓库所有者(自动从 git remote 检测)", "flag.page": "页码", + "flag.pm.project_id": "GitLink 项目 ID", "flag.pr.assignee_id": "指派人用户 ID", "flag.pr.base": "目标分支", "flag.pr.body": "PR 描述", diff --git a/shortcuts/pm/pm.go b/shortcuts/pm/pm.go new file mode 100644 index 0000000..4ecbdb1 --- /dev/null +++ b/shortcuts/pm/pm.go @@ -0,0 +1,71 @@ +// Package pm implements GitLink project management shortcuts. +package pm + +import ( + "net/url" + "strings" + + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// Shortcuts returns project management read shortcuts. +func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut { + tr := shortcutTranslator(translators...) + return []*common.Shortcut{ + pmListShortcut(tr, "dashboards", tr.T("cmd.pm.dashboards.short"), "/pm/dashboards"), + pmListShortcut(tr, "sprint-issues", tr.T("cmd.pm.sprint_issues.short"), "/pm/sprint_issues"), + pmListShortcut(tr, "weekly-issues", tr.T("cmd.pm.weekly_issues.short"), "/pm/weekly_issues"), + pmListShortcut(tr, "issue-tags", tr.T("cmd.pm.issue_tags.short"), "/pm/issue_tags"), + pmListShortcut(tr, "pipelines", tr.T("cmd.pm.pipelines.short"), "/pm/pipelines"), + pmListShortcut(tr, "action-runs", tr.T("cmd.pm.action_runs.short"), "/pm/action_runs"), + } +} + +func pmListShortcut(tr *i18n.Translator, name, description, path string) *common.Shortcut { + return &common.Shortcut{ + Name: name, + Description: description, + Flags: []common.Flag{ + {Name: "project-id", Short: "P", Usage: tr.T("flag.pm.project_id"), Required: true}, + {Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"}, + {Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"}, + }, + Run: func(ctx *common.RuntimeContext) error { + projectID, err := ctx.RequireArg("project-id") + if err != nil { + return err + } + q := pmQuery(ctx, projectID) + env, err := ctx.CallAPIWithQuery("GET", path, q) + if err != nil { + return err + } + return ctx.Output(env) + }, + } +} + +func pmQuery(ctx *common.RuntimeContext, projectID string) url.Values { + q := url.Values{} + q.Set("project_id", strings.TrimSpace(projectID)) + q.Set("page", firstNonEmpty(ctx.Arg("page"), "1")) + q.Set("limit", firstNonEmpty(ctx.Arg("limit"), "20")) + return q +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator { + if len(translators) > 0 && translators[0] != nil { + return translators[0] + } + return i18n.Default() +} diff --git a/shortcuts/pm/pm_test.go b/shortcuts/pm/pm_test.go new file mode 100644 index 0000000..dc3fd1d --- /dev/null +++ b/shortcuts/pm/pm_test.go @@ -0,0 +1,133 @@ +package pm + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gitlink-org/gitlink-cli/internal/client" + "github.com/gitlink-org/gitlink-cli/internal/i18n" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error { + t.Helper() + shortcut := findShortcut(t, name) + ctx := &common.RuntimeContext{ + Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL}, + Format: "json", + Args: args, + Tr: i18n.Default(), + } + return shortcut.Run(ctx) +} + +func findShortcut(t *testing.T, name string) *common.Shortcut { + t.Helper() + for _, shortcut := range Shortcuts() { + if shortcut.Name == name { + return shortcut + } + } + t.Fatalf("shortcut %q not found", name) + return nil +} + +func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + t.Fatalf("write JSON: %v", err) + } +} + +func TestPMReadShortcuts(t *testing.T) { + tests := []struct { + name string + path string + }{ + {name: "dashboards", path: "/pm/dashboards.json"}, + {name: "sprint-issues", path: "/pm/sprint_issues.json"}, + {name: "weekly-issues", path: "/pm/weekly_issues.json"}, + {name: "issue-tags", path: "/pm/issue_tags.json"}, + {name: "pipelines", path: "/pm/pipelines.json"}, + {name: "action-runs", path: "/pm/action_runs.json"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Fatalf("method = %s, want GET", r.Method) + } + if r.URL.Path != tt.path { + t.Fatalf("path = %s, want %s", r.URL.Path, tt.path) + } + if got := r.URL.Query().Get("project_id"); got != "123" { + t.Fatalf("project_id = %q, want 123", got) + } + if got := r.URL.Query().Get("page"); got != "2" { + t.Fatalf("page = %q, want 2", got) + } + if got := r.URL.Query().Get("limit"); got != "50" { + t.Fatalf("limit = %q, want 50", got) + } + writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"}) + })) + defer server.Close() + + err := runShortcut(t, server, tt.name, map[string]string{ + "project-id": "123", + "page": "2", + "limit": "50", + }) + if err != nil { + t.Fatalf("%s failed: %v", tt.name, err) + } + }) + } +} + +func TestPMReadShortcutsDefaultPagination(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/pm/dashboards.json" { + t.Fatalf("path = %s, want /pm/dashboards.json", r.URL.Path) + } + if got := r.URL.Query().Get("page"); got != "1" { + t.Fatalf("page = %q, want 1", got) + } + if got := r.URL.Query().Get("limit"); got != "20" { + t.Fatalf("limit = %q, want 20", got) + } + writeJSON(t, w, map[string]interface{}{"status": 0}) + })) + defer server.Close() + + if err := runShortcut(t, server, "dashboards", map[string]string{"project-id": "123"}); err != nil { + t.Fatalf("dashboards failed: %v", err) + } +} + +func TestPMReadShortcutRequiresProjectID(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Fatalf("missing project-id should fail before remote API: %s %s", r.Method, r.URL.Path) + })) + defer server.Close() + + if err := runShortcut(t, server, "dashboards", nil); err == nil { + t.Fatal("expected missing project-id error") + } +} + +func TestPMReadShortcutHTTPError(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("server error")) + })) + defer server.Close() + + if err := runShortcut(t, server, "dashboards", map[string]string{"project-id": "123"}); err == nil { + t.Fatal("expected error for HTTP 500") + } +} diff --git a/shortcuts/register.go b/shortcuts/register.go index 1fedc7e..ffc8ce7 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -18,6 +18,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/milestone" "github.com/gitlink-org/gitlink-cli/shortcuts/org" "github.com/gitlink-org/gitlink-cli/shortcuts/pipeline" + "github.com/gitlink-org/gitlink-cli/shortcuts/pm" "github.com/gitlink-org/gitlink-cli/shortcuts/pr" "github.com/gitlink-org/gitlink-cli/shortcuts/profile" "github.com/gitlink-org/gitlink-cli/shortcuts/release" @@ -43,6 +44,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "member": member.Shortcuts(), "milestone": milestone.Shortcuts(), "pipeline": pipeline.Shortcuts(), + "pm": pm.Shortcuts(tr), "pr": pr.Shortcuts(tr), "profile": profile.Shortcuts(tr), "release": release.Shortcuts(tr), @@ -68,6 +70,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "member": "Repository member operations", "milestone": "Milestone operations", "pipeline": "Pipeline operations", + "pm": tr.T("cmd.pm.short"), "pr": tr.T("cmd.pr.short"), "profile": tr.T("cmd.profile.short"), "release": tr.T("cmd.release.short"), diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 00f4c57..4c14316 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -13,7 +13,7 @@ func TestRegisterAll(t *testing.T) { expectedGroups := []string{ "repo", "issue", "label", "license", "pr", "profile", "release", "branch", "org", "user", "search", "ci", "workflow", - "compare", "member", "milestone", "pipeline", "webhook", + "compare", "member", "milestone", "pipeline", "pm", "webhook", "dataset", "health", "ignore", "wiki", } diff --git a/skills/README.md b/skills/README.md index d507074..9be10f1 100644 --- a/skills/README.md +++ b/skills/README.md @@ -144,7 +144,7 @@ skills/ | **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` | | **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` | | **gitlink-wiki** | Wiki 页面管理 | `wiki +list`, `wiki +view`, `wiki +create`, `wiki +update`, `wiki +delete` | -| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 | +| **gitlink-pm** | 项目管理 | `pm +dashboards`, `pm +sprint-issues`, `pm +weekly-issues`, `pm +action-runs` | | **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes | | **gitlink-health** | 开源项目健康度 | 详情见SKILL.md | diff --git a/skills/gitlink-pm/SKILL.md b/skills/gitlink-pm/SKILL.md index f5333ed..d8707bc 100644 --- a/skills/gitlink-pm/SKILL.md +++ b/skills/gitlink-pm/SKILL.md @@ -1,6 +1,6 @@ --- name: gitlink-pm -version: 1.0.0 +version: 2.0.0 description: "项目管理(PM):Sprint、看板、周报等项目管理功能。当用户需要使用 GitLink PM 功能时触发。" metadata: requires: @@ -10,37 +10,56 @@ metadata: # gitlink-pm(项目管理) -> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) +> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)。 -**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。`gh` 仅适用于 GitHub 平台。** +**CRITICAL — GitLink 操作只能用 `gitlink-cli`。禁止用 `gh`(GitHub CLI)操作 GitLink 资源。** -GitLink PM 模块提供敏捷项目管理能力,目前通过 Raw API 访问。 +GitLink PM 模块提供敏捷项目管理能力。优先使用 `pm` shortcut,不再直接拼接 `/pm/...` Raw API。 -## API 端点 - -> 前缀:`/api/pm` +## 命令 ```bash # 看板 -gitlink-cli api GET /pm/dashboards --query 'project_id=123' +gitlink-cli pm +dashboards --project-id 123 --format json # Sprint Issue 列表 -gitlink-cli api GET /pm/sprint_issues --query 'project_id=123' +gitlink-cli pm +sprint-issues --project-id 123 --page 1 --limit 20 --format json -# 周报 -gitlink-cli api GET /pm/weekly_issues --query 'project_id=123' +# 周报 Issue +gitlink-cli pm +weekly-issues --project-id 123 --format json -# Issue 标签 -gitlink-cli api GET /pm/issue_tags --query 'project_id=123' +# PM Issue 标签 +gitlink-cli pm +issue-tags --project-id 123 --format json -# 流水线 -gitlink-cli api GET /pm/pipelines --query 'project_id=123' +# PM 流水线 +gitlink-cli pm +pipelines --project-id 123 --format json # Action 运行记录 -gitlink-cli api GET /pm/action_runs --query 'project_id=123' +gitlink-cli pm +action-runs --project-id 123 --format json ``` +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--project-id, -P` | 是 | GitLink 数字项目 ID | +| `--page, -p` | 否 | 页码,默认 `1` | +| `--limit, -l` | 否 | 每页数量,默认 `20` | + +## API 映射 + +| Shortcut | API | +|----------|-----| +| `pm +dashboards` | `GET /pm/dashboards` | +| `pm +sprint-issues` | `GET /pm/sprint_issues` | +| `pm +weekly-issues` | `GET /pm/weekly_issues` | +| `pm +issue-tags` | `GET /pm/issue_tags` | +| `pm +pipelines` | `GET /pm/pipelines` | +| `pm +action-runs` | `GET /pm/action_runs` | + ## 注意事项 -- PM 接口需要项目 ID(`project_id`),可通过 `repo +info` 获取 -- PM 功能需要项目开启 PM 模块 +- PM 接口需要项目 ID(`project_id`),可通过 `repo +info` 获取。 +- PM 功能需要项目开启 PM 模块。 +- 这些命令均为只读查询,适合 Agent 生成 Sprint 报告、看板摘要和项目管理周报。 +- 建议在 Agent 场景始终使用 `--format json`。