package cmd import ( "fmt" "strconv" "strings" "code.gitea.io/sdk/gitea" "forgejo.zerova.net/public/fgj-sid/internal/api" "forgejo.zerova.net/public/fgj-sid/internal/config" "github.com/spf13/cobra" ) var webhookCmd = &cobra.Command{ Use: "webhook", Aliases: []string{"webhooks", "hook"}, Short: "Manage repository webhooks", Long: "List, create, update, and delete webhooks attached to a repository.", } var webhookListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "List webhooks for a repository", Example: ` # List webhooks on the current repository fgj webhook list # List with JSON output fgj webhook list --json`, RunE: runWebhookList, } var webhookCreateCmd = &cobra.Command{ Use: "create ", Short: "Create a repository webhook", Long: `Create a webhook that delivers events to . Event names follow the Gitea/Forgejo webhook event model: push, pull_request, issues, issue_comment, release, create, delete, fork, wiki, repository, and others. Omit --events to deliver only the default (push).`, Example: ` # Create a Gitea-format push webhook fgj webhook create https://example.com/hook # Multiple events and a content type fgj webhook create https://ci.example.com/hook \ --events push,pull_request,release \ --content-type application/json \ --secret "$HOOK_SECRET" # Slack-style webhook fgj webhook create https://hooks.slack.com/services/XXX --type slack`, Args: cobra.ExactArgs(1), RunE: runWebhookCreate, } var webhookUpdateCmd = &cobra.Command{ Use: "update ", Aliases: []string{"edit"}, Short: "Update a repository webhook", Long: "Update an existing webhook. Flags you omit are left unchanged.", Args: cobra.ExactArgs(1), Example: ` # Disable a webhook fgj webhook update 12 --active=false # Change events and URL fgj webhook update 12 --url https://new.example.com/hook --events push,release`, RunE: runWebhookUpdate, } var webhookDeleteCmd = &cobra.Command{ Use: "delete ", Aliases: []string{"rm"}, Short: "Delete a repository webhook", Args: cobra.ExactArgs(1), RunE: runWebhookDelete, } func init() { rootCmd.AddCommand(webhookCmd) webhookCmd.AddCommand(webhookListCmd) webhookCmd.AddCommand(webhookCreateCmd) webhookCmd.AddCommand(webhookUpdateCmd) webhookCmd.AddCommand(webhookDeleteCmd) addRepoFlags(webhookListCmd) addJSONFlags(webhookListCmd, "Output as JSON") addRepoFlags(webhookCreateCmd) webhookCreateCmd.Flags().String("type", "gitea", "Hook type (gitea, slack, discord, msteams, telegram, feishu, gogs)") webhookCreateCmd.Flags().StringSlice("events", []string{"push"}, "Events to deliver (comma-separated)") webhookCreateCmd.Flags().String("content-type", "application/json", "Content type (application/json or application/x-www-form-urlencoded)") webhookCreateCmd.Flags().String("secret", "", "HMAC secret used to sign payloads") webhookCreateCmd.Flags().String("branch-filter", "", "Glob filter for branches that trigger the hook") webhookCreateCmd.Flags().String("authorization-header", "", "Authorization header value sent with each delivery") webhookCreateCmd.Flags().Bool("active", true, "Whether the hook is active on creation") addRepoFlags(webhookUpdateCmd) webhookUpdateCmd.Flags().String("url", "", "New target URL") webhookUpdateCmd.Flags().StringSlice("events", nil, "New event list (replaces existing)") webhookUpdateCmd.Flags().String("content-type", "", "New content type") webhookUpdateCmd.Flags().String("secret", "", "New HMAC secret") webhookUpdateCmd.Flags().String("branch-filter", "", "New branch filter") webhookUpdateCmd.Flags().String("authorization-header", "", "New authorization header") webhookUpdateCmd.Flags().Bool("active", true, "Enable or disable the hook") addRepoFlags(webhookDeleteCmd) webhookDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") } func runWebhookList(cmd *cobra.Command, args []string) error { client, owner, name, err := newWebhookClient(cmd) if err != nil { return err } hooks, _, err := client.ListRepoHooks(owner, name, gitea.ListHooksOptions{}) if err != nil { return fmt.Errorf("failed to list webhooks: %w", err) } if wantJSON(cmd) { return outputJSON(cmd, hooks) } if len(hooks) == 0 { fmt.Fprintln(ios.Out, "No webhooks.") return nil } tp := ios.NewTablePrinter() tp.AddHeader("ID", "TYPE", "URL", "EVENTS", "ACTIVE") for _, h := range hooks { url := h.Config["url"] active := "no" if h.Active { active = "yes" } tp.AddRow( strconv.FormatInt(h.ID, 10), h.Type, url, strings.Join(h.Events, ","), active, ) } return tp.Render() } func runWebhookCreate(cmd *cobra.Command, args []string) error { client, owner, name, err := newWebhookClient(cmd) if err != nil { return err } url := args[0] hookType, _ := cmd.Flags().GetString("type") events, _ := cmd.Flags().GetStringSlice("events") contentType, _ := cmd.Flags().GetString("content-type") secret, _ := cmd.Flags().GetString("secret") branchFilter, _ := cmd.Flags().GetString("branch-filter") authHeader, _ := cmd.Flags().GetString("authorization-header") active, _ := cmd.Flags().GetBool("active") opt := gitea.CreateHookOption{ Type: gitea.HookType(hookType), Config: map[string]string{ "url": url, "content_type": contentType, }, Events: events, BranchFilter: branchFilter, Active: active, AuthorizationHeader: authHeader, } if secret != "" { opt.Config["secret"] = secret } hook, _, err := client.CreateRepoHook(owner, name, opt) if err != nil { return fmt.Errorf("failed to create webhook: %w", err) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Created webhook %d (%s → %s)\n", cs.SuccessIcon(), hook.ID, hook.Type, url) return nil } func runWebhookUpdate(cmd *cobra.Command, args []string) error { client, owner, name, err := newWebhookClient(cmd) if err != nil { return err } id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("invalid webhook id %q: %w", args[0], err) } opt := gitea.EditHookOption{} // Only set fields the user explicitly provided. cfg := map[string]string{} if url, _ := cmd.Flags().GetString("url"); url != "" { cfg["url"] = url } if ct, _ := cmd.Flags().GetString("content-type"); ct != "" { cfg["content_type"] = ct } if secret, _ := cmd.Flags().GetString("secret"); secret != "" { cfg["secret"] = secret } if len(cfg) > 0 { opt.Config = cfg } if cmd.Flags().Changed("events") { events, _ := cmd.Flags().GetStringSlice("events") opt.Events = events } if cmd.Flags().Changed("branch-filter") { bf, _ := cmd.Flags().GetString("branch-filter") opt.BranchFilter = bf } if cmd.Flags().Changed("authorization-header") { auth, _ := cmd.Flags().GetString("authorization-header") opt.AuthorizationHeader = auth } if cmd.Flags().Changed("active") { active, _ := cmd.Flags().GetBool("active") opt.Active = &active } if _, err := client.EditRepoHook(owner, name, id, opt); err != nil { return fmt.Errorf("failed to update webhook %d: %w", id, err) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Updated webhook %d\n", cs.SuccessIcon(), id) return nil } func runWebhookDelete(cmd *cobra.Command, args []string) error { client, owner, name, err := newWebhookClient(cmd) if err != nil { return err } id, err := strconv.ParseInt(args[0], 10, 64) if err != nil { return fmt.Errorf("invalid webhook id %q: %w", args[0], err) } skipConfirm, _ := cmd.Flags().GetBool("yes") if !skipConfirm && ios.IsStdinTTY() { answer, err := promptLine(fmt.Sprintf("Delete webhook %d in %s/%s? [y/N]: ", id, owner, name)) if err != nil { return err } if answer != "y" && answer != "Y" && answer != "yes" { fmt.Fprintln(ios.ErrOut, "Cancelled.") return nil } } if _, err := client.DeleteRepoHook(owner, name, id); err != nil { return fmt.Errorf("failed to delete webhook %d: %w", id, err) } cs := ios.ColorScheme() fmt.Fprintf(ios.Out, "%s Deleted webhook %d\n", cs.SuccessIcon(), id) return nil } func newWebhookClient(cmd *cobra.Command) (*api.Client, string, string, error) { repo, _ := cmd.Flags().GetString("repo") owner, name, err := parseRepo(repo) if err != nil { return nil, "", "", err } cfg, err := config.Load() if err != nil { return nil, "", "", err } client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return nil, "", "", err } return client, owner, name, nil }