krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: internal/control/webhook.go · raw

  1package control
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"strconv"
  8
  9	"gitbay.org/gitbay/internal/policy"
 10	"gitbay.org/gitbay/internal/protocol"
 11	"gitbay.org/gitbay/internal/store"
 12	"gitbay.org/gitbay/internal/webhook"
 13)
 14
 15func init() {
 16	register(Command{Path: []string{"webhook", "add"},
 17		Summary: "add a webhook: webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd})
 18	register(Command{Path: []string{"webhook", "list"},
 19		Summary: "list webhooks: webhook list <owner/name>", ReadOnly: true, Run: runWebhookList})
 20	register(Command{Path: []string{"webhook", "remove"},
 21		Summary: "remove a webhook: webhook remove <owner/name> <id>", Run: runWebhookRemove})
 22	register(Command{Path: []string{"webhook", "deliveries"},
 23		Summary: "recent deliveries: webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries})
 24	register(Command{Path: []string{"webhook", "redeliver"},
 25		Summary: "queue a delivery again: webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver})
 26}
 27
 28func runWebhookAdd(c *Ctx, args []string) int {
 29	var path, url, secret string
 30	events := "*"
 31	for i := 0; i < len(args); i++ {
 32		switch args[i] {
 33		case "--secret", "--events":
 34			if i+1 >= len(args) {
 35				return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
 36			}
 37			if args[i] == "--secret" {
 38				secret = args[i+1]
 39			} else {
 40				events = args[i+1]
 41			}
 42			i++
 43		default:
 44			if path == "" {
 45				path = args[i]
 46			} else if url == "" {
 47				url = args[i]
 48			} else {
 49				return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
 50			}
 51		}
 52	}
 53	if path == "" || url == "" {
 54		return c.fail(protocol.ExitUsage, "usage: webhook add <owner/name> <url> [--secret <s>] [--events <k1,k2>|*]")
 55	}
 56	repo, code := resolveRepo(c, path, policy.CanAdmin)
 57	if code >= 0 {
 58		return code
 59	}
 60	if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil {
 61		return c.fail(protocol.ExitUsage, "%v", err)
 62	}
 63	id, err := c.Store.AddWebhook(repo.ID, url, secret, events)
 64	if err != nil {
 65		return c.fail(protocol.ExitFailure, "%v", err)
 66	}
 67	return c.emit(map[string]any{"id": id, "url": url, "events": events}, func(w io.Writer) {
 68		fmt.Fprintf(w, "webhook %d added for %s (%s)\n", id, repo.Path(), events)
 69	})
 70}
 71
 72func runWebhookList(c *Ctx, args []string) int {
 73	if len(args) != 1 {
 74		return c.fail(protocol.ExitUsage, "usage: webhook list <owner/name>")
 75	}
 76	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
 77	if code >= 0 {
 78		return code
 79	}
 80	hooks, err := c.Store.ListWebhooks(repo.ID)
 81	if err != nil {
 82		return c.fail(protocol.ExitFailure, "%v", err)
 83	}
 84	type out struct {
 85		ID     int64  `json:"id"`
 86		URL    string `json:"url"`
 87		Events string `json:"events"`
 88		Active bool   `json:"active"`
 89		Secret bool   `json:"has_secret"`
 90	}
 91	var ds []out
 92	for _, h := range hooks {
 93		ds = append(ds, out{h.ID, h.URL, h.Events, h.Active, h.Secret != ""})
 94	}
 95	return c.emit(ds, func(w io.Writer) {
 96		for _, d := range ds {
 97			fmt.Fprintf(w, "%d\t%s\t%s\n", d.ID, d.URL, d.Events)
 98		}
 99	})
100}
101
102func runWebhookRemove(c *Ctx, args []string) int {
103	if len(args) != 2 {
104		return c.fail(protocol.ExitUsage, "usage: webhook remove <owner/name> <id>")
105	}
106	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
107	if code >= 0 {
108		return code
109	}
110	id, err := strconv.ParseInt(args[1], 10, 64)
111	if err != nil {
112		return c.fail(protocol.ExitUsage, "bad webhook id %q", args[1])
113	}
114	if err := c.Store.RemoveWebhook(repo.ID, id); err != nil {
115		if errors.Is(err, store.ErrNotFound) {
116			return c.fail(protocol.ExitNotFound, "no webhook %d on %s", id, repo.Path())
117		}
118		return c.fail(protocol.ExitFailure, "%v", err)
119	}
120	return c.emit(map[string]any{"removed": id}, func(w io.Writer) {
121		fmt.Fprintf(w, "removed webhook %d\n", id)
122	})
123}
124
125func runWebhookDeliveries(c *Ctx, args []string) int {
126	limit := 20
127	var path string
128	for i := 0; i < len(args); i++ {
129		if args[i] == "--limit" {
130			if i+1 >= len(args) {
131				return c.fail(protocol.ExitUsage, "--limit requires a value")
132			}
133			n, err := strconv.Atoi(args[i+1])
134			if err != nil || n < 1 || n > 200 {
135				return c.fail(protocol.ExitUsage, "--limit must be 1..200")
136			}
137			limit = n
138			i++
139			continue
140		}
141		if path != "" {
142			return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
143		}
144		path = args[i]
145	}
146	if path == "" {
147		return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
148	}
149	repo, code := resolveRepo(c, path, policy.CanAdmin)
150	if code >= 0 {
151		return code
152	}
153	ds, err := c.Store.ListDeliveries(repo.ID, limit)
154	if err != nil {
155		return c.fail(protocol.ExitFailure, "%v", err)
156	}
157	type out struct {
158		ID         int64  `json:"id"`
159		URL        string `json:"url"`
160		Event      string `json:"event"`
161		Status     string `json:"status"`
162		Attempts   int    `json:"attempts"`
163		LastStatus int    `json:"last_status,omitempty"`
164		LastError  string `json:"last_error,omitempty"`
165	}
166	var rows []out
167	for _, d := range ds {
168		rows = append(rows, out{d.ID, d.URL, d.EventKind, d.Status, d.Attempts, d.LastStatus, d.LastError})
169	}
170	return c.emit(rows, func(w io.Writer) {
171		for _, d := range rows {
172			extra := ""
173			if d.LastError != "" {
174				extra = "\t" + d.LastError
175			}
176			fmt.Fprintf(w, "%d\t%s\t%s\t%s (%d attempts)%s\n", d.ID, d.Event, d.URL, d.Status, d.Attempts, extra)
177		}
178	})
179}
180
181func runWebhookRedeliver(c *Ctx, args []string) int {
182	if len(args) != 2 {
183		return c.fail(protocol.ExitUsage, "usage: webhook redeliver <owner/name> <delivery-id>")
184	}
185	repo, code := resolveRepo(c, args[0], policy.CanAdmin)
186	if code >= 0 {
187		return code
188	}
189	id, err := strconv.ParseInt(args[1], 10, 64)
190	if err != nil {
191		return c.fail(protocol.ExitUsage, "bad delivery id %q", args[1])
192	}
193	if err := c.Store.Redeliver(repo.ID, id); err != nil {
194		if errors.Is(err, store.ErrNotFound) {
195			return c.fail(protocol.ExitNotFound, "no delivery %d on %s", id, repo.Path())
196		}
197		return c.fail(protocol.ExitFailure, "%v", err)
198	}
199	return c.emit(map[string]any{"requeued": id}, func(w io.Writer) {
200		fmt.Fprintf(w, "delivery %d requeued\n", id)
201	})
202}