feat(pipeline): add OpenAPI shortcuts
This commit is contained in:
parent
2f8b987c2c
commit
41a2666c5b
27
README.md
27
README.md
|
|
@ -28,7 +28,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
## Why gitlink-cli?
|
||||
|
||||
- **Agent-Native Design** — Structured [Skills](./skills/) out of the box, compatible with Claude Code, OpenClaw, and other AI platforms — Agents can operate GitLink with zero extra setup
|
||||
- **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Org, Search, and User workflows are covered by high-level commands
|
||||
- **Wide Coverage** — Repository, Issue, PR, Webhook, Member, Branch, Release, CI, Pipeline, Org, Search, and User workflows are covered by high-level commands
|
||||
- **AI-Friendly & Optimized** — Every command is tested with real Agents, featuring concise parameters, smart defaults, and structured output
|
||||
- **Cross-Platform** — Runs on macOS, Linux, and Windows (x64/arm64), install via `npm install -g @gitlink-ai/cli` in one command, binary auto-downloaded
|
||||
- **Open Source, Zero Barriers** — MulanPSL-2.0 license, ready to use, just `npm install`
|
||||
|
|
@ -48,6 +48,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| 🏷️ Release | Create, view, delete releases |
|
||||
| 🏢 Org | Manage organizations, members, teams |
|
||||
| 🔧 CI | View builds, logs, CI/CD operations |
|
||||
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
|
||||
| 🔔 Webhook | Manage repo webhooks and test deliveries |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
| 👤 User | View user profiles and info |
|
||||
|
|
@ -315,6 +316,28 @@ gitlink-cli ci +log --owner Gitlink --repo forgeplus -i <build_id>
|
|||
gitlink-cli ci +restart --owner Gitlink --repo forgeplus -i <build_id>
|
||||
```
|
||||
|
||||
### Pipeline Operations
|
||||
|
||||
```bash
|
||||
# List platform pipelines
|
||||
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
|
||||
|
||||
# List repository pipeline runs
|
||||
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
|
||||
|
||||
# Start a pipeline workflow, previewing the request first
|
||||
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
|
||||
|
||||
# Inspect pipeline details and logs
|
||||
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
|
||||
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
|
||||
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
|
||||
|
||||
# Toggle or delete pipeline workflows, previewing destructive writes first
|
||||
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
|
||||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
```bash
|
||||
|
|
@ -492,6 +515,7 @@ See [skills/README.md](skills/README.md) for details.
|
|||
| `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) |
|
||||
| `gitlink-release` | Release management (create, view, delete, etc.) |
|
||||
| `gitlink-ci` | CI/CD operations (builds, logs, etc.) |
|
||||
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
|
||||
| `gitlink-search` | Search (repositories, users, etc.) |
|
||||
| `gitlink-org` | Organization management (members, teams, etc.) |
|
||||
| `gitlink-user` | User management (profile info, etc.) |
|
||||
|
|
@ -524,6 +548,7 @@ gitlink-cli/
|
|||
│ ├── release/ # Release shortcuts
|
||||
│ ├── org/ # Organization shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── search/ # Search shortcuts
|
||||
│ ├── user/ # User shortcuts
|
||||
│ └── register.go # Registration entry point
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
## 为什么选择 gitlink-cli?
|
||||
|
||||
- **Agent-Native 设计** — 开箱即用结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、组织、搜索、用户等常用工作流均提供高层命令
|
||||
- **广泛覆盖** — 仓库、Issue、PR、Webhook、成员、分支、Release、CI、Pipeline、组织、搜索、用户等常用工作流均提供高层命令
|
||||
- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出
|
||||
- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装
|
||||
- **开源零门槛** — 木兰宽松许可证第2版(MulanPSL-2.0),`npm install` 即用
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
| 🏷️ 发布 | 创建、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
|
|
@ -294,6 +295,28 @@ gitlink-cli release +create --owner Gitlink --repo forgeplus -t v1.0.0 -n "v1.0.
|
|||
gitlink-cli release +view --owner Gitlink --repo forgeplus -i <version_id>
|
||||
```
|
||||
|
||||
### 流水线管理
|
||||
|
||||
```bash
|
||||
# 列出平台流水线
|
||||
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
|
||||
|
||||
# 列出仓库流水线运行记录
|
||||
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
|
||||
|
||||
# 运行流水线工作流,先用 dry-run 预览请求
|
||||
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
|
||||
|
||||
# 查看流水线详情、日志和运行结果
|
||||
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
|
||||
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
|
||||
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
|
||||
|
||||
# 启停或删除流水线工作流,写入/删除前先预览
|
||||
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
|
||||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
### 搜索
|
||||
|
||||
```bash
|
||||
|
|
@ -372,6 +395,7 @@ git push gitlink
|
|||
| `gitlink-release` | 发布管理(创建、查看、删除等) |
|
||||
| `gitlink-org` | 组织管理(成员、团队等) |
|
||||
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
|
||||
| `gitlink-pipeline` | 流水线工作流操作(运行、日志、启停、删除等) |
|
||||
| `gitlink-search` | 搜索功能(仓库、用户等) |
|
||||
| `gitlink-user` | 用户管理(个人信息等) |
|
||||
| `gitlink-pm` | 项目管理(Sprint、看板、周报等) |
|
||||
|
|
@ -403,6 +427,7 @@ gitlink-cli/
|
|||
│ ├── release/ # Release shortcuts
|
||||
│ ├── org/ # 组织 shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── search/ # 搜索 shortcuts
|
||||
│ ├── user/ # 用户 shortcuts
|
||||
│ └── register.go # 注册入口
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# Pipeline OpenAPI Shortcuts
|
||||
|
||||
Submitter: Wang Yue
|
||||
|
||||
This change adds a dedicated `pipeline` shortcut group for GitLink Pipeline OpenAPI coverage.
|
||||
|
||||
## Commands
|
||||
|
||||
- `pipeline +list`
|
||||
- `pipeline +runs`
|
||||
- `pipeline +run`
|
||||
- `pipeline +view`
|
||||
- `pipeline +delete`
|
||||
- `pipeline +save-yaml`
|
||||
- `pipeline +enable`
|
||||
- `pipeline +disable`
|
||||
- `pipeline +logs`
|
||||
- `pipeline +results`
|
||||
|
||||
## API Mapping
|
||||
|
||||
| Shortcut | Method | API path |
|
||||
|----------|--------|----------|
|
||||
| `pipeline +list` | GET | `/api/pm/pipelines.json` |
|
||||
| `pipeline +runs` | GET | `/api/v1/{owner}/{repo}/actions/runs.json` |
|
||||
| `pipeline +run` | POST | `/api/v1/{owner}/{repo}/actions/runs.json` |
|
||||
| `pipeline +view` | GET | `/api/v1/{owner}/{repo}/pipelines/{id}.json` |
|
||||
| `pipeline +delete` | DELETE | `/api/v1/{owner}/{repo}/pipelines/{id}.json` |
|
||||
| `pipeline +save-yaml` | POST | `/api/v1/{owner}/{repo}/pipelines/save_yaml` |
|
||||
| `pipeline +enable` | POST | `/api/v1/{owner}/{repo}/actions/enable.json` |
|
||||
| `pipeline +disable` | POST | `/api/v1/{owner}/{repo}/actions/disable.json` |
|
||||
| `pipeline +logs` | POST | `/api/v1/{owner}/{repo}/actions/runs/{run_id}/jobs/0` |
|
||||
| `pipeline +results` | GET | `/api/v1/{owner}/{repo}/pipelines/run_results.json` |
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit tests cover request methods, paths, query parameters, request bodies, dry-run behavior, and invalid ID validation.
|
||||
- Help documentation is available through `gitlink-cli pipeline --help` and command-specific help.
|
||||
- Write and delete commands support `--dry-run` to preview requests before changing pipeline state.
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
package pipeline
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List platform pipelines",
|
||||
Flags: []common.Flag{
|
||||
{Name: "owner-id", Usage: "Owner user or organization ID"},
|
||||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
q := pageLimitQuery(ctx)
|
||||
setQueryIfPresent(q, ctx, "owner-id", "owner_id")
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/pm/pipelines", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "runs",
|
||||
Description: "List pipeline run records",
|
||||
Flags: runFilterFlags(),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", pipelineV1RepoPath(ctx)+"/actions/runs", runFilterQuery(ctx))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "run",
|
||||
Description: "Run a pipeline workflow",
|
||||
Flags: append(runFilterFlags(),
|
||||
common.Flag{Name: "dry-run", Usage: "Preview the run request without starting a pipeline", Bool: true, Default: "false"},
|
||||
),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := runFilterQuery(ctx)
|
||||
path := pipelineV1RepoPath(ctx) + "/actions/runs"
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"action": "run_pipeline",
|
||||
"method": "POST",
|
||||
"path": path,
|
||||
"query": q,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("POST", path, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "Show pipeline details",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := requiredPositiveInt(ctx, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/pipelines/%d", pipelineV1RepoPath(ctx), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete a pipeline",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the delete request without changing pipeline state", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := requiredPositiveInt(ctx, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/pipelines/%d", pipelineV1RepoPath(ctx), id)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"action": "delete_pipeline",
|
||||
"method": "DELETE",
|
||||
"path": path,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "save-yaml",
|
||||
Description: "Save a visual pipeline YAML graph",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
|
||||
{Name: "pipeline-json", Usage: "Pipeline graph JSON object or string", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the save request without changing pipeline state", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := requiredPositiveInt(ctx, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pipelineJSON, err := ctx.RequireArg("pipeline-json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"id": id,
|
||||
"pipeline_json": parseJSONValue(pipelineJSON),
|
||||
}
|
||||
path := pipelineV1RepoPath(ctx) + "/pipelines/save_yaml"
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"action": "save_pipeline_yaml",
|
||||
"method": "POST",
|
||||
"path": path,
|
||||
"payload": payload,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable",
|
||||
Description: "Enable a pipeline workflow",
|
||||
Flags: workflowStateFlags(true),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runWorkflowState(ctx, "enable")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "disable",
|
||||
Description: "Disable a pipeline workflow",
|
||||
Flags: workflowStateFlags(true),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
return runWorkflowState(ctx, "disable")
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "logs",
|
||||
Description: "Query pipeline run logs",
|
||||
Flags: []common.Flag{
|
||||
{Name: "run-id", Short: "r", Usage: "Pipeline run ID", Required: true},
|
||||
{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
|
||||
{Name: "index", Usage: "Run index", Required: true},
|
||||
{Name: "job", Short: "j", Usage: "Job index", Default: "0"},
|
||||
{Name: "cursor", Usage: "Log cursor"},
|
||||
{Name: "step", Usage: "Log step", Default: "1"},
|
||||
{Name: "expanded", Usage: "Whether the log cursor is expanded", Bool: true, Default: "true"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
runID, err := ctx.RequireArg("run-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := requiredPositiveInt(ctx, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index, err := ctx.RequireArg("index")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
job, err := requiredNonNegativeIntWithDefault(ctx, "job", 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
step, err := requiredPositiveIntWithDefault(ctx, "step", 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"id": id,
|
||||
"index": index,
|
||||
"job": job,
|
||||
"owner": ctx.Owner,
|
||||
"repo": ctx.Repo,
|
||||
"log_cursors": []map[string]interface{}{
|
||||
{
|
||||
"cursor": ctx.Arg("cursor"),
|
||||
"expanded": ctx.Arg("expanded") == "true",
|
||||
"step": step,
|
||||
},
|
||||
},
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/actions/runs/%s/jobs/0", pipelineV1RepoPath(ctx), url.PathEscape(runID)), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "results",
|
||||
Description: "Show pipeline run report results",
|
||||
Flags: []common.Flag{
|
||||
{Name: "run-id", Short: "r", Usage: "Pipeline run ID"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "run-id", "run_id")
|
||||
env, err := ctx.CallAPIWithQuery("GET", pipelineV1RepoPath(ctx)+"/pipelines/run_results", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pipelineV1RepoPath(ctx *common.RuntimeContext) string {
|
||||
return "/v1" + ctx.RepoPath()
|
||||
}
|
||||
|
||||
func runFilterFlags() []common.Flag {
|
||||
return []common.Flag{
|
||||
{Name: "ref", Short: "r", Usage: "Branch, tag, or commit SHA"},
|
||||
{Name: "workflow", Short: "w", Usage: "Workflow file name"},
|
||||
}
|
||||
}
|
||||
|
||||
func runFilterQuery(ctx *common.RuntimeContext) url.Values {
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "ref", "ref")
|
||||
setQueryIfPresent(q, ctx, "workflow", "workflow")
|
||||
return q
|
||||
}
|
||||
|
||||
func workflowStateFlags(includeDryRun bool) []common.Flag {
|
||||
flags := []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Pipeline ID", Required: true},
|
||||
{Name: "workflow", Short: "w", Usage: "Workflow file name", Required: true},
|
||||
}
|
||||
if includeDryRun {
|
||||
flags = append(flags, common.Flag{Name: "dry-run", Usage: "Preview the request without changing pipeline state", Bool: true, Default: "false"})
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func runWorkflowState(ctx *common.RuntimeContext, action string) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := requiredPositiveInt(ctx, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflow, err := ctx.RequireArg("workflow")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"id": id,
|
||||
"workflow": workflow,
|
||||
}
|
||||
path := fmt.Sprintf("%s/actions/%s", pipelineV1RepoPath(ctx), action)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(map[string]interface{}{
|
||||
"repository": fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
"dry_run": true,
|
||||
"action": action + "_pipeline",
|
||||
"method": "POST",
|
||||
"path": path,
|
||||
"payload": payload,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", path, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func pageLimitQuery(ctx *common.RuntimeContext) url.Values {
|
||||
q := url.Values{}
|
||||
setQueryIfPresent(q, ctx, "page", "page")
|
||||
setQueryIfPresent(q, ctx, "limit", "limit")
|
||||
return q
|
||||
}
|
||||
|
||||
func setQueryIfPresent(q url.Values, ctx *common.RuntimeContext, flagName, queryName string) {
|
||||
if value := ctx.Arg(flagName); value != "" {
|
||||
q.Set(queryName, value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSONValue(value string) interface{} {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return value
|
||||
}
|
||||
var parsed interface{}
|
||||
if err := json.Unmarshal([]byte(trimmed), &parsed); err == nil {
|
||||
return parsed
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func requiredPositiveInt(ctx *common.RuntimeContext, flagName string) (int, error) {
|
||||
value, err := ctx.RequireArg(flagName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id, err := strconv.Atoi(value)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("--%s must be a positive integer", flagName)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func requiredPositiveIntWithDefault(ctx *common.RuntimeContext, flagName string, defaultValue int) (int, error) {
|
||||
value := ctx.Arg(flagName)
|
||||
if value == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
id, err := strconv.Atoi(value)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("--%s must be a positive integer", flagName)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func requiredNonNegativeIntWithDefault(ctx *common.RuntimeContext, flagName string, defaultValue int) (int, error) {
|
||||
value := ctx.Arg(flagName)
|
||||
if value == "" {
|
||||
return defaultValue, nil
|
||||
}
|
||||
id, err := strconv.Atoi(value)
|
||||
if err != nil || id < 0 {
|
||||
return 0, fmt.Errorf("--%s must be a non-negative integer", flagName)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,389 @@
|
|||
package pipeline
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestPipelineList(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "GET", "/pm/pipelines.json")
|
||||
assertPipelineQuery(t, r, "owner_id", "42")
|
||||
assertPipelineQuery(t, r, "page", "2")
|
||||
assertPipelineQuery(t, r, "limit", "50")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0, "message": "ok"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "list", map[string]string{
|
||||
"owner-id": "42",
|
||||
"page": "2",
|
||||
"limit": "50",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineRuns(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "GET", "/v1/owner/repo/actions/runs.json")
|
||||
assertPipelineQuery(t, r, "ref", "master")
|
||||
assertPipelineQuery(t, r, "workflow", "build.yml")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"runs": []interface{}{}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "runs", map[string]string{
|
||||
"ref": "master",
|
||||
"workflow": "build.yml",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("runs shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineRun(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "POST", "/v1/owner/repo/actions/runs.json")
|
||||
assertPipelineQuery(t, r, "ref", "master")
|
||||
assertPipelineQuery(t, r, "workflow", "build.yml")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "run", map[string]string{
|
||||
"ref": "master",
|
||||
"workflow": "build.yml",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineView(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "GET", "/v1/owner/repo/pipelines/7.json")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"id": 7})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runPipelineShortcut(t, server, "view", map[string]string{"id": "7"}); err != nil {
|
||||
t.Fatalf("view shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineDelete(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "DELETE", "/v1/owner/repo/pipelines/7.json")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runPipelineShortcut(t, server, "delete", map[string]string{"id": "7"}); err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineSaveYamlPayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "POST", "/v1/owner/repo/pipelines/save_yaml.json")
|
||||
payload = decodePipelineJSON(t, r)
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "save-yaml", map[string]string{
|
||||
"id": "7",
|
||||
"pipeline-json": `{"nodes":[]}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("save-yaml shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertPipelineEqual(t, payload["id"], float64(7))
|
||||
graph, ok := payload["pipeline_json"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("pipeline_json = %T, want object", payload["pipeline_json"])
|
||||
}
|
||||
nodes, ok := graph["nodes"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("nodes = %T, want array", graph["nodes"])
|
||||
}
|
||||
if len(nodes) != 0 {
|
||||
t.Fatalf("nodes length = %d, want 0", len(nodes))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineEnablePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "POST", "/v1/owner/repo/actions/enable.json")
|
||||
payload = decodePipelineJSON(t, r)
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "enable", map[string]string{
|
||||
"id": "7",
|
||||
"workflow": "build.yml",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("enable shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertPipelineEqual(t, payload["id"], float64(7))
|
||||
assertPipelineEqual(t, payload["workflow"], "build.yml")
|
||||
}
|
||||
|
||||
func TestPipelineDisablePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "POST", "/v1/owner/repo/actions/disable.json")
|
||||
payload = decodePipelineJSON(t, r)
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "disable", map[string]string{
|
||||
"id": "7",
|
||||
"workflow": "build.yml",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("disable shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertPipelineEqual(t, payload["id"], float64(7))
|
||||
assertPipelineEqual(t, payload["workflow"], "build.yml")
|
||||
}
|
||||
|
||||
func TestPipelineLogsPayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "POST", "/v1/owner/repo/actions/runs/99/jobs/0.json")
|
||||
payload = decodePipelineJSON(t, r)
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "logs", map[string]string{
|
||||
"run-id": "99",
|
||||
"id": "7",
|
||||
"index": "43",
|
||||
"job": "0",
|
||||
"cursor": "cursor-1",
|
||||
"step": "1",
|
||||
"expanded": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("logs shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertPipelineEqual(t, payload["id"], float64(7))
|
||||
assertPipelineEqual(t, payload["index"], "43")
|
||||
assertPipelineEqual(t, payload["job"], float64(0))
|
||||
assertPipelineEqual(t, payload["owner"], "owner")
|
||||
assertPipelineEqual(t, payload["repo"], "repo")
|
||||
cursors, ok := payload["log_cursors"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("log_cursors = %T, want array", payload["log_cursors"])
|
||||
}
|
||||
if len(cursors) != 1 {
|
||||
t.Fatalf("log_cursors length = %d, want 1", len(cursors))
|
||||
}
|
||||
cursor, ok := cursors[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("cursor entry = %T, want object", cursors[0])
|
||||
}
|
||||
assertPipelineEqual(t, cursor["cursor"], "cursor-1")
|
||||
assertPipelineEqual(t, cursor["expanded"], true)
|
||||
assertPipelineEqual(t, cursor["step"], float64(1))
|
||||
}
|
||||
|
||||
func TestPipelineResults(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertPipelineRequest(t, r, "GET", "/v1/owner/repo/pipelines/run_results.json")
|
||||
assertPipelineQuery(t, r, "run_id", "99")
|
||||
writePipelineJSON(t, w, map[string]interface{}{"reports": []interface{}{}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runPipelineShortcut(t, server, "results", map[string]string{"run-id": "99"}); err != nil {
|
||||
t.Fatalf("results shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineDryRunDoesNotCallAPI(t *testing.T) {
|
||||
dryRunCases := []struct {
|
||||
name string
|
||||
args map[string]string
|
||||
}{
|
||||
{name: "run", args: map[string]string{"dry-run": "true", "ref": "master"}},
|
||||
{name: "delete", args: map[string]string{"dry-run": "true", "id": "7"}},
|
||||
{name: "save-yaml", args: map[string]string{"dry-run": "true", "id": "7", "pipeline-json": `{"nodes":[]}`}},
|
||||
{name: "enable", args: map[string]string{"dry-run": "true", "id": "7", "workflow": "build.yml"}},
|
||||
{name: "disable", args: map[string]string{"dry-run": "true", "id": "7", "workflow": "build.yml"}},
|
||||
}
|
||||
|
||||
for _, tc := range dryRunCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runPipelineShortcut(t, server, tc.name, tc.args); err != nil {
|
||||
t.Fatalf("%s dry-run failed: %v", tc.name, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineRejectsInvalidID(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid id should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "view", map[string]string{"id": "abc"})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid id to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func runPipelineShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findPipelineShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findPipelineShortcut(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 newPipelineTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func assertPipelineRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.Path != path {
|
||||
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPipelineQuery(t *testing.T, r *http.Request, key, want string) {
|
||||
t.Helper()
|
||||
if got := r.URL.Query().Get(key); got != want {
|
||||
t.Fatalf("query %s = %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func decodePipelineJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode request body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writePipelineJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
t.Fatalf("failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertPipelineEqual(t *testing.T, got interface{}, want interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineShortcutNames(t *testing.T) {
|
||||
got := map[string]bool{}
|
||||
for _, shortcut := range Shortcuts() {
|
||||
got[shortcut.Name] = true
|
||||
}
|
||||
want := []string{"list", "runs", "run", "view", "delete", "save-yaml", "enable", "disable", "logs", "results"}
|
||||
for _, name := range want {
|
||||
if !got[name] {
|
||||
t.Fatalf("missing shortcut %q in %v", name, got)
|
||||
}
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("shortcut count = %d, want %d: %v", len(got), len(want), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPipelineRunIDIsPathEscaped(t *testing.T) {
|
||||
server := newPipelineTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
t.Fatalf("got method %s, want POST", r.Method)
|
||||
}
|
||||
if got, want := r.URL.EscapedPath(), "/v1/owner/repo/actions/runs/run%2F99/jobs/0.json"; got != want {
|
||||
t.Fatalf("escaped path = %q, want %q", got, want)
|
||||
}
|
||||
writePipelineJSON(t, w, map[string]interface{}{"status": 0})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runPipelineShortcut(t, server, "logs", map[string]string{
|
||||
"run-id": "run/99",
|
||||
"id": "7",
|
||||
"index": "43",
|
||||
"expanded": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("logs shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleShortcuts() {
|
||||
for _, shortcut := range Shortcuts() {
|
||||
fmt.Println(shortcut.Name)
|
||||
}
|
||||
// Output:
|
||||
// list
|
||||
// runs
|
||||
// run
|
||||
// view
|
||||
// delete
|
||||
// save-yaml
|
||||
// enable
|
||||
// disable
|
||||
// logs
|
||||
// results
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/member"
|
||||
"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/pr"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
|
|
@ -23,37 +24,39 @@ import (
|
|||
// RegisterAll mounts all shortcut groups onto the root command.
|
||||
func RegisterAll(root *cobra.Command) {
|
||||
groups := map[string][]*common.Shortcut{
|
||||
"repo": repo.Shortcuts(),
|
||||
"repo": repo.Shortcuts(),
|
||||
"issue": issue.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(),
|
||||
"release": release.Shortcuts(),
|
||||
"branch": branch.Shortcuts(),
|
||||
"org": org.Shortcuts(),
|
||||
"user": user.Shortcuts(),
|
||||
"search": search.Shortcuts(),
|
||||
"ci": ci.Shortcuts(),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
"release": release.Shortcuts(),
|
||||
"branch": branch.Shortcuts(),
|
||||
"org": org.Shortcuts(),
|
||||
"user": user.Shortcuts(),
|
||||
"search": search.Shortcuts(),
|
||||
"ci": ci.Shortcuts(),
|
||||
"compare": compare.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
"repo": "Repository operations",
|
||||
"repo": "Repository operations",
|
||||
"issue": "Issue operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": "Pull request operations",
|
||||
"release": "Release operations",
|
||||
"branch": "Branch operations",
|
||||
"org": "Organization operations",
|
||||
"user": "User operations",
|
||||
"search": "Search operations",
|
||||
"ci": "CI/CD operations",
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": "Webhook operations",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
"release": "Release operations",
|
||||
"branch": "Branch operations",
|
||||
"org": "Organization operations",
|
||||
"user": "User operations",
|
||||
"search": "Search operations",
|
||||
"ci": "CI/CD operations",
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"webhook": "Webhook operations",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ skills/
|
|||
│ ├── SKILL.md # CI 操作指南
|
||||
│ └── examples/
|
||||
│ └── ci-workflow.md # CI 工作流
|
||||
├── gitlink-pipeline/ # 流水线工作流
|
||||
│ └── SKILL.md # Pipeline 操作指南
|
||||
├── gitlink-pm/ # 项目管理
|
||||
│ └── SKILL.md # PM 操作指南
|
||||
└── gitlink-workflow/ # AI 自动化工作流
|
||||
|
|
@ -138,6 +140,7 @@ skills/
|
|||
| **gitlink-user** | 用户管理 | `user +me`, `user +info` |
|
||||
| **gitlink-org** | 组织管理 | `org +list`, `org +info`, `org +members` |
|
||||
| **gitlink-ci** | CI/CD | `ci +builds`, `ci +logs` |
|
||||
| **gitlink-pipeline** | 流水线工作流 | `pipeline +runs`, `pipeline +run`, `pipeline +logs` |
|
||||
| **gitlink-pm** | 项目管理 | 通过 Raw API 访问 |
|
||||
| **gitlink-workflow** | AI 工作流 | Issue 分类、PR Review、Release Notes |
|
||||
|
||||
|
|
@ -238,6 +241,7 @@ gitlink-cli org +info -i Gitlink
|
|||
|
||||
**发布和搜索**:
|
||||
- [gitlink-release/SKILL.md](gitlink-release/SKILL.md) - Release 命令
|
||||
- [gitlink-pipeline/SKILL.md](gitlink-pipeline/SKILL.md) - Pipeline 命令
|
||||
- [gitlink-search/SKILL.md](gitlink-search/SKILL.md) - 搜索命令
|
||||
|
||||
**组织和用户**:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
name: gitlink-pipeline
|
||||
version: 1.0.0
|
||||
description: "Pipeline workflow operations: list pipelines, run workflows, inspect runs, fetch logs, save visual YAML, enable, disable, and delete pipelines."
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli pipeline --help"
|
||||
---
|
||||
|
||||
# gitlink-pipeline
|
||||
|
||||
Read [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) first for authentication, global flags, and API behavior.
|
||||
|
||||
**CRITICAL**: confirm user intent before running write or delete commands. Prefer `--dry-run` before `pipeline +run`, `pipeline +save-yaml`, `pipeline +enable`, `pipeline +disable`, or `pipeline +delete`.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | Description |
|
||||
|----------|-------------|
|
||||
| `pipeline +list` | List platform pipelines |
|
||||
| `pipeline +runs` | List repository pipeline run records |
|
||||
| `pipeline +run` | Start a pipeline workflow |
|
||||
| `pipeline +view` | Show pipeline details |
|
||||
| `pipeline +delete` | Delete a pipeline |
|
||||
| `pipeline +save-yaml` | Save a visual pipeline YAML graph |
|
||||
| `pipeline +enable` | Enable a pipeline workflow |
|
||||
| `pipeline +disable` | Disable a pipeline workflow |
|
||||
| `pipeline +logs` | Query pipeline run logs |
|
||||
| `pipeline +results` | Show pipeline run report results |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# List platform pipelines
|
||||
gitlink-cli pipeline +list --owner-id 123 --page 1 --limit 20
|
||||
|
||||
# List and run workflow records for a repository
|
||||
gitlink-cli pipeline +runs --owner Gitlink --repo forgeplus --ref master --workflow build.yml
|
||||
gitlink-cli pipeline +run --owner Gitlink --repo forgeplus --ref master --workflow build.yml --dry-run
|
||||
|
||||
# Inspect a pipeline and its logs
|
||||
gitlink-cli pipeline +view --owner Gitlink --repo forgeplus --id 7
|
||||
gitlink-cli pipeline +logs --owner Gitlink --repo forgeplus --run-id 99 --id 7 --index 43
|
||||
gitlink-cli pipeline +results --owner Gitlink --repo forgeplus --run-id 99
|
||||
|
||||
# Save a visual pipeline graph
|
||||
gitlink-cli pipeline +save-yaml --owner Gitlink --repo forgeplus \
|
||||
--id 7 --pipeline-json '{"nodes":[]}' --dry-run
|
||||
|
||||
# Toggle or delete pipeline workflows
|
||||
gitlink-cli pipeline +enable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
|
||||
gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow build.yml --dry-run
|
||||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
## API Mapping
|
||||
|
||||
| Shortcut | Method | API path |
|
||||
|----------|--------|----------|
|
||||
| `pipeline +list` | GET | `/api/pm/pipelines.json` |
|
||||
| `pipeline +runs` | GET | `/api/v1/{owner}/{repo}/actions/runs.json` |
|
||||
| `pipeline +run` | POST | `/api/v1/{owner}/{repo}/actions/runs.json` |
|
||||
| `pipeline +view` | GET | `/api/v1/{owner}/{repo}/pipelines/{id}.json` |
|
||||
| `pipeline +delete` | DELETE | `/api/v1/{owner}/{repo}/pipelines/{id}.json` |
|
||||
| `pipeline +save-yaml` | POST | `/api/v1/{owner}/{repo}/pipelines/save_yaml` |
|
||||
| `pipeline +enable` | POST | `/api/v1/{owner}/{repo}/actions/enable.json` |
|
||||
| `pipeline +disable` | POST | `/api/v1/{owner}/{repo}/actions/disable.json` |
|
||||
| `pipeline +logs` | POST | `/api/v1/{owner}/{repo}/actions/runs/{run_id}/jobs/0` |
|
||||
| `pipeline +results` | GET | `/api/v1/{owner}/{repo}/pipelines/run_results.json` |
|
||||
Loading…
Reference in New Issue