gitlink-cli/shortcuts/webhook/webhook.go

341 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package webhook
import (
"fmt"
"net/url"
"strings"
clierrors "github.com/gitlink-org/gitlink-cli/internal/errors"
"github.com/gitlink-org/gitlink-cli/internal/output"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// 支持的Webhook事件类型
var supportedEvents = []string{
"push",
"pull_request",
"issue",
"issue_assign",
"issue_comment",
"pull_request_assign",
"pull_request_comment",
"merge_request",
"repository",
"branch",
"tag",
}
func isEventSupported(event string) bool {
for _, supported := range supportedEvents {
if event == supported {
return true
}
}
return false
}
func parseEvents(eventsStr string) []string {
if eventsStr == "" {
return []string{"push"} // 默认事件
}
events := strings.Split(eventsStr, ",")
var validEvents []string
for _, event := range events {
event = strings.TrimSpace(event)
if isEventSupported(event) {
validEvents = append(validEvents, event)
}
}
return validEvents
}
// webhookRepoPath returns the webhook API path prefix: /v1/{owner}/{repo}
// Note: BaseURL already includes /api prefix
func webhookRepoPath(ctx *common.RuntimeContext) string {
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
}
func Shortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "list",
Description: "List all webhooks for a repository",
Flags: []common.Flag{
{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 {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
q.Set("page", ctx.Arg("page"))
q.Set("limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", webhookRepoPath(ctx)+"/webhooks", q)
if err != nil {
return fmt.Errorf("获取 Webhook 列表失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "create",
Description: "Create a new webhook",
Flags: []common.Flag{
{Name: "url", Short: "u", Usage: "Webhook callback URL", Required: true},
{Name: "events", Short: "e", Usage: "Trigger events (comma-separated), e.g., push,pull_request,issue", Default: "push"},
{Name: "active", Usage: "Webhook active status (true/false)", Default: "true"},
{Name: "secret", Usage: "Webhook secret for HMAC verification"},
{Name: "description", Short: "d", Usage: "Webhook description"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
webhookURL, err := ctx.RequireArg("url", "--url https://example.com/hook")
if err != nil {
return err
}
events := parseEvents(ctx.Arg("events"))
if len(events) == 0 {
return clierrors.InputError(
"no valid events specified",
fmt.Sprintf("支持的事件类型: %s", strings.Join(supportedEvents, ", ")),
)
}
payload := map[string]interface{}{
"url": webhookURL,
"http_method": "POST",
"active": true,
"content_type": "json",
}
if len(events) > 0 {
payload["events"] = events
} else {
payload["events"] = []string{"push"}
}
if secret := ctx.Arg("secret"); secret != "" {
payload["secret"] = secret
}
if description := ctx.Arg("description"); description != "" {
payload["description"] = description
}
env, err := ctx.CallAPI("POST", webhookRepoPath(ctx)+"/webhooks", payload)
if err != nil {
return fmt.Errorf("创建 Webhook 失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "update",
Description: "Update an existing webhook",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
{Name: "url", Short: "u", Usage: "Webhook callback URL"},
{Name: "events", Short: "e", Usage: "Trigger events (comma-separated)"},
{Name: "active", Usage: "Webhook active status (true/false)"},
{Name: "content_type", Usage: "Content type (json/form)"},
{Name: "secret", Usage: "Webhook secret for HMAC verification"},
{Name: "description", Short: "d", Usage: "Webhook description"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
webhookID, err := ctx.RequireArg("id", "--id 1")
if err != nil {
return err
}
payload := map[string]interface{}{
"http_method": "POST",
"active": true,
"content_type": "json",
}
// 如果用户没有提供URL获取当前webhook的URL
webhookURL := ctx.Arg("url")
if webhookURL == "" {
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if err != nil {
return fmt.Errorf("获取 Webhook 当前信息失败: %w", err)
}
webhookData, ok := getEnv.Data.(map[string]interface{})
if !ok {
return fmt.Errorf("failed to parse webhook data")
}
currentURL, ok := webhookData["url"].(string)
if !ok || currentURL == "" {
return fmt.Errorf("failed to get current webhook URL")
}
webhookURL = currentURL
}
payload["url"] = webhookURL
if events := ctx.Arg("events"); events != "" {
validEvents := parseEvents(events)
if len(validEvents) == 0 {
return clierrors.InputError(
"no valid events specified",
fmt.Sprintf("支持的事件类型: %s", strings.Join(supportedEvents, ", ")),
)
}
payload["events"] = validEvents
}
if contentType := ctx.Arg("content_type"); contentType != "" {
payload["content_type"] = contentType
}
if secret := ctx.Arg("secret"); secret != "" {
payload["secret"] = secret
}
if description := ctx.Arg("description"); description != "" {
payload["description"] = description
}
env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), payload)
if err != nil {
return fmt.Errorf("更新 Webhook 失败: %w", err)
}
return ctx.Output(env)
},
},
{
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
}
webhookID, err := ctx.RequireArg("id", "--id 1")
if err != nil {
return err
}
_, delErr := ctx.CallAPI("DELETE", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if delErr != nil {
// 验证是否真的删除成功类似release的处理
_, viewErr := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if viewErr != nil {
// Webhook不存在了说明删除成功
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
"message": "Webhook deleted successfully",
}, nil))
}
return fmt.Errorf("删除 Webhook 失败: %w", delErr)
}
return ctx.Output(output.SuccessEnvelope(map[string]interface{}{
"message": "Webhook deleted successfully",
}, nil))
},
},
{
Name: "test",
Description: "Test a webhook delivery (send a ping event)",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "Webhook ID", Required: true},
{Name: "event", Short: "e", Usage: "Event type to test (default: push)", Default: "push"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
webhookID, err := ctx.RequireArg("id", "--id 1")
if err != nil {
return err
}
eventType := ctx.Arg("event")
if !isEventSupported(eventType) {
return clierrors.InputError(
fmt.Sprintf("unsupported event type: %s", eventType),
fmt.Sprintf("支持的事件类型: %s", strings.Join(supportedEvents, ", ")),
)
}
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", webhookRepoPath(ctx), webhookID), nil)
if err != nil {
return fmt.Errorf("测试 Webhook 失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "info",
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
}
webhookID, err := ctx.RequireArg("id", "--id 1")
if err != nil {
return err
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
if err != nil {
return fmt.Errorf("查看 Webhook 详情失败: %w", err)
}
return ctx.Output(env)
},
},
{
Name: "events",
Description: "List all supported event types for webhooks",
Run: func(ctx *common.RuntimeContext) error {
eventInfo := make([]map[string]interface{}, 0)
for _, event := range supportedEvents {
eventInfo = append(eventInfo, map[string]interface{}{
"event": event,
"supported": true,
"description": getEventDescription(event),
})
}
return ctx.Output(output.SuccessEnvelope(eventInfo, nil))
},
},
}
}
func getEventDescription(event string) string {
descriptions := map[string]string{
"push": "Code push events",
"pull_request": "Pull request events",
"issue": "Issue events",
"issue_assign": "Issue assignment events",
"issue_comment": "Issue comment events",
"pull_request_assign": "Pull request assignment events",
"pull_request_comment":"Pull request comment events",
"merge_request": "Merge request events",
"repository": "Repository events",
"branch": "Branch creation/deletion events",
"tag": "Tag creation/deletion events",
}
if desc, ok := descriptions[event]; ok {
return desc
}
return "Custom event"
}