feat(pm): add project management shortcuts

This commit is contained in:
NeeNe 2026-07-02 13:10:51 +08:00
parent 71ca2bb683
commit 132151d68e
11 changed files with 363 additions and 20 deletions

View File

@ -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

View File

@ -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 # 注册入口

View File

@ -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 ./...
```

View File

@ -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",

View File

@ -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 描述",

71
shortcuts/pm/pm.go Normal file
View File

@ -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()
}

133
shortcuts/pm/pm_test.go Normal file
View File

@ -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")
}
}

View File

@ -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"),

View File

@ -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",
}

View File

@ -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 |

View File

@ -1,6 +1,6 @@
---
name: gitlink-pm
version: 1.0.0
version: 2.0.0
description: "项目管理PMSprint、看板、周报等项目管理功能。当用户需要使用 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`