feat: add webhook shortcut group
This commit is contained in:
parent
c4ec40ab8b
commit
83a642a72d
21
README.md
21
README.md
|
|
@ -5,7 +5,7 @@
|
|||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
|
||||
The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, CI/CD, and AI-powered workflows, with 40+ commands and 12 AI Agent [Skills](./skills/).
|
||||
The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans and AI Agents. Supports **macOS, Linux, and Windows**. Covers repository management, issue tracking, pull requests, webhooks, CI/CD, and AI-powered workflows, with 40+ commands and 13 AI Agent [Skills](./skills/).
|
||||
|
||||
**[中文文档](./README.zh-CN.md)**
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
## Why gitlink-cli?
|
||||
|
||||
- **Agent-Native Design** — 12 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, Branch, Release, CI, Org, Search, User — all core domains covered
|
||||
- **Wide Coverage** — Repository, Issue, PR, Webhook, Branch, Release, CI, Org, Search, User — all core domains covered
|
||||
- **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`
|
||||
|
|
@ -33,6 +33,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 |
|
||||
| 🔔 Webhook | Manage repo webhooks and test deliveries |
|
||||
| 🔍 Search | Search repositories, users |
|
||||
| 👤 User | View user profiles and info |
|
||||
| 📋 PM | Sprint management, kanban boards, weekly reports |
|
||||
|
|
@ -55,7 +56,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
**From npm (recommended):**
|
||||
|
||||
```bash
|
||||
# One command: installs CLI binary + all 12 AI Agent Skills
|
||||
# One command: installs CLI binary + all 13 AI Agent Skills
|
||||
npm install -g @gitlink-ai/cli
|
||||
```
|
||||
|
||||
|
|
@ -143,6 +144,20 @@ gitlink-cli repo +create -n my-project -d "Project description"
|
|||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Webhook Management
|
||||
|
||||
```bash
|
||||
# List webhooks
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Create a webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
|
||||
--url https://example.com/hook --events push,create
|
||||
|
||||
# Test a webhook
|
||||
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
|
||||
```
|
||||
|
||||
### Issue Management
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
[](https://golang.org)
|
||||
[](https://www.npmjs.com/package/@gitlink-ai/cli)
|
||||
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 11 个 AI Agent [Skills](./skills/)。
|
||||
[GitLink(确实开源)](https://www.gitlink.org.cn) 官方 CLI 工具 — 为人类和 AI Agent 双重设计。支持 **macOS、Linux、Windows**,覆盖仓库管理、Issue 追踪、Pull Request、Webhook、CI/CD 和 AI 自动化工作流,包含 40+ 命令和 12 个 AI Agent [Skills](./skills/)。
|
||||
|
||||
**[English](./README.md)**
|
||||
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
## 为什么选择 gitlink-cli?
|
||||
|
||||
- **Agent-Native 设计** — 开箱即用 11 个结构化 [Skills](./skills/),兼容 Claude Code — Agent 零配置即可操作 GitLink
|
||||
- **广泛覆盖** — 仓库、Issue、PR、分支、Release、CI、组织、搜索、用户 — 核心功能全覆盖
|
||||
- **广泛覆盖** — 仓库、Issue、PR、Webhook、分支、Release、CI、组织、搜索、用户 — 核心功能全覆盖
|
||||
- **AI 友好 & 优化** — 每条命令都经过真实 Agent 测试,简洁参数、智能默认值、结构化输出
|
||||
- **跨平台** — macOS、Linux、Windows (x64/arm64) 全支持,`npm` 一条命令安装
|
||||
- **开源零门槛** — 木兰宽松许可证第2版(MulanPSL-2.0),`npm install` 即用
|
||||
|
|
@ -155,6 +155,20 @@ gitlink-cli repo +create -n my-project -d "项目描述"
|
|||
gitlink-cli repo +fork --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
### Webhook 管理
|
||||
|
||||
```bash
|
||||
# 列出 webhook
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建 webhook
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
|
||||
--url https://example.com/hook --events push,create
|
||||
|
||||
# 测试 webhook
|
||||
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
|
||||
```
|
||||
|
||||
### Issue 管理
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/pr"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/release"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/repo"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/search"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
)
|
||||
|
|
@ -27,6 +28,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"user": user.Shortcuts(),
|
||||
"search": search.Shortcuts(),
|
||||
"ci": ci.Shortcuts(),
|
||||
"webhook": webhook.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
|
|
@ -39,6 +41,7 @@ func RegisterAll(root *cobra.Command) {
|
|||
"user": "User operations",
|
||||
"search": "Search operations",
|
||||
"ci": "CI/CD operations",
|
||||
"webhook": "Webhook operations",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "List webhooks",
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath()+"/webhooks", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "Create a webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "url", Short: "u", Usage: "Webhook target URL", Required: true},
|
||||
{Name: "type", Short: "t", Usage: "Webhook provider type", Default: "gitea"},
|
||||
{Name: "events", Short: "e", Usage: "Comma-separated events, for example: push,create,delete", Required: true},
|
||||
{Name: "secret", Short: "s", Usage: "Webhook secret"},
|
||||
{Name: "content-type", Usage: "POST content type: json or form", Default: "json"},
|
||||
{Name: "http-method", Usage: "Delivery method: GET or POST", Default: "POST"},
|
||||
{Name: "branch-filter", Usage: "Branch filter glob", Default: "*"},
|
||||
{Name: "active", Short: "a", Usage: "Enable webhook (true/false)", Default: "true"},
|
||||
},
|
||||
Run: runCreateWebhook,
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: "Show webhook details",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Description: "Update a webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
{Name: "url", Short: "u", Usage: "Webhook target URL"},
|
||||
{Name: "type", Short: "t", Usage: "Webhook provider type"},
|
||||
{Name: "events", Short: "e", Usage: "Comma-separated events"},
|
||||
{Name: "secret", Short: "s", Usage: "Webhook secret"},
|
||||
{Name: "content-type", Usage: "POST content type: json or form"},
|
||||
{Name: "http-method", Usage: "Delivery method: GET or POST"},
|
||||
{Name: "branch-filter", Usage: "Branch filter glob"},
|
||||
{Name: "active", Short: "a", Usage: "Enable webhook (true/false)"},
|
||||
},
|
||||
Run: runUpdateWebhook,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "Delete a webhook",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/webhooks/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test",
|
||||
Description: "Trigger a webhook test delivery",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runCreateWebhook(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload, err := buildWebhookPayload(ctx, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", ctx.RepoPath()+"/webhooks", payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runUpdateWebhook(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", ctx.RepoPath(), id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current, _ := currentEnv.Data.(map[string]interface{})
|
||||
|
||||
payload, err := buildWebhookPayload(ctx, current, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/webhooks/%s", ctx.RepoPath(), id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func buildWebhookPayload(ctx *common.RuntimeContext, current map[string]interface{}, requireEvents bool) (map[string]interface{}, error) {
|
||||
payload := map[string]interface{}{}
|
||||
|
||||
if v := ctx.Arg("type"); v != "" {
|
||||
payload["type"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["type"].(string); ok && v != "" {
|
||||
payload["type"] = v
|
||||
}
|
||||
}
|
||||
|
||||
if v := ctx.Arg("url"); v != "" {
|
||||
payload["url"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["url"].(string); ok && v != "" {
|
||||
payload["url"] = v
|
||||
}
|
||||
}
|
||||
|
||||
if v := ctx.Arg("content-type"); v != "" {
|
||||
payload["content_type"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["content_type"].(string); ok && v != "" {
|
||||
payload["content_type"] = v
|
||||
}
|
||||
}
|
||||
|
||||
if v := ctx.Arg("http-method"); v != "" {
|
||||
payload["http_method"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["http_method"].(string); ok && v != "" {
|
||||
payload["http_method"] = v
|
||||
}
|
||||
}
|
||||
|
||||
if v := ctx.Arg("secret"); v != "" {
|
||||
payload["secret"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["secret"].(string); ok && v != "" {
|
||||
payload["secret"] = v
|
||||
}
|
||||
}
|
||||
|
||||
if v := ctx.Arg("branch-filter"); v != "" {
|
||||
payload["branch_filter"] = v
|
||||
} else if current != nil {
|
||||
if v, ok := current["branch_filter"].(string); ok && v != "" {
|
||||
payload["branch_filter"] = v
|
||||
}
|
||||
}
|
||||
|
||||
eventsValue := ctx.Arg("events")
|
||||
if eventsValue != "" {
|
||||
events, err := parseCommaList(eventsValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload["events"] = events
|
||||
} else if current != nil {
|
||||
if events, ok := normalizeStringSlice(current["events"]); ok {
|
||||
payload["events"] = events
|
||||
}
|
||||
}
|
||||
|
||||
activeValue := ctx.Arg("active")
|
||||
if activeValue != "" {
|
||||
active, err := parseBoolString(activeValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload["active"] = active
|
||||
} else if current != nil {
|
||||
if active, ok := current["active"].(bool); ok {
|
||||
payload["active"] = active
|
||||
}
|
||||
}
|
||||
|
||||
if requireEvents {
|
||||
if _, ok := payload["events"]; !ok {
|
||||
return nil, fmt.Errorf("required flag --events is missing")
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := payload["type"]; !ok {
|
||||
payload["type"] = "gitea"
|
||||
}
|
||||
if _, ok := payload["content_type"]; !ok {
|
||||
payload["content_type"] = "json"
|
||||
}
|
||||
if _, ok := payload["http_method"]; !ok {
|
||||
payload["http_method"] = "POST"
|
||||
}
|
||||
if _, ok := payload["branch_filter"]; !ok {
|
||||
payload["branch_filter"] = "*"
|
||||
}
|
||||
if _, ok := payload["active"]; !ok {
|
||||
payload["active"] = true
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
func parseCommaList(value string) ([]string, error) {
|
||||
items := strings.Split(value, ",")
|
||||
results := make([]string, 0, len(items))
|
||||
for _, item := range items {
|
||||
value := strings.TrimSpace(item)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
results = append(results, value)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
return nil, fmt.Errorf("no values provided")
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func normalizeStringSlice(value interface{}) ([]string, bool) {
|
||||
switch v := value.(type) {
|
||||
case []string:
|
||||
return v, true
|
||||
case []interface{}:
|
||||
items := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
s, ok := item.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, s)
|
||||
}
|
||||
return items, len(items) > 0
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func parseBoolString(value string) (bool, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "true", "1", "yes", "y", "on":
|
||||
return true, nil
|
||||
case "false", "0", "no", "n", "off":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("invalid boolean value %q: use true or false", value)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestWebhookCreateBuildsPayload(t *testing.T) {
|
||||
var createPayload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/webhooks.json":
|
||||
createPayload = decodeWebhookJSON(t, r)
|
||||
writeWebhookJSON(t, w, map[string]interface{}{"id": float64(1)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runWebhookShortcut(t, server, "create", map[string]string{
|
||||
"url": "https://example.com/hook",
|
||||
"events": "push,create",
|
||||
"type": "slack",
|
||||
"content-type": "form",
|
||||
"http-method": "GET",
|
||||
"secret": "abc123",
|
||||
"branch-filter": "master",
|
||||
"active": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertWebhookEqual(t, createPayload["url"], "https://example.com/hook")
|
||||
assertWebhookEqual(t, createPayload["type"], "slack")
|
||||
assertWebhookEqual(t, createPayload["content_type"], "form")
|
||||
assertWebhookEqual(t, createPayload["http_method"], "GET")
|
||||
assertWebhookEqual(t, createPayload["secret"], "abc123")
|
||||
assertWebhookEqual(t, createPayload["branch_filter"], "master")
|
||||
assertWebhookEqual(t, createPayload["active"], false)
|
||||
}
|
||||
|
||||
func TestWebhookUpdatePreservesExistingFields(t *testing.T) {
|
||||
var updatePayload map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET" && r.URL.Path == "/v1/owner/repo/webhooks/68.json":
|
||||
writeWebhookJSON(t, w, map[string]interface{}{
|
||||
"id": float64(68),
|
||||
"type": "gitea",
|
||||
"url": "https://old.example.com/hook",
|
||||
"content_type": "json",
|
||||
"http_method": "POST",
|
||||
"secret": "old-secret",
|
||||
"branch_filter": "*",
|
||||
"events": []interface{}{"push", "create"},
|
||||
"active": true,
|
||||
})
|
||||
case r.Method == "PUT" && r.URL.Path == "/v1/owner/repo/webhooks/68.json":
|
||||
updatePayload = decodeWebhookJSON(t, r)
|
||||
writeWebhookJSON(t, w, map[string]interface{}{"id": float64(68)})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runWebhookShortcut(t, server, "update", map[string]string{
|
||||
"id": "68",
|
||||
"url": "https://new.example.com/hook",
|
||||
"active": "false",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertWebhookEqual(t, updatePayload["url"], "https://new.example.com/hook")
|
||||
assertWebhookEqual(t, updatePayload["type"], "gitea")
|
||||
assertWebhookEqual(t, updatePayload["content_type"], "json")
|
||||
assertWebhookEqual(t, updatePayload["http_method"], "POST")
|
||||
assertWebhookEqual(t, updatePayload["secret"], "old-secret")
|
||||
assertWebhookEqual(t, updatePayload["branch_filter"], "*")
|
||||
assertWebhookEqual(t, updatePayload["active"], false)
|
||||
}
|
||||
|
||||
func TestWebhookTestTriggersDelivery(t *testing.T) {
|
||||
called := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "POST" && r.URL.Path == "/v1/owner/repo/webhooks/68/tests.json":
|
||||
called = true
|
||||
writeWebhookJSON(t, w, map[string]interface{}{"status": float64(0), "message": "success"})
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := runWebhookShortcut(t, server, "test", map[string]string{"id": "68"})
|
||||
if err != nil {
|
||||
t.Fatalf("test shortcut failed: %v", err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("webhook test endpoint was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func runWebhookShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findWebhookShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findWebhookShortcut(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 decodeWebhookJSON(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 writeWebhookJSON(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 assertWebhookEqual(t *testing.T, got interface{}, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
name: gitlink-webhook
|
||||
version: 1.0.0
|
||||
description: "Webhook 配置与测试:列出、创建、查看、更新、删除并触发 webhook 测试。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli webhook --help"
|
||||
---
|
||||
|
||||
# gitlink-webhook(Webhook 管理)
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | 说明 |
|
||||
|----------|------|
|
||||
| `webhook +list` | 列出仓库 webhook |
|
||||
| `webhook +create` | 创建 webhook |
|
||||
| `webhook +view` | 查看 webhook 详情 |
|
||||
| `webhook +update` | 更新 webhook |
|
||||
| `webhook +delete` | 删除 webhook |
|
||||
| `webhook +test` | 触发 webhook 测试投递 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```bash
|
||||
gitlink-cli webhook +list --owner Gitlink --repo forgeplus
|
||||
gitlink-cli webhook +create --owner Gitlink --repo forgeplus \
|
||||
--url https://example.com/hook --events push,create
|
||||
gitlink-cli webhook +test --owner Gitlink --repo forgeplus --id 68
|
||||
```
|
||||
Loading…
Reference in New Issue