331 lines
9.3 KiB
Go
331 lines
9.3 KiB
Go
package webhook
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"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 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")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
events := parseEvents(ctx.Arg("events"))
|
||
if len(events) == 0 {
|
||
return fmt.Errorf("no valid events specified. Supported events: %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 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")
|
||
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("failed to get current webhook info: %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 fmt.Errorf("no valid events specified. Supported events: %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 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")
|
||
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 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")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
eventType := ctx.Arg("event")
|
||
if !isEventSupported(eventType) {
|
||
return fmt.Errorf("unsupported event type: %s. Supported events: %s", eventType, strings.Join(supportedEvents, ", "))
|
||
}
|
||
|
||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/webhooks/%s/tests", webhookRepoPath(ctx), webhookID), nil)
|
||
if err != nil {
|
||
return 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")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/webhooks/%s", webhookRepoPath(ctx), webhookID), nil)
|
||
if err != nil {
|
||
return 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"
|
||
}
|