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}