Merge pull request 'feat(workflow): replace raw api calls with shortcut-backed fetches' (#324) from ohanabi/gitlink-cli:feat/workflow-shortcut-backed-fetches into master

This commit is contained in:
wbtiger 2026-07-14 22:29:15 +08:00
commit cdf41dff07
13 changed files with 672 additions and 58 deletions

View File

@ -679,6 +679,7 @@ gitlink-cli profile +contribution --user zhangsan --year 2025
- `workflow +triage`
- `workflow +health`
- `workflow +pr-summary`
- `workflow +review-context`
- `workflow +repo-report`
`workflow +pr-summary` defaults to `table` when `--format` is omitted.
@ -747,6 +748,9 @@ gitlink-cli workflow +health --owner Gitlink --repo gitlink-cli --stale-days 30
# PR review summary by read-only GitLink fetch
gitlink-cli workflow +pr-summary --owner Gitlink --repo gitlink-cli --number 1 --format markdown
# PR review context bundle by read-only GitLink fetch
gitlink-cli workflow +review-context --owner Gitlink --repo gitlink-cli --number 1 --format json
# PR review summary from a local JSON file
gitlink-cli workflow +pr-summary --from shortcuts/workflow/testdata/pr_summary.json --format json
@ -769,6 +773,7 @@ Safety:
- They do not modify remote GitLink data.
- They do not depend on LLM APIs.
- `workflow +pr-summary` does not comment, approve, reject, or merge pull requests.
- `workflow +review-context` bundles repository, PR, file, review, issue, and label context without remote writes.
- `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes.
### Dataset

View File

@ -525,6 +525,21 @@ gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
```
### 工作流 Agent 命令
```bash
# 只读获取 PR 审查上下文包仓库、PR、文件、Review、Issue、标签
gitlink-cli workflow +review-context --owner Gitlink --repo forgeplus --number 42 --format json
# 生成 PR 审查摘要
gitlink-cli workflow +pr-summary --owner Gitlink --repo forgeplus --number 42 --format markdown
# 生成仓库工作流报告
gitlink-cli workflow +repo-report --owner Gitlink --repo forgeplus --format markdown
```
> `workflow +review-context` 只读取 GitLink 数据,不会评论、审批、拒绝、合并或修改标签。
### 发布管理
```bash

View File

@ -0,0 +1,46 @@
# Workflow Shortcut-Backed Fetches
## Summary
Adds `workflow +review-context`, a read-only workflow command that returns a deterministic PR review context bundle for AI agents and maintainers.
```bash
gitlink-cli workflow +review-context --owner Gitlink --repo gitlink-cli --number 1 --format json
```
## Behavior
The command aggregates shortcut-backed read-only fetches into one structured output:
- Repository info, equivalent to `repo +info`
- Pull request details, equivalent to `pr +view`
- Changed files, equivalent to `pr +files`
- Existing reviews, equivalent to `pr +reviews`
- Open issue context, equivalent to `issue +list --state open`
- Issue labels, equivalent to `label +list`
Each optional section can be disabled with `--include-...=false`, and `--issue-limit` / `--label-limit` bound the amount of context returned. Partial section failures are recorded in `notes` so an Agent can continue with the available data.
## Why
Code review and gatekeeper Skills previously instructed Agents to stitch together multiple raw API calls or separate shortcut calls before analysis. A single read-only workflow command makes review context deterministic, easier to test, and safer for Agent usage.
## Safety
- The command is read-only.
- It does not comment, approve, reject, merge, label, close, or modify remote resources.
- Write operations such as PR review submission remain explicit commands and still require user confirmation.
## Documentation
- Updates README examples in English and Chinese.
- Updates `gitlink-workflow` with `workflow +review-context`.
- Updates code review and gatekeeper Skills to prefer the workflow context bundle.
- Updates insight guidance to use shortcut-covered repo and user fetches before Raw API.
## Verification
```bash
go test ./shortcuts/workflow ./shortcuts
go test ./...
```

View File

@ -104,7 +104,7 @@ func apiList(data interface{}) []interface{} {
case []interface{}:
return v
case map[string]interface{}:
for _, key := range []string{"issues", "pulls", "pull_requests", "files", "commits", "releases", "builds", "items", "records", "data"} {
for _, key := range []string{"issues", "pulls", "pull_requests", "files", "commits", "reviews", "issue_tags", "labels", "releases", "builds", "items", "records", "data"} {
if raw, ok := v[key]; ok {
if items := apiList(raw); len(items) > 0 {
return items

View File

@ -0,0 +1,297 @@
package workflow
import (
"fmt"
"net/url"
"os"
"strings"
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
type ReviewContextOptions struct {
Owner string
Repo string
Number int
IssueLimit int
LabelLimit int
IncludeRepo bool
IncludePR bool
IncludeFiles bool
IncludeReviews bool
IncludeIssues bool
IncludeLabels bool
}
type ReviewContext struct {
Repository string `json:"repository"`
PullRequest int `json:"pull_request"`
Source string `json:"source"`
Sections []string `json:"sections"`
RepositoryInfo map[string]interface{} `json:"repository_info,omitempty"`
PR map[string]interface{} `json:"pr,omitempty"`
Files []map[string]interface{} `json:"files,omitempty"`
Reviews []map[string]interface{} `json:"reviews,omitempty"`
OpenIssues []map[string]interface{} `json:"open_issues,omitempty"`
Labels []map[string]interface{} `json:"labels,omitempty"`
Notes []ScoringNote `json:"notes,omitempty"`
}
func newReviewContextShortcut() *common.Shortcut {
return &common.Shortcut{
Name: "review-context",
Description: "Fetch read-only PR review context from shortcut-backed endpoints",
Flags: []common.Flag{
{Name: "number", Short: "n", Usage: "Pull request number", Required: true},
{Name: "issue-limit", Usage: "Maximum open issues to include", Default: "20"},
{Name: "label-limit", Usage: "Maximum labels to include", Default: "50"},
{Name: "include-repo", Usage: "Include repository info", Bool: true, Default: "true"},
{Name: "include-pr", Usage: "Include pull request details", Bool: true, Default: "true"},
{Name: "include-files", Usage: "Include pull request changed files", Bool: true, Default: "true"},
{Name: "include-reviews", Usage: "Include pull request reviews", Bool: true, Default: "true"},
{Name: "include-issues", Usage: "Include open issue context", Bool: true, Default: "true"},
{Name: "include-labels", Usage: "Include issue labels", Bool: true, Default: "true"},
},
Run: runReviewContext,
}
}
func runReviewContext(ctx *common.RuntimeContext) error {
number, err := parseIntArg(ctx.Arg("number"), 0, "number")
if err != nil {
return err
}
if number <= 0 {
return fmt.Errorf("workflow +review-context requires --number with --owner and --repo for read-only fetch")
}
issueLimit, err := parseIntArg(ctx.Arg("issue-limit"), 20, "issue-limit")
if err != nil {
return err
}
labelLimit, err := parseIntArg(ctx.Arg("label-limit"), 50, "label-limit")
if err != nil {
return err
}
context, err := FetchReviewContext(ctx, ReviewContextOptions{
Number: number,
IssueLimit: issueLimit,
LabelLimit: labelLimit,
IncludeRepo: parseBoolDefault(ctx.Arg("include-repo"), true),
IncludePR: parseBoolDefault(ctx.Arg("include-pr"), true),
IncludeFiles: parseBoolDefault(ctx.Arg("include-files"), true),
IncludeReviews: parseBoolDefault(ctx.Arg("include-reviews"), true),
IncludeIssues: parseBoolDefault(ctx.Arg("include-issues"), true),
IncludeLabels: parseBoolDefault(ctx.Arg("include-labels"), true),
})
if err != nil {
return err
}
format := ctx.Format
if strings.TrimSpace(cmdutil.Format) == "" {
format = "json"
}
rendered, err := RenderReviewContext(context, format)
if err != nil {
return err
}
_, err = fmt.Fprint(os.Stdout, rendered)
return err
}
func FetchReviewContext(ctx *common.RuntimeContext, opts ReviewContextOptions) (ReviewContext, error) {
owner, repo, err := resolveFetchRepo(ctx, opts.Owner, opts.Repo)
if err != nil {
return ReviewContext{}, fmt.Errorf("workflow +review-context remote mode requires --owner and --repo or a Git remote: %w", err)
}
if opts.Number <= 0 {
return ReviewContext{}, fmt.Errorf("pull request number is required")
}
if opts.IssueLimit <= 0 {
opts.IssueLimit = 20
}
if opts.LabelLimit <= 0 {
opts.LabelLimit = 50
}
result := ReviewContext{
Repository: fmt.Sprintf("%s/%s", owner, repo),
PullRequest: opts.Number,
Source: "shortcut-backed-read-only-fetch",
Sections: []string{},
Notes: []ScoringNote{},
}
successes := 0
if opts.IncludeRepo {
if info, err := fetchRepoInfo(ctx, owner, repo); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "repo_info", Note: fmt.Sprintf("repo +info equivalent failed: %v", err)})
} else {
result.RepositoryInfo = info
result.Sections = append(result.Sections, "repo_info")
successes++
}
}
if opts.IncludePR {
if pr, err := fetchReviewContextPR(ctx, owner, repo, opts.Number); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "pr_view", Note: fmt.Sprintf("pr +view equivalent failed: %v", err)})
} else {
result.PR = pr
result.Sections = append(result.Sections, "pr")
successes++
}
}
if opts.IncludeFiles {
if files, err := fetchReviewContextList(ctx, fmt.Sprintf("%s/pulls/%d/files", plainRepoPath(owner, repo), opts.Number), nil, 100); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "pr_files", Note: fmt.Sprintf("pr +files equivalent failed: %v", err)})
} else {
result.Files = files
result.Sections = append(result.Sections, "files")
successes++
}
}
if opts.IncludeReviews {
if reviews, err := fetchReviewContextList(ctx, fmt.Sprintf("%s/pulls/%d/reviews", workflowRepoPath(owner, repo), opts.Number), nil, 100); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "pr_reviews", Note: fmt.Sprintf("pr +reviews equivalent failed: %v", err)})
} else {
result.Reviews = reviews
result.Sections = append(result.Sections, "reviews")
successes++
}
}
if opts.IncludeIssues {
query := url.Values{}
query.Set("category", "opened")
if issues, err := fetchReviewContextList(ctx, workflowRepoPath(owner, repo)+"/issues", query, opts.IssueLimit); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "open_issues", Note: fmt.Sprintf("issue +list equivalent failed: %v", err)})
} else {
result.OpenIssues = issues
result.Sections = append(result.Sections, "open_issues")
successes++
}
}
if opts.IncludeLabels {
if labels, err := fetchReviewContextList(ctx, workflowRepoPath(owner, repo)+"/issue_tags", nil, opts.LabelLimit); err != nil {
result.Notes = append(result.Notes, ScoringNote{Metric: "labels", Note: fmt.Sprintf("label +list equivalent failed: %v", err)})
} else {
result.Labels = labels
result.Sections = append(result.Sections, "labels")
successes++
}
}
result.Notes = uniqueScoringNotes(result.Notes)
if successes == 0 {
return ReviewContext{}, fmt.Errorf("fetch review context: all enabled sections failed")
}
return result, nil
}
func fetchReviewContextPR(ctx *common.RuntimeContext, owner, repo string, number int) (map[string]interface{}, error) {
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pulls/%d", plainRepoPath(owner, repo), number), nil)
if err != nil {
return nil, err
}
item := prAPIObject(env.Data)
if item == nil {
return nil, fmt.Errorf("PR response did not contain an object")
}
return item, nil
}
func fetchReviewContextList(ctx *common.RuntimeContext, path string, query url.Values, limit int) ([]map[string]interface{}, error) {
if limit <= 0 {
limit = 100
}
q := cloneValues(query)
q.Set("page", "1")
q.Set("limit", fmt.Sprintf("%d", limit))
env, err := ctx.CallAPIWithQuery("GET", path, q)
if err != nil {
return nil, err
}
items := apiList(env.Data)
out := make([]map[string]interface{}, 0, len(items))
for _, raw := range items {
item, ok := raw.(map[string]interface{})
if !ok {
continue
}
out = append(out, item)
if len(out) >= limit {
break
}
}
return out, nil
}
func RenderReviewContext(context ReviewContext, format string) (string, error) {
var rendered strings.Builder
switch normalizeFormat(format) {
case "json":
if err := writeJSON(&rendered, context); err != nil {
return "", err
}
case "markdown":
if err := writeReviewContextMarkdown(&rendered, context); err != nil {
return "", err
}
case "table":
if err := writeReviewContextTable(&rendered, context); err != nil {
return "", err
}
default:
return "", fmt.Errorf("unsupported workflow output format %q", format)
}
return rendered.String(), nil
}
func writeReviewContextMarkdown(w *strings.Builder, context ReviewContext) error {
_, _ = fmt.Fprintf(w, "# PR Review Context\n\n")
_, _ = fmt.Fprintf(w, "- Repository: `%s`\n", context.Repository)
_, _ = fmt.Fprintf(w, "- Pull request: `#%d`\n", context.PullRequest)
_, _ = fmt.Fprintf(w, "- Source: `%s`\n", context.Source)
_, _ = fmt.Fprintf(w, "- Sections: `%s`\n", strings.Join(context.Sections, ", "))
_, _ = fmt.Fprintf(w, "\n## Summary\n\n")
_, _ = fmt.Fprintf(w, "- Changed files: `%d`\n", len(context.Files))
_, _ = fmt.Fprintf(w, "- Reviews: `%d`\n", len(context.Reviews))
_, _ = fmt.Fprintf(w, "- Open issues included: `%d`\n", len(context.OpenIssues))
_, _ = fmt.Fprintf(w, "- Labels included: `%d`\n", len(context.Labels))
if len(context.Notes) > 0 {
_, _ = fmt.Fprintf(w, "\n## Notes\n\n")
for _, note := range context.Notes {
_, _ = fmt.Fprintf(w, "- `%s`: %s\n", note.Metric, note.Note)
}
}
return nil
}
func writeReviewContextTable(w *strings.Builder, context ReviewContext) error {
_, _ = fmt.Fprintf(w, "REPOSITORY\tPR\tSECTIONS\tFILES\tREVIEWS\tISSUES\tLABELS\tNOTES\n")
_, _ = fmt.Fprintf(w, "%s\t#%d\t%d\t%d\t%d\t%d\t%d\t%d\n",
context.Repository,
context.PullRequest,
len(context.Sections),
len(context.Files),
len(context.Reviews),
len(context.OpenIssues),
len(context.Labels),
len(context.Notes),
)
return nil
}
func plainRepoPath(owner, repo string) string {
return fmt.Sprintf("/%s/%s", strings.TrimSpace(owner), strings.TrimSpace(repo))
}
func parseBoolDefault(value string, defaultValue bool) bool {
value = strings.TrimSpace(value)
if value == "" {
return defaultValue
}
return parseBoolArg(value)
}

View File

@ -0,0 +1,238 @@
package workflow
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func TestFetchReviewContextAllSections(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo.json":
writeWorkflowJSON(t, w, map[string]interface{}{"name": "repo", "default_branch": "master"})
case r.Method == "GET" && r.URL.Path == "/owner/repo/pulls/7.json":
writeWorkflowJSON(t, w, map[string]interface{}{
"pull_request": map[string]interface{}{
"number": 7,
"title": "feat: add workflow context",
"user": map[string]interface{}{"login": "alice"},
},
})
case r.Method == "GET" && r.URL.Path == "/owner/repo/pulls/7/files.json":
writeWorkflowJSON(t, w, map[string]interface{}{
"files": []map[string]interface{}{
{"filename": "shortcuts/workflow/review_context.go", "additions": 120},
},
})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/pulls/7/reviews.json":
writeWorkflowJSON(t, w, map[string]interface{}{
"reviews": []map[string]interface{}{
{"id": 1, "status": "approved", "user": map[string]interface{}{"login": "reviewer"}},
},
})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issues.json":
if got := r.URL.Query().Get("category"); got != "opened" {
t.Fatalf("issue category = %q, want opened", got)
}
if got := r.URL.Query().Get("limit"); got != "2" {
t.Fatalf("issue limit = %q, want 2", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{
"issues": []map[string]interface{}{
{"number": 1, "title": "bug: install fails"},
},
})
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/issue_tags.json":
if got := r.URL.Query().Get("limit"); got != "3" {
t.Fatalf("label limit = %q, want 3", got)
}
writeWorkflowJSON(t, w, map[string]interface{}{
"issue_tags": []map[string]interface{}{
{"id": 2, "name": "bug"},
},
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
}
}))
defer server.Close()
ctx := workflowTestContext(server)
got, err := FetchReviewContext(ctx, ReviewContextOptions{
Number: 7,
IssueLimit: 2,
LabelLimit: 3,
IncludeRepo: true,
IncludePR: true,
IncludeFiles: true,
IncludeReviews: true,
IncludeIssues: true,
IncludeLabels: true,
})
if err != nil {
t.Fatalf("FetchReviewContext returned error: %v", err)
}
if got.Repository != "owner/repo" || got.PullRequest != 7 {
t.Fatalf("got repository=%q pr=%d", got.Repository, got.PullRequest)
}
if len(got.Sections) != 6 {
t.Fatalf("sections = %v, want 6 sections", got.Sections)
}
if len(got.Files) != 1 || len(got.Reviews) != 1 || len(got.OpenIssues) != 1 || len(got.Labels) != 1 {
t.Fatalf("context lists not populated: files=%d reviews=%d issues=%d labels=%d", len(got.Files), len(got.Reviews), len(got.OpenIssues), len(got.Labels))
}
if len(got.Notes) != 0 {
t.Fatalf("notes = %+v, want empty", got.Notes)
}
}
func TestFetchReviewContextPartialFailureKeepsNotes(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/owner/repo.json":
writeWorkflowJSON(t, w, map[string]interface{}{"name": "repo"})
case "/owner/repo/pulls/9/files.json":
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("files unavailable"))
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
}
}))
defer server.Close()
got, err := FetchReviewContext(workflowTestContext(server), ReviewContextOptions{
Number: 9,
IncludeRepo: true,
IncludeFiles: true,
})
if err != nil {
t.Fatalf("FetchReviewContext returned error: %v", err)
}
if got.RepositoryInfo == nil {
t.Fatal("expected repository info to be populated")
}
if len(got.Notes) != 1 || got.Notes[0].Metric != "pr_files" {
t.Fatalf("notes = %+v, want one pr_files note", got.Notes)
}
}
func TestFetchReviewContextAllSectionsFail(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()
_, err := FetchReviewContext(workflowTestContext(server), ReviewContextOptions{
Number: 9,
IncludeRepo: true,
})
if err == nil {
t.Fatal("expected error when all enabled sections fail")
}
}
func TestReviewContextShortcutRemoteFetchJSON(t *testing.T) {
restoreFormat := setCommandFormatForTest(t, "json")
defer restoreFormat()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/owner/repo.json":
writeWorkflowJSON(t, w, map[string]interface{}{"name": "repo"})
case "/owner/repo/pulls/3.json":
writeWorkflowJSON(t, w, map[string]interface{}{"number": 3, "title": "fix: bug"})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.String())
}
}))
defer server.Close()
ctx := &common.RuntimeContext{
Client: &client.Client{
HTTP: server.Client(),
BaseURL: server.URL,
},
Owner: "owner",
Repo: "repo",
Format: "json",
Args: map[string]string{
"number": "3",
"issue-limit": "20",
"label-limit": "50",
"include-repo": "true",
"include-pr": "true",
"include-files": "false",
"include-reviews": "false",
"include-issues": "false",
"include-labels": "false",
},
}
output := captureStdout(t, func() error {
return findWorkflowShortcut(t, "review-context").Run(ctx)
})
var result ReviewContext
if err := json.Unmarshal([]byte(output), &result); err != nil {
t.Fatalf("json.Unmarshal returned error: %v\noutput=%s", err, output)
}
if result.PullRequest != 3 || result.Repository != "owner/repo" {
t.Fatalf("result = %+v, want PR 3 owner/repo", result)
}
if len(result.Sections) != 2 {
t.Fatalf("sections = %v, want repo and pr", result.Sections)
}
}
func TestReviewContextShortcutMissingNumber(t *testing.T) {
ctx := &common.RuntimeContext{Args: map[string]string{}}
err := findWorkflowShortcut(t, "review-context").Run(ctx)
if err == nil {
t.Fatal("expected error for missing number")
}
if !strings.Contains(err.Error(), "requires --number") {
t.Fatalf("error = %v, want missing number hint", err)
}
}
func TestRenderReviewContextFormats(t *testing.T) {
context := ReviewContext{
Repository: "owner/repo",
PullRequest: 4,
Source: "shortcut-backed-read-only-fetch",
Sections: []string{"repo_info", "pr"},
Files: []map[string]interface{}{{"filename": "README.md"}},
Notes: []ScoringNote{{Metric: "labels", Note: "label +list equivalent failed"}},
}
table, err := RenderReviewContext(context, "table")
if err != nil {
t.Fatalf("RenderReviewContext table returned error: %v", err)
}
if !strings.Contains(table, "REPOSITORY") || !strings.Contains(table, "owner/repo") {
t.Fatalf("table output = %q", table)
}
markdown, err := RenderReviewContext(context, "markdown")
if err != nil {
t.Fatalf("RenderReviewContext markdown returned error: %v", err)
}
if !strings.Contains(markdown, "# PR Review Context") || !strings.Contains(markdown, "label +list") {
t.Fatalf("markdown output = %q", markdown)
}
}
func TestParseBoolDefault(t *testing.T) {
if !parseBoolDefault("", true) {
t.Fatal("empty value should use true default")
}
if parseBoolDefault("false", true) {
t.Fatal("false value should override true default")
}
}

View File

@ -25,6 +25,7 @@ func Shortcuts() []*common.Shortcut {
newHealthShortcut(),
newPRSummaryShortcut(),
newRepoReportShortcut(),
newReviewContextShortcut(),
}
}

View File

@ -28,6 +28,9 @@ func TestShortcutsExposesWorkflowCommands(t *testing.T) {
if !names["pr-summary"] {
t.Fatal("Shortcuts missing pr-summary")
}
if !names["review-context"] {
t.Fatal("Shortcuts missing review-context")
}
if !names["repo-report"] {
t.Fatal("Shortcuts missing repo-report")
}

View File

@ -145,7 +145,7 @@ skills/
| **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-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
| **gitlink-workflow** | AI 工作流 | `workflow +triage`, `workflow +pr-summary`, `workflow +review-context`, `workflow +repo-report` |
| **gitlink-health** | 开源项目健康度 | 详情见SKILL.md |
| **gitlink-semantic-audit** | CLI/平台语义审计(伪成功、参数错配、端点失效体检) | `api GET/POST ...` 与 shortcut 对照 |

View File

@ -17,7 +17,7 @@ metadata:
## 工作流概览
本 Skill 提供一套完整的 AI 驱动代码审查工作流,覆盖从获取 PR 变更到生成审查报告的全过程。不需要额外的 CLI Shortcuts——现有 `gitlink-cli` 命令 + AI Agent 的分析能力即可完成
本 Skill 提供一套完整的 AI 驱动代码审查工作流,覆盖从获取 PR 变更到生成审查报告的全过程。优先使用 `workflow +review-context` 获取结构化上下文,避免手工拼接多个 Raw API 请求
| 阶段 | 操作 | AI Agent 角色 |
|------|------|--------------|
@ -38,14 +38,11 @@ metadata:
#### Step 1获取 PR 上下文
```bash
# 获取 PR 详情
gitlink-cli pr +view --id <pr_id> --format json
# 一次性获取仓库、PR、变更文件、已有 Review、开放 Issue 和标签上下文
gitlink-cli workflow +review-context --owner <owner> --repo <repo> --number <pr_id> --format json
# 获取变更文件列表
gitlink-cli pr +files --id <pr_id> --format json
# 获取 Diff 内容(含变更行号和代码上下文)
gitlink-cli pr +diff --id <pr_id> --format json
# 如只需要规则化审查摘要
gitlink-cli workflow +pr-summary --owner <owner> --repo <repo> --number <pr_id> --format markdown
```
#### Step 2逐文件分析
@ -163,18 +160,17 @@ gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
# 2. 获取仓库文件列表(遍历关键目录)
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=src&ref=master'
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=tests&ref=master'
gitlink-cli repo +tree --owner <owner> --repo <repo> --path src --ref master --format json
gitlink-cli repo +tree --owner <owner> --repo <repo> --path tests --ref master --format json
# 3. 获取关键文件内容
gitlink-cli api GET /:owner/:repo/raw/master/README.md
gitlink-cli api GET /:owner/:repo/raw/master/.gitignore
gitlink-cli api GET /:owner/:repo/raw/master/.eslintrc.js # 或类似配置
gitlink-cli api GET /:owner/:repo/raw/master/package.json # 或 go.mod, Cargo.toml
gitlink-cli repo +raw --owner <owner> --repo <repo> --path README.md --ref master --format json
gitlink-cli repo +raw --owner <owner> --repo <repo> --path .gitignore --ref master --format json
gitlink-cli repo +manifest --owner <owner> --repo <repo> --kind node --ref master --format json
# 4. 获取语言统计和贡献者
gitlink-cli api GET /:owner/:repo/languages
gitlink-cli api GET /:owner/:repo/contributors
gitlink-cli repo +languages --owner <owner> --repo <repo> --format json
gitlink-cli repo +contributors --owner <owner> --repo <repo> --format json
```
**健康度检查清单:**
@ -254,34 +250,28 @@ gitlink-cli api POST /:owner/:repo/issues/:id --body '{
---
## Raw API 参考
## Shortcut 参考
代码审查相关的 GitLink API 端点:
```bash
# 获取 PR 详情
gitlink-cli api GET /:owner/:repo/pulls/:id --format json
# 获取 PR 审查上下文包
gitlink-cli workflow +review-context --owner <owner> --repo <repo> --number <id> --format json
# 获取 PR 变更文件列表
gitlink-cli api GET /:owner/:repo/pulls/:id/files --format json
# 获取规则化 PR 审查摘要
gitlink-cli workflow +pr-summary --owner <owner> --repo <repo> --number <id> --format markdown
# 获取 PR Diff
gitlink-cli api GET /:owner/:repo/pulls/:id/diff --format json
# 提交 PR Review
gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{"body":"...","event":"COMMENT"}'
# 提交 PR Review写操作前必须确认用户意图可先 dry-run
gitlink-cli pr +review --owner <owner> --repo <repo> -i <id> --status common -c "..." --dry-run
# 获取仓库文件列表
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=<path>&ref=<branch>'
gitlink-cli repo +tree --owner <owner> --repo <repo> --path <path> --ref <branch> --format json
# 获取仓库语言统计
gitlink-cli api GET /:owner/:repo/languages --format json
gitlink-cli repo +languages --owner <owner> --repo <repo> --format json
# 获取贡献者列表
gitlink-cli api GET /:owner/:repo/contributors --format json
# 获取仓库动态
gitlink-cli api GET /:owner/:repo/activity --format json
gitlink-cli repo +contributors --owner <owner> --repo <repo> --format json
```
## 代码审查最佳实践

View File

@ -96,20 +96,20 @@ behavior:
| 步骤 | 数据 | 命令 |
|------|------|------|
| 聚合上下文 | 仓库、PR、文件、Review、Issue、标签 | `gitlink-cli workflow +review-context --number <id> --format json` |
| PR 元信息 | 标题/描述/作者/关联 issue | `gitlink-cli pr +view -i <id> --format json` |
| 变更文件 | 文件路径列表 | `gitlink-cli pr +files -i <id> --format json` |
| Diff | 变更内容供 AI 审查 | `gitlink-cli pr +diff -i <id> --format json` |
| commits | commit 列表(消息供 commit_quality | `gitlink-cli api GET /:owner/:repo/pulls/:id/commits --format json` |
| CI 状态 | 构建结果 | `gitlink-cli ci +builds --format json` |
> 实测注意:`pr +files` 与 `pr +diff` 底层都打 `/pulls/:id/files`——`+files` 取路径列表,`+diff` 取含 patch 的同一份数据,按需取用即可。无 `pr +commits` 快捷命令commit 列表只能走 Raw API。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
> 实测注意:优先使用 `workflow +review-context` 获取聚合上下文;如需更细的 diff 再补充 `pr +diff`。CI 通过/失败需从 builds 返回的 `status` 字段判断;**无 build 记录时按「CI 未知」处理**(见 §3.5)。
```bash
PR=42
gitlink-cli workflow +review-context --number "$PR" --format json
gitlink-cli pr +view -i "$PR" --format json # title / body / 关联 issue
gitlink-cli pr +files -i "$PR" --format json # changed files
gitlink-cli pr +diff -i "$PR" --format json # diff供 AI 审查)
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli ci +builds --format json
```
@ -295,7 +295,7 @@ PR=42
gitlink-cli pr +view -i "$PR" --format json
gitlink-cli pr +files -i "$PR" --format json
gitlink-cli pr +diff -i "$PR" --format json
gitlink-cli api GET /:owner/:repo/pulls/$PR/commits --format json
gitlink-cli workflow +review-context --number "$PR" --format json
gitlink-cli ci +builds --format json
# → AI 产出发现 → 按 §4 评分 → §5 硬门禁 → §6 裁决 → §7 渲染评分卡
# → dry-run仅把评分卡打印给用户结尾提示「如需回写到 PR请加 --apply」

View File

@ -49,13 +49,13 @@ gitlink-cli pr +list --state open --format json
gitlink-cli pr +list --state merged --format json
# 4. 获取仓库文件结构检查文档、CI 配置)
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=&ref=master'
gitlink-cli repo +tree --owner <owner> --repo <repo> --ref master --format json
# 5. 获取语言统计
gitlink-cli api GET /:owner/:repo/languages --format json
gitlink-cli repo +languages --owner <owner> --repo <repo> --format json
# 6. 获取贡献者列表
gitlink-cli api GET /:owner/:repo/contributors --format json
gitlink-cli repo +contributors --owner <owner> --repo <repo> --format json
```
### 分析指标
@ -172,7 +172,7 @@ gitlink-cli api GET /:owner/:repo/activity --format json
```bash
# 1. 获取贡献者列表
gitlink-cli api GET /:owner/:repo/contributors --format json
gitlink-cli repo +contributors --owner <owner> --repo <repo> --format json
# 2. 获取每个贡献者的 PR
# 通过 PR 列表按 author 过滤
@ -249,29 +249,29 @@ gitlink-cli issue +list --state open --format json
---
## Raw API 参考
## Shortcut 与 Raw API 参考
```bash
# 仓库信息
gitlink-cli api GET /:owner/:repo --format json
gitlink-cli repo +info --owner <owner> --repo <repo> --format json
# 仓库语言统计
gitlink-cli api GET /:owner/:repo/languages --format json
gitlink-cli repo +languages --owner <owner> --repo <repo> --format json
# 贡献者列表
gitlink-cli api GET /:owner/:repo/contributors --format json
gitlink-cli repo +contributors --owner <owner> --repo <repo> --format json
# 仓库动态
# 仓库动态(当前未覆盖为 shortcut保留 Raw API
gitlink-cli api GET /:owner/:repo/activity --format json
# 文件列表(检查文档/配置完整性)
gitlink-cli api GET /:owner/:repo/sub_entries --query 'filepath=&ref=master'
gitlink-cli repo +tree --owner <owner> --repo <repo> --ref master --format json
# 获取用户信息
gitlink-cli api GET /users/:user_id --format json
gitlink-cli user +info --login <user_login> --format json
# 用户贡献热力图
gitlink-cli api GET /users/:user_id/headmaps --format json
gitlink-cli user +heatmap --user <user_login> --format json
```
## 注意事项

View File

@ -44,13 +44,10 @@ gitlink-cli api POST /:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>
# 1. 获取 PR 详情
gitlink-cli pr +view --id <pr_id> --format json
# 2. 获取变更文件列表
gitlink-cli pr +files --id <pr_id> --format json
# 2. 获取完整审查上下文仓库、PR、变更文件、Review、Issue、标签
gitlink-cli workflow +review-context --number <pr_id> --format json
# 3. 获取 PR 提交列表
gitlink-cli pr +diff --id <pr_id> --format json
# 4. 添加 Review 评论
# 3. 添加 Review 评论(写操作前需确认用户意图)
gitlink-cli api POST /:owner/:repo/pulls/:id/reviews --body '{"body":"代码审查意见...","event":"COMMENT"}'
```
@ -97,8 +94,8 @@ gitlink-cli issue +list --state closed --format json
gitlink-cli pr +list --state open --format json
gitlink-cli pr +list --state merged --format json
# 3. 获取项目动态
gitlink-cli api GET /:owner/:repo/activity --format json
# 3. 获取仓库工作流报告
gitlink-cli workflow +repo-report --format json
```
## Workflow: PR Summary (Read-only)
@ -119,6 +116,28 @@ Rules:
- This command is read-only: it does not comment, approve, reject, merge, label, or close pull requests.
- Do not use LLM APIs for this workflow; it is rule-based and explainable.
## Workflow: Review Context (Read-only)
Use `workflow +review-context` when an Agent needs one deterministic JSON bundle for PR review or gatekeeping. It aggregates shortcut-backed read-only fetches for repository info, PR details, changed files, existing reviews, open issues, and labels.
```bash
gitlink-cli workflow +review-context --owner Gitlink --repo gitlink-cli --number 1 --format json
# Trim context for large repositories
gitlink-cli workflow +review-context --owner Gitlink --repo gitlink-cli --number 1 \
--issue-limit 10 --label-limit 30 --format json
# Only fetch PR and changed files
gitlink-cli workflow +review-context --owner Gitlink --repo gitlink-cli --number 1 \
--include-repo=false --include-reviews=false --include-issues=false --include-labels=false \
--format json
```
Rules:
- This command is read-only and never comments, approves, rejects, merges, labels, or closes resources.
- Prefer it before `workflow +pr-summary` when a review agent needs raw context plus existing review state.
- The command records partial fetch failures in `notes` so Agents can proceed with available context.
## Workflow: Repo Report (Read-only)
Use `workflow +repo-report` when a maintainer or Agent needs a single repository workflow report