loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request '[FEAT] sourcehut webhooks' (#3022) from oliverpool/forgejo:webhook_7_sourcehut into forgejo

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3022
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Reviewed-by: 0ko <0ko@noreply.codeberg.org>

+1122 -224
+10
modules/structs/repo.go
··· 115 115 RepoTransfer *RepoTransfer `json:"repo_transfer"` 116 116 } 117 117 118 + // GetName implements the gitrepo.Repository interface 119 + func (r Repository) GetName() string { 120 + return r.Name 121 + } 122 + 123 + // GetOwnerName implements the gitrepo.Repository interface 124 + func (r Repository) GetOwnerName() string { 125 + return r.Owner.UserName 126 + } 127 + 118 128 // CreateRepoOption options when creating repository 119 129 // swagger:model 120 130 type CreateRepoOption struct {
+13 -12
modules/webhook/type.go
··· 73 73 74 74 // Types of webhooks 75 75 const ( 76 - FORGEJO HookType = "forgejo" 77 - GITEA HookType = "gitea" 78 - GOGS HookType = "gogs" 79 - SLACK HookType = "slack" 80 - DISCORD HookType = "discord" 81 - DINGTALK HookType = "dingtalk" 82 - TELEGRAM HookType = "telegram" 83 - MSTEAMS HookType = "msteams" 84 - FEISHU HookType = "feishu" 85 - MATRIX HookType = "matrix" 86 - WECHATWORK HookType = "wechatwork" 87 - PACKAGIST HookType = "packagist" 76 + FORGEJO HookType = "forgejo" 77 + GITEA HookType = "gitea" 78 + GOGS HookType = "gogs" 79 + SLACK HookType = "slack" 80 + DISCORD HookType = "discord" 81 + DINGTALK HookType = "dingtalk" 82 + TELEGRAM HookType = "telegram" 83 + MSTEAMS HookType = "msteams" 84 + FEISHU HookType = "feishu" 85 + MATRIX HookType = "matrix" 86 + WECHATWORK HookType = "wechatwork" 87 + PACKAGIST HookType = "packagist" 88 + SOURCEHUT_BUILDS HookType = "sourcehut_builds" //nolint:revive 88 89 ) 89 90 90 91 // HookStatus is the status of a web hook
+9
options/locale/locale_en-US.ini
··· 640 640 641 641 admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first. 642 642 643 + required_prefix = Input must start with "%s" 644 + 643 645 [user] 644 646 change_avatar = Change your avatar… 645 647 joined_on = Joined on %s ··· 2269 2271 settings.remove_team_success = The team's access to the repository has been removed. 2270 2272 settings.add_webhook = Add webhook 2271 2273 settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. 2274 + settings.add_webhook.invalid_path = Path must not contain a part that is "." or ".." or the empty string. It cannot start or end with a slash. 2272 2275 settings.hooks_desc = Webhooks automatically make HTTP POST requests to a server when certain Forgejo events trigger. Read more in the <a target="_blank" rel="noopener noreferrer" href="%s">webhooks guide</a>. 2273 2276 settings.webhook_deletion = Remove webhook 2274 2277 settings.webhook_deletion_desc = Removing a webhook deletes its settings and delivery history. Continue? ··· 2384 2387 settings.packagist_username = Packagist username 2385 2388 settings.packagist_api_token = API token 2386 2389 settings.packagist_package_url = Packagist package URL 2390 + settings.web_hook_name_sourcehut_builds = SourceHut Builds 2391 + settings.sourcehut_builds.manifest_path = Build manifest path 2392 + settings.sourcehut_builds.graphql_url = GraphQL URL (e.g. https://builds.sr.ht/query) 2393 + settings.sourcehut_builds.visibility = Job visibility 2394 + settings.sourcehut_builds.secrets = Secrets 2395 + settings.sourcehut_builds.secrets_helper = Give the job access to the build secrets (requires the SECRETS:RO grant) 2387 2396 settings.deploy_keys = Deploy keys 2388 2397 settings.add_deploy_key = Add deploy key 2389 2398 settings.deploy_key_desc = Deploy keys have read-only pull access to the repository.
+7
public/assets/img/sourcehut.svg
··· 1 + <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"> 2 + <style> 3 + path { fill: black; } 4 + @media (prefers-color-scheme: dark) { path { fill: white; } } 5 + </style> 6 + <path d="M256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm0 448c-110.5 0-200-89.5-200-200S145.5 56 256 56s200 89.5 200 200-89.5 200-200 200z"/> 7 + </svg>
+12 -12
routers/web/repo/setting/webhook.go
··· 148 148 } 149 149 150 150 // ParseHookEvent convert web form content to webhook.HookEvent 151 - func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { 151 + func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent { 152 152 return &webhook_module.HookEvent{ 153 153 PushOnly: form.PushOnly(), 154 154 SendEverything: form.SendEverything(), ··· 188 188 return 189 189 } 190 190 191 - fields := handler.FormFields(func(form any) { 191 + fields := handler.UnmarshalForm(func(form any) { 192 192 errs := binding.Bind(ctx.Req, form) 193 193 middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError 194 194 }) ··· 215 215 w.URL = fields.URL 216 216 w.ContentType = fields.ContentType 217 217 w.Secret = fields.Secret 218 - w.HookEvent = ParseHookEvent(fields.WebhookForm) 219 - w.IsActive = fields.WebhookForm.Active 218 + w.HookEvent = ParseHookEvent(fields.WebhookCoreForm) 219 + w.IsActive = fields.Active 220 220 w.HTTPMethod = fields.HTTPMethod 221 - err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) 221 + err := w.SetHeaderAuthorization(fields.AuthorizationHeader) 222 222 if err != nil { 223 223 ctx.ServerError("SetHeaderAuthorization", err) 224 224 return ··· 245 245 HTTPMethod: fields.HTTPMethod, 246 246 ContentType: fields.ContentType, 247 247 Secret: fields.Secret, 248 - HookEvent: ParseHookEvent(fields.WebhookForm), 249 - IsActive: fields.WebhookForm.Active, 248 + HookEvent: ParseHookEvent(fields.WebhookCoreForm), 249 + IsActive: fields.Active, 250 250 Type: hookType, 251 251 Meta: string(meta), 252 252 OwnerID: orCtx.OwnerID, 253 253 IsSystemWebhook: orCtx.IsSystemWebhook, 254 254 } 255 - err = w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) 255 + err = w.SetHeaderAuthorization(fields.AuthorizationHeader) 256 256 if err != nil { 257 257 ctx.ServerError("SetHeaderAuthorization", err) 258 258 return ··· 286 286 return 287 287 } 288 288 289 - fields := handler.FormFields(func(form any) { 289 + fields := handler.UnmarshalForm(func(form any) { 290 290 errs := binding.Bind(ctx.Req, form) 291 291 middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError 292 292 }) ··· 295 295 w.URL = fields.URL 296 296 w.ContentType = fields.ContentType 297 297 w.Secret = fields.Secret 298 - w.HookEvent = ParseHookEvent(fields.WebhookForm) 299 - w.IsActive = fields.WebhookForm.Active 298 + w.HookEvent = ParseHookEvent(fields.WebhookCoreForm) 299 + w.IsActive = fields.Active 300 300 w.HTTPMethod = fields.HTTPMethod 301 301 302 - err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) 302 + err := w.SetHeaderAuthorization(fields.AuthorizationHeader) 303 303 if err != nil { 304 304 ctx.ServerError("SetHeaderAuthorization", err) 305 305 return
+16 -5
services/forms/repo_form.go
··· 12 12 "code.gitea.io/gitea/models" 13 13 issues_model "code.gitea.io/gitea/models/issues" 14 14 project_model "code.gitea.io/gitea/models/project" 15 + webhook_model "code.gitea.io/gitea/models/webhook" 15 16 "code.gitea.io/gitea/modules/setting" 16 17 "code.gitea.io/gitea/modules/structs" 17 18 "code.gitea.io/gitea/modules/web/middleware" ··· 235 236 // \__/\ / \___ >___ /___| /\____/ \____/|__|_ \ 236 237 // \/ \/ \/ \/ \/ 237 238 238 - // WebhookForm form for changing web hook 239 - type WebhookForm struct { 239 + // WebhookCoreForm form for changing web hook (common to all webhook types) 240 + type WebhookCoreForm struct { 240 241 Events string 241 242 Create bool 242 243 Delete bool ··· 265 266 } 266 267 267 268 // PushOnly if the hook will be triggered when push 268 - func (f WebhookForm) PushOnly() bool { 269 + func (f WebhookCoreForm) PushOnly() bool { 269 270 return f.Events == "push_only" 270 271 } 271 272 272 273 // SendEverything if the hook will be triggered any event 273 - func (f WebhookForm) SendEverything() bool { 274 + func (f WebhookCoreForm) SendEverything() bool { 274 275 return f.Events == "send_everything" 275 276 } 276 277 277 278 // ChooseEvents if the hook will be triggered choose events 278 - func (f WebhookForm) ChooseEvents() bool { 279 + func (f WebhookCoreForm) ChooseEvents() bool { 279 280 return f.Events == "choose_events" 281 + } 282 + 283 + // WebhookForm form for changing web hook (specific handling depending on the webhook type) 284 + type WebhookForm struct { 285 + WebhookCoreForm 286 + URL string 287 + ContentType webhook_model.HookContentType 288 + Secret string 289 + HTTPMethod string 290 + Metadata any 280 291 } 281 292 282 293 // .___
+12 -53
services/webhook/default.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "crypto/hmac" 9 - "crypto/sha1" 10 - "crypto/sha256" 11 - "encoding/hex" 12 8 "fmt" 13 9 "html/template" 14 - "io" 15 10 "net/http" 16 11 "net/url" 17 12 "strings" ··· 21 16 "code.gitea.io/gitea/modules/svg" 22 17 webhook_module "code.gitea.io/gitea/modules/webhook" 23 18 "code.gitea.io/gitea/services/forms" 19 + "code.gitea.io/gitea/services/webhook/shared" 24 20 ) 25 21 26 22 var _ Handler = defaultHandler{} ··· 39 35 func (dh defaultHandler) Icon(size int) template.HTML { 40 36 if dh.forgejo { 41 37 // forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work 42 - return imgIcon("forgejo.svg", size) 38 + return shared.ImgIcon("forgejo.svg", size) 43 39 } 44 40 return svg.RenderHTML("gitea-gitea", size, "img") 45 41 } 46 42 47 43 func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } 48 44 49 - func (defaultHandler) FormFields(bind func(any)) FormFields { 45 + func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 50 46 var form struct { 51 - forms.WebhookForm 47 + forms.WebhookCoreForm 52 48 PayloadURL string `binding:"Required;ValidUrl"` 53 49 HTTPMethod string `binding:"Required;In(POST,GET)"` 54 50 ContentType int `binding:"Required"` ··· 60 56 if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { 61 57 contentType = webhook_model.ContentTypeForm 62 58 } 63 - return FormFields{ 64 - WebhookForm: form.WebhookForm, 65 - URL: form.PayloadURL, 66 - ContentType: contentType, 67 - Secret: form.Secret, 68 - HTTPMethod: form.HTTPMethod, 69 - Metadata: nil, 59 + return forms.WebhookForm{ 60 + WebhookCoreForm: form.WebhookCoreForm, 61 + URL: form.PayloadURL, 62 + ContentType: contentType, 63 + Secret: form.Secret, 64 + HTTPMethod: form.HTTPMethod, 65 + Metadata: nil, 70 66 } 71 67 } 72 68 ··· 130 126 } 131 127 132 128 body = []byte(t.PayloadContent) 133 - return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) 134 - } 135 - 136 - func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { 137 - var signatureSHA1 string 138 - var signatureSHA256 string 139 - if len(secret) > 0 { 140 - sig1 := hmac.New(sha1.New, secret) 141 - sig256 := hmac.New(sha256.New, secret) 142 - _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) 143 - if err != nil { 144 - // this error should never happen, since the hashes are writing to []byte and always return a nil error. 145 - return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) 146 - } 147 - signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) 148 - signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) 149 - } 150 - 151 - event := t.EventType.Event() 152 - eventType := string(t.EventType) 153 - req.Header.Add("X-Forgejo-Delivery", t.UUID) 154 - req.Header.Add("X-Forgejo-Event", event) 155 - req.Header.Add("X-Forgejo-Event-Type", eventType) 156 - req.Header.Add("X-Forgejo-Signature", signatureSHA256) 157 - req.Header.Add("X-Gitea-Delivery", t.UUID) 158 - req.Header.Add("X-Gitea-Event", event) 159 - req.Header.Add("X-Gitea-Event-Type", eventType) 160 - req.Header.Add("X-Gitea-Signature", signatureSHA256) 161 - req.Header.Add("X-Gogs-Delivery", t.UUID) 162 - req.Header.Add("X-Gogs-Event", event) 163 - req.Header.Add("X-Gogs-Event-Type", eventType) 164 - req.Header.Add("X-Gogs-Signature", signatureSHA256) 165 - req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) 166 - req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) 167 - req.Header["X-GitHub-Delivery"] = []string{t.UUID} 168 - req.Header["X-GitHub-Event"] = []string{event} 169 - req.Header["X-GitHub-Event-Type"] = []string{eventType} 170 - return nil 129 + return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) 171 130 }
+13 -12
services/webhook/dingtalk.go
··· 17 17 "code.gitea.io/gitea/modules/util" 18 18 webhook_module "code.gitea.io/gitea/modules/webhook" 19 19 "code.gitea.io/gitea/services/forms" 20 + "code.gitea.io/gitea/services/webhook/shared" 20 21 ) 21 22 22 23 type dingtalkHandler struct{} 23 24 24 25 func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK } 25 26 func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } 26 - func (dingtalkHandler) Icon(size int) template.HTML { return imgIcon("dingtalk.ico", size) } 27 + func (dingtalkHandler) Icon(size int) template.HTML { return shared.ImgIcon("dingtalk.ico", size) } 27 28 28 - func (dingtalkHandler) FormFields(bind func(any)) FormFields { 29 + func (dingtalkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 29 30 var form struct { 30 - forms.WebhookForm 31 + forms.WebhookCoreForm 31 32 PayloadURL string `binding:"Required;ValidUrl"` 32 33 } 33 34 bind(&form) 34 35 35 - return FormFields{ 36 - WebhookForm: form.WebhookForm, 37 - URL: form.PayloadURL, 38 - ContentType: webhook_model.ContentTypeJSON, 39 - Secret: "", 40 - HTTPMethod: http.MethodPost, 41 - Metadata: nil, 36 + return forms.WebhookForm{ 37 + WebhookCoreForm: form.WebhookCoreForm, 38 + URL: form.PayloadURL, 39 + ContentType: webhook_model.ContentTypeJSON, 40 + Secret: "", 41 + HTTPMethod: http.MethodPost, 42 + Metadata: nil, 42 43 } 43 44 } 44 45 ··· 225 226 226 227 type dingtalkConvertor struct{} 227 228 228 - var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} 229 + var _ shared.PayloadConvertor[DingtalkPayload] = dingtalkConvertor{} 229 230 230 231 func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 231 - return newJSONRequest(dingtalkConvertor{}, w, t, true) 232 + return shared.NewJSONRequest(dingtalkConvertor{}, w, t, true) 232 233 }
+12 -11
services/webhook/discord.go
··· 22 22 "code.gitea.io/gitea/modules/util" 23 23 webhook_module "code.gitea.io/gitea/modules/webhook" 24 24 "code.gitea.io/gitea/services/forms" 25 + "code.gitea.io/gitea/services/webhook/shared" 25 26 ) 26 27 27 28 type discordHandler struct{} 28 29 29 30 func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } 30 - func (discordHandler) Icon(size int) template.HTML { return imgIcon("discord.png", size) } 31 + func (discordHandler) Icon(size int) template.HTML { return shared.ImgIcon("discord.png", size) } 31 32 32 - func (discordHandler) FormFields(bind func(any)) FormFields { 33 + func (discordHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 33 34 var form struct { 34 - forms.WebhookForm 35 + forms.WebhookCoreForm 35 36 PayloadURL string `binding:"Required;ValidUrl"` 36 37 Username string 37 38 IconURL string 38 39 } 39 40 bind(&form) 40 41 41 - return FormFields{ 42 - WebhookForm: form.WebhookForm, 43 - URL: form.PayloadURL, 44 - ContentType: webhook_model.ContentTypeJSON, 45 - Secret: "", 46 - HTTPMethod: http.MethodPost, 42 + return forms.WebhookForm{ 43 + WebhookCoreForm: form.WebhookCoreForm, 44 + URL: form.PayloadURL, 45 + ContentType: webhook_model.ContentTypeJSON, 46 + Secret: "", 47 + HTTPMethod: http.MethodPost, 47 48 Metadata: &DiscordMeta{ 48 49 Username: form.Username, 49 50 IconURL: form.IconURL, ··· 287 288 AvatarURL string 288 289 } 289 290 290 - var _ payloadConvertor[DiscordPayload] = discordConvertor{} 291 + var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{} 291 292 292 293 func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 293 294 meta := &DiscordMeta{} ··· 298 299 Username: meta.Username, 299 300 AvatarURL: meta.IconURL, 300 301 } 301 - return newJSONRequest(sc, w, t, true) 302 + return shared.NewJSONRequest(sc, w, t, true) 302 303 } 303 304 304 305 func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
+13 -12
services/webhook/feishu.go
··· 15 15 api "code.gitea.io/gitea/modules/structs" 16 16 webhook_module "code.gitea.io/gitea/modules/webhook" 17 17 "code.gitea.io/gitea/services/forms" 18 + "code.gitea.io/gitea/services/webhook/shared" 18 19 ) 19 20 20 21 type feishuHandler struct{} 21 22 22 23 func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } 23 - func (feishuHandler) Icon(size int) template.HTML { return imgIcon("feishu.png", size) } 24 + func (feishuHandler) Icon(size int) template.HTML { return shared.ImgIcon("feishu.png", size) } 24 25 25 - func (feishuHandler) FormFields(bind func(any)) FormFields { 26 + func (feishuHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 26 27 var form struct { 27 - forms.WebhookForm 28 + forms.WebhookCoreForm 28 29 PayloadURL string `binding:"Required;ValidUrl"` 29 30 } 30 31 bind(&form) 31 32 32 - return FormFields{ 33 - WebhookForm: form.WebhookForm, 34 - URL: form.PayloadURL, 35 - ContentType: webhook_model.ContentTypeJSON, 36 - Secret: "", 37 - HTTPMethod: http.MethodPost, 38 - Metadata: nil, 33 + return forms.WebhookForm{ 34 + WebhookCoreForm: form.WebhookCoreForm, 35 + URL: form.PayloadURL, 36 + ContentType: webhook_model.ContentTypeJSON, 37 + Secret: "", 38 + HTTPMethod: http.MethodPost, 39 + Metadata: nil, 39 40 } 40 41 } 41 42 ··· 192 193 193 194 type feishuConvertor struct{} 194 195 195 - var _ payloadConvertor[FeishuPayload] = feishuConvertor{} 196 + var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{} 196 197 197 198 func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 198 - return newJSONRequest(feishuConvertor{}, w, t, true) 199 + return shared.NewJSONRequest(feishuConvertor{}, w, t, true) 199 200 }
-8
services/webhook/general.go
··· 6 6 import ( 7 7 "fmt" 8 8 "html" 9 - "html/template" 10 9 "net/url" 11 - "strconv" 12 10 "strings" 13 11 14 12 webhook_model "code.gitea.io/gitea/models/webhook" ··· 354 352 Created: w.CreatedUnix.AsTime(), 355 353 }, nil 356 354 } 357 - 358 - func imgIcon(name string, size int) template.HTML { 359 - s := strconv.Itoa(size) 360 - src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name) 361 - return template.HTML(`<img width="` + s + `" height="` + s + `" src="` + src + `">`) 362 - }
+11 -10
services/webhook/gogs.go
··· 10 10 webhook_model "code.gitea.io/gitea/models/webhook" 11 11 webhook_module "code.gitea.io/gitea/modules/webhook" 12 12 "code.gitea.io/gitea/services/forms" 13 + "code.gitea.io/gitea/services/webhook/shared" 13 14 ) 14 15 15 16 type gogsHandler struct{ defaultHandler } 16 17 17 18 func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS } 18 - func (gogsHandler) Icon(size int) template.HTML { return imgIcon("gogs.ico", size) } 19 + func (gogsHandler) Icon(size int) template.HTML { return shared.ImgIcon("gogs.ico", size) } 19 20 20 - func (gogsHandler) FormFields(bind func(any)) FormFields { 21 + func (gogsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 21 22 var form struct { 22 - forms.WebhookForm 23 + forms.WebhookCoreForm 23 24 PayloadURL string `binding:"Required;ValidUrl"` 24 25 ContentType int `binding:"Required"` 25 26 Secret string ··· 30 31 if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { 31 32 contentType = webhook_model.ContentTypeForm 32 33 } 33 - return FormFields{ 34 - WebhookForm: form.WebhookForm, 35 - URL: form.PayloadURL, 36 - ContentType: contentType, 37 - Secret: form.Secret, 38 - HTTPMethod: http.MethodPost, 39 - Metadata: nil, 34 + return forms.WebhookForm{ 35 + WebhookCoreForm: form.WebhookCoreForm, 36 + URL: form.PayloadURL, 37 + ContentType: contentType, 38 + Secret: form.Secret, 39 + HTTPMethod: http.MethodPost, 40 + Metadata: nil, 40 41 } 41 42 }
+13 -12
services/webhook/matrix.go
··· 25 25 "code.gitea.io/gitea/modules/util" 26 26 webhook_module "code.gitea.io/gitea/modules/webhook" 27 27 "code.gitea.io/gitea/services/forms" 28 + "code.gitea.io/gitea/services/webhook/shared" 28 29 ) 29 30 30 31 type matrixHandler struct{} ··· 35 36 return svg.RenderHTML("gitea-matrix", size, "img") 36 37 } 37 38 38 - func (matrixHandler) FormFields(bind func(any)) FormFields { 39 + func (matrixHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 39 40 var form struct { 40 - forms.WebhookForm 41 + forms.WebhookCoreForm 41 42 HomeserverURL string `binding:"Required;ValidUrl"` 42 43 RoomID string `binding:"Required"` 43 44 MessageType int 44 45 45 46 // enforce requirement of authorization_header 46 - // (value will still be set in the embedded WebhookForm) 47 + // (value will still be set in the embedded WebhookCoreForm) 47 48 AuthorizationHeader string `binding:"Required"` 48 49 } 49 50 bind(&form) 50 51 51 - return FormFields{ 52 - WebhookForm: form.WebhookForm, 53 - URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), 54 - ContentType: webhook_model.ContentTypeJSON, 55 - Secret: "", 56 - HTTPMethod: http.MethodPut, 52 + return forms.WebhookForm{ 53 + WebhookCoreForm: form.WebhookCoreForm, 54 + URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), 55 + ContentType: webhook_model.ContentTypeJSON, 56 + Secret: "", 57 + HTTPMethod: http.MethodPut, 57 58 Metadata: &MatrixMeta{ 58 59 HomeserverURL: form.HomeserverURL, 59 60 Room: form.RoomID, ··· 70 71 mc := matrixConvertor{ 71 72 MsgType: messageTypeText[meta.MessageType], 72 73 } 73 - payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType) 74 + payload, err := shared.NewPayload(mc, []byte(t.PayloadContent), t.EventType) 74 75 if err != nil { 75 76 return nil, nil, err 76 77 } ··· 90 91 } 91 92 req.Header.Set("Content-Type", "application/json") 92 93 93 - return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially 94 + return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially 94 95 } 95 96 96 97 const matrixPayloadSizeLimit = 1024 * 64 ··· 125 126 Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` 126 127 } 127 128 128 - var _ payloadConvertor[MatrixPayload] = matrixConvertor{} 129 + var _ shared.PayloadConvertor[MatrixPayload] = matrixConvertor{} 129 130 130 131 type matrixConvertor struct { 131 132 MsgType string
+13 -12
services/webhook/msteams.go
··· 17 17 "code.gitea.io/gitea/modules/util" 18 18 webhook_module "code.gitea.io/gitea/modules/webhook" 19 19 "code.gitea.io/gitea/services/forms" 20 + "code.gitea.io/gitea/services/webhook/shared" 20 21 ) 21 22 22 23 type msteamsHandler struct{} 23 24 24 25 func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS } 25 26 func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } 26 - func (msteamsHandler) Icon(size int) template.HTML { return imgIcon("msteams.png", size) } 27 + func (msteamsHandler) Icon(size int) template.HTML { return shared.ImgIcon("msteams.png", size) } 27 28 28 - func (msteamsHandler) FormFields(bind func(any)) FormFields { 29 + func (msteamsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 29 30 var form struct { 30 - forms.WebhookForm 31 + forms.WebhookCoreForm 31 32 PayloadURL string `binding:"Required;ValidUrl"` 32 33 } 33 34 bind(&form) 34 35 35 - return FormFields{ 36 - WebhookForm: form.WebhookForm, 37 - URL: form.PayloadURL, 38 - ContentType: webhook_model.ContentTypeJSON, 39 - Secret: "", 40 - HTTPMethod: http.MethodPost, 41 - Metadata: nil, 36 + return forms.WebhookForm{ 37 + WebhookCoreForm: form.WebhookCoreForm, 38 + URL: form.PayloadURL, 39 + ContentType: webhook_model.ContentTypeJSON, 40 + Secret: "", 41 + HTTPMethod: http.MethodPost, 42 + Metadata: nil, 42 43 } 43 44 } 44 45 ··· 370 371 371 372 type msteamsConvertor struct{} 372 373 373 - var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} 374 + var _ shared.PayloadConvertor[MSTeamsPayload] = msteamsConvertor{} 374 375 375 376 func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 376 - return newJSONRequest(msteamsConvertor{}, w, t, true) 377 + return shared.NewJSONRequest(msteamsConvertor{}, w, t, true) 377 378 }
+11 -10
services/webhook/packagist.go
··· 15 15 "code.gitea.io/gitea/modules/log" 16 16 webhook_module "code.gitea.io/gitea/modules/webhook" 17 17 "code.gitea.io/gitea/services/forms" 18 + "code.gitea.io/gitea/services/webhook/shared" 18 19 ) 19 20 20 21 type packagistHandler struct{} 21 22 22 23 func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } 23 - func (packagistHandler) Icon(size int) template.HTML { return imgIcon("packagist.png", size) } 24 + func (packagistHandler) Icon(size int) template.HTML { return shared.ImgIcon("packagist.png", size) } 24 25 25 - func (packagistHandler) FormFields(bind func(any)) FormFields { 26 + func (packagistHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 26 27 var form struct { 27 - forms.WebhookForm 28 + forms.WebhookCoreForm 28 29 Username string `binding:"Required"` 29 30 APIToken string `binding:"Required"` 30 31 PackageURL string `binding:"Required;ValidUrl"` 31 32 } 32 33 bind(&form) 33 34 34 - return FormFields{ 35 - WebhookForm: form.WebhookForm, 36 - URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), 37 - ContentType: webhook_model.ContentTypeJSON, 38 - Secret: "", 39 - HTTPMethod: http.MethodPost, 35 + return forms.WebhookForm{ 36 + WebhookCoreForm: form.WebhookCoreForm, 37 + URL: fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), 38 + ContentType: webhook_model.ContentTypeJSON, 39 + Secret: "", 40 + HTTPMethod: http.MethodPost, 40 41 Metadata: &PackagistMeta{ 41 42 Username: form.Username, 42 43 APIToken: form.APIToken, ··· 85 86 URL: meta.PackageURL, 86 87 }, 87 88 } 88 - return newJSONRequestWithPayload(payload, w, t, false) 89 + return shared.NewJSONRequestWithPayload(payload, w, t, false) 89 90 }
+55 -9
services/webhook/payloader.go services/webhook/shared/payloader.go
··· 1 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - package webhook 4 + package shared 5 5 6 6 import ( 7 7 "bytes" 8 + "crypto/hmac" 9 + "crypto/sha1" 10 + "crypto/sha256" 11 + "encoding/hex" 12 + "errors" 8 13 "fmt" 14 + "io" 9 15 "net/http" 10 16 11 17 webhook_model "code.gitea.io/gitea/models/webhook" ··· 14 20 webhook_module "code.gitea.io/gitea/modules/webhook" 15 21 ) 16 22 17 - // payloadConvertor defines the interface to convert system payload to webhook payload 18 - type payloadConvertor[T any] interface { 23 + var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event") 24 + 25 + // PayloadConvertor defines the interface to convert system payload to webhook payload 26 + type PayloadConvertor[T any] interface { 19 27 Create(*api.CreatePayload) (T, error) 20 28 Delete(*api.DeletePayload) (T, error) 21 29 Fork(*api.ForkPayload) (T, error) ··· 39 47 return convert(p) 40 48 } 41 49 42 - func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { 50 + func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { 43 51 switch event { 44 52 case webhook_module.HookEventCreate: 45 53 return convertUnmarshalledJSON(rc.Create, data) ··· 83 91 return t, fmt.Errorf("newPayload unsupported event: %s", event) 84 92 } 85 93 86 - func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { 87 - payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType) 94 + func NewJSONRequest[T any](pc PayloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { 95 + payload, err := NewPayload(pc, []byte(t.PayloadContent), t.EventType) 88 96 if err != nil { 89 97 return nil, nil, err 90 98 } 91 - return newJSONRequestWithPayload(payload, w, t, withDefaultHeaders) 99 + return NewJSONRequestWithPayload(payload, w, t, withDefaultHeaders) 92 100 } 93 101 94 - func newJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { 102 + func NewJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { 95 103 body, err := json.MarshalIndent(payload, "", " ") 96 104 if err != nil { 97 105 return nil, nil, err ··· 109 117 req.Header.Set("Content-Type", "application/json") 110 118 111 119 if withDefaultHeaders { 112 - return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) 120 + return req, body, AddDefaultHeaders(req, []byte(w.Secret), t, body) 113 121 } 114 122 return req, body, nil 115 123 } 124 + 125 + // AddDefaultHeaders adds the X-Forgejo, X-Gitea, X-Gogs, X-Hub, X-GitHub headers to the given request 126 + func AddDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { 127 + var signatureSHA1 string 128 + var signatureSHA256 string 129 + if len(secret) > 0 { 130 + sig1 := hmac.New(sha1.New, secret) 131 + sig256 := hmac.New(sha256.New, secret) 132 + _, err := io.MultiWriter(sig1, sig256).Write(payloadContent) 133 + if err != nil { 134 + // this error should never happen, since the hashes are writing to []byte and always return a nil error. 135 + return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) 136 + } 137 + signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) 138 + signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) 139 + } 140 + 141 + event := t.EventType.Event() 142 + eventType := string(t.EventType) 143 + req.Header.Add("X-Forgejo-Delivery", t.UUID) 144 + req.Header.Add("X-Forgejo-Event", event) 145 + req.Header.Add("X-Forgejo-Event-Type", eventType) 146 + req.Header.Add("X-Forgejo-Signature", signatureSHA256) 147 + req.Header.Add("X-Gitea-Delivery", t.UUID) 148 + req.Header.Add("X-Gitea-Event", event) 149 + req.Header.Add("X-Gitea-Event-Type", eventType) 150 + req.Header.Add("X-Gitea-Signature", signatureSHA256) 151 + req.Header.Add("X-Gogs-Delivery", t.UUID) 152 + req.Header.Add("X-Gogs-Event", event) 153 + req.Header.Add("X-Gogs-Event-Type", eventType) 154 + req.Header.Add("X-Gogs-Signature", signatureSHA256) 155 + req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) 156 + req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) 157 + req.Header["X-GitHub-Delivery"] = []string{t.UUID} 158 + req.Header["X-GitHub-Event"] = []string{event} 159 + req.Header["X-GitHub-Event-Type"] = []string{eventType} 160 + return nil 161 + }
+15
services/webhook/shared/img.go
··· 1 + package shared 2 + 3 + import ( 4 + "html" 5 + "html/template" 6 + "strconv" 7 + 8 + "code.gitea.io/gitea/modules/setting" 9 + ) 10 + 11 + func ImgIcon(name string, size int) template.HTML { 12 + s := strconv.Itoa(size) 13 + src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name) 14 + return template.HTML(`<img width="` + s + `" height="` + s + `" src="` + src + `">`) 15 + }
+12 -11
services/webhook/slack.go
··· 20 20 webhook_module "code.gitea.io/gitea/modules/webhook" 21 21 gitea_context "code.gitea.io/gitea/services/context" 22 22 "code.gitea.io/gitea/services/forms" 23 + "code.gitea.io/gitea/services/webhook/shared" 23 24 24 25 "gitea.com/go-chi/binding" 25 26 ) ··· 27 28 type slackHandler struct{} 28 29 29 30 func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } 30 - func (slackHandler) Icon(size int) template.HTML { return imgIcon("slack.png", size) } 31 + func (slackHandler) Icon(size int) template.HTML { return shared.ImgIcon("slack.png", size) } 31 32 32 33 type slackForm struct { 33 - forms.WebhookForm 34 + forms.WebhookCoreForm 34 35 PayloadURL string `binding:"Required;ValidUrl"` 35 36 Channel string `binding:"Required"` 36 37 Username string ··· 53 54 return errs 54 55 } 55 56 56 - func (slackHandler) FormFields(bind func(any)) FormFields { 57 + func (slackHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 57 58 var form slackForm 58 59 bind(&form) 59 60 60 - return FormFields{ 61 - WebhookForm: form.WebhookForm, 62 - URL: form.PayloadURL, 63 - ContentType: webhook_model.ContentTypeJSON, 64 - Secret: "", 65 - HTTPMethod: http.MethodPost, 61 + return forms.WebhookForm{ 62 + WebhookCoreForm: form.WebhookCoreForm, 63 + URL: form.PayloadURL, 64 + ContentType: webhook_model.ContentTypeJSON, 65 + Secret: "", 66 + HTTPMethod: http.MethodPost, 66 67 Metadata: &SlackMeta{ 67 68 Channel: strings.TrimSpace(form.Channel), 68 69 Username: form.Username, ··· 334 335 Color string 335 336 } 336 337 337 - var _ payloadConvertor[SlackPayload] = slackConvertor{} 338 + var _ shared.PayloadConvertor[SlackPayload] = slackConvertor{} 338 339 339 340 func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 340 341 meta := &SlackMeta{} ··· 347 348 IconURL: meta.IconURL, 348 349 Color: meta.Color, 349 350 } 350 - return newJSONRequest(sc, w, t, true) 351 + return shared.NewJSONRequest(sc, w, t, true) 351 352 } 352 353 353 354 var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`)
+312
services/webhook/sourcehut/builds.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package sourcehut 5 + 6 + import ( 7 + "cmp" 8 + "context" 9 + "fmt" 10 + "html/template" 11 + "io/fs" 12 + "net/http" 13 + "strings" 14 + 15 + webhook_model "code.gitea.io/gitea/models/webhook" 16 + "code.gitea.io/gitea/modules/git" 17 + "code.gitea.io/gitea/modules/gitrepo" 18 + "code.gitea.io/gitea/modules/json" 19 + "code.gitea.io/gitea/modules/log" 20 + "code.gitea.io/gitea/modules/setting" 21 + api "code.gitea.io/gitea/modules/structs" 22 + webhook_module "code.gitea.io/gitea/modules/webhook" 23 + gitea_context "code.gitea.io/gitea/services/context" 24 + "code.gitea.io/gitea/services/forms" 25 + "code.gitea.io/gitea/services/webhook/shared" 26 + 27 + "gitea.com/go-chi/binding" 28 + "gopkg.in/yaml.v3" 29 + ) 30 + 31 + type BuildsHandler struct{} 32 + 33 + func (BuildsHandler) Type() webhook_module.HookType { return webhook_module.SOURCEHUT_BUILDS } 34 + func (BuildsHandler) Metadata(w *webhook_model.Webhook) any { 35 + s := &BuildsMeta{} 36 + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { 37 + log.Error("sourcehut.BuildsHandler.Metadata(%d): %v", w.ID, err) 38 + } 39 + return s 40 + } 41 + 42 + func (BuildsHandler) Icon(size int) template.HTML { 43 + return shared.ImgIcon("sourcehut.svg", size) 44 + } 45 + 46 + type buildsForm struct { 47 + forms.WebhookCoreForm 48 + PayloadURL string `binding:"Required;ValidUrl"` 49 + ManifestPath string `binding:"Required"` 50 + Visibility string `binding:"Required;In(PUBLIC,UNLISTED,PRIVATE)"` 51 + Secrets bool 52 + } 53 + 54 + var _ binding.Validator = &buildsForm{} 55 + 56 + // Validate implements binding.Validator. 57 + func (f *buildsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { 58 + ctx := gitea_context.GetWebContext(req) 59 + if !fs.ValidPath(f.ManifestPath) { 60 + errs = append(errs, binding.Error{ 61 + FieldNames: []string{"ManifestPath"}, 62 + Classification: "", 63 + Message: ctx.Locale.TrString("repo.settings.add_webhook.invalid_path"), 64 + }) 65 + } 66 + if !strings.HasPrefix(f.AuthorizationHeader, "Bearer ") { 67 + errs = append(errs, binding.Error{ 68 + FieldNames: []string{"AuthorizationHeader"}, 69 + Classification: "", 70 + Message: ctx.Locale.TrString("form.required_prefix", "Bearer "), 71 + }) 72 + } 73 + return errs 74 + } 75 + 76 + func (BuildsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 77 + var form buildsForm 78 + bind(&form) 79 + 80 + return forms.WebhookForm{ 81 + WebhookCoreForm: form.WebhookCoreForm, 82 + URL: form.PayloadURL, 83 + ContentType: webhook_model.ContentTypeJSON, 84 + Secret: "", 85 + HTTPMethod: http.MethodPost, 86 + Metadata: &BuildsMeta{ 87 + ManifestPath: form.ManifestPath, 88 + Visibility: form.Visibility, 89 + Secrets: form.Secrets, 90 + }, 91 + } 92 + } 93 + 94 + type ( 95 + graphqlPayload[V any] struct { 96 + Query string `json:"query,omitempty"` 97 + Error string `json:"error,omitempty"` 98 + Variables V `json:"variables,omitempty"` 99 + } 100 + // buildsVariables according to https://man.sr.ht/builds.sr.ht/graphql.md 101 + buildsVariables struct { 102 + Manifest string `json:"manifest"` 103 + Tags []string `json:"tags"` 104 + Note string `json:"note"` 105 + Secrets bool `json:"secrets"` 106 + Execute bool `json:"execute"` 107 + Visibility string `json:"visibility"` 108 + } 109 + 110 + // BuildsMeta contains the metadata for the webhook 111 + BuildsMeta struct { 112 + ManifestPath string `json:"manifest_path"` 113 + Visibility string `json:"visibility"` 114 + Secrets bool `json:"secrets"` 115 + } 116 + ) 117 + 118 + type sourcehutConvertor struct { 119 + ctx context.Context 120 + meta BuildsMeta 121 + } 122 + 123 + var _ shared.PayloadConvertor[graphqlPayload[buildsVariables]] = sourcehutConvertor{} 124 + 125 + func (BuildsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 126 + meta := BuildsMeta{} 127 + if err := json.Unmarshal([]byte(w.Meta), &meta); err != nil { 128 + return nil, nil, fmt.Errorf("newSourcehutRequest meta json: %w", err) 129 + } 130 + pc := sourcehutConvertor{ 131 + ctx: ctx, 132 + meta: meta, 133 + } 134 + return shared.NewJSONRequest(pc, w, t, false) 135 + } 136 + 137 + // Create implements PayloadConvertor Create method 138 + func (pc sourcehutConvertor) Create(p *api.CreatePayload) (graphqlPayload[buildsVariables], error) { 139 + return pc.newPayload(p.Repo, p.Sha, p.Ref, p.RefType+" "+git.RefName(p.Ref).ShortName()+" created", true) 140 + } 141 + 142 + // Delete implements PayloadConvertor Delete method 143 + func (pc sourcehutConvertor) Delete(_ *api.DeletePayload) (graphqlPayload[buildsVariables], error) { 144 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 145 + } 146 + 147 + // Fork implements PayloadConvertor Fork method 148 + func (pc sourcehutConvertor) Fork(_ *api.ForkPayload) (graphqlPayload[buildsVariables], error) { 149 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 150 + } 151 + 152 + // Push implements PayloadConvertor Push method 153 + func (pc sourcehutConvertor) Push(p *api.PushPayload) (graphqlPayload[buildsVariables], error) { 154 + return pc.newPayload(p.Repo, p.HeadCommit.ID, p.Ref, p.HeadCommit.Message, true) 155 + } 156 + 157 + // Issue implements PayloadConvertor Issue method 158 + func (pc sourcehutConvertor) Issue(_ *api.IssuePayload) (graphqlPayload[buildsVariables], error) { 159 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 160 + } 161 + 162 + // IssueComment implements PayloadConvertor IssueComment method 163 + func (pc sourcehutConvertor) IssueComment(_ *api.IssueCommentPayload) (graphqlPayload[buildsVariables], error) { 164 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 165 + } 166 + 167 + // PullRequest implements PayloadConvertor PullRequest method 168 + func (pc sourcehutConvertor) PullRequest(_ *api.PullRequestPayload) (graphqlPayload[buildsVariables], error) { 169 + // TODO 170 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 171 + } 172 + 173 + // Review implements PayloadConvertor Review method 174 + func (pc sourcehutConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (graphqlPayload[buildsVariables], error) { 175 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 176 + } 177 + 178 + // Repository implements PayloadConvertor Repository method 179 + func (pc sourcehutConvertor) Repository(_ *api.RepositoryPayload) (graphqlPayload[buildsVariables], error) { 180 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 181 + } 182 + 183 + // Wiki implements PayloadConvertor Wiki method 184 + func (pc sourcehutConvertor) Wiki(_ *api.WikiPayload) (graphqlPayload[buildsVariables], error) { 185 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 186 + } 187 + 188 + // Release implements PayloadConvertor Release method 189 + func (pc sourcehutConvertor) Release(_ *api.ReleasePayload) (graphqlPayload[buildsVariables], error) { 190 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 191 + } 192 + 193 + func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buildsVariables], error) { 194 + return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported 195 + } 196 + 197 + // mustBuildManifest adjusts the manifest to submit to the builds service 198 + // 199 + // in case of an error the Error field will be set, to be visible by the end-user under recent deliveries 200 + func (pc sourcehutConvertor) newPayload(repo *api.Repository, commitID, ref, note string, trusted bool) (graphqlPayload[buildsVariables], error) { 201 + manifest, err := pc.buildManifest(repo, commitID, ref) 202 + if err != nil { 203 + if len(manifest) == 0 { 204 + return graphqlPayload[buildsVariables]{}, err 205 + } 206 + // the manifest contains an error for the user: log the actual error and construct the payload 207 + // the error will be visible under the "recent deliveries" of the webhook settings. 208 + log.Warn("sourcehut.builds: could not construct manifest for %s: %v", repo.FullName, err) 209 + msg := fmt.Sprintf("%s:%s %s", repo.FullName, ref, manifest) 210 + return graphqlPayload[buildsVariables]{ 211 + Error: msg, 212 + }, nil 213 + } 214 + 215 + gitRef := git.RefName(ref) 216 + return graphqlPayload[buildsVariables]{ 217 + Query: `mutation ( 218 + $manifest: String! 219 + $tags: [String!] 220 + $note: String! 221 + $secrets: Boolean! 222 + $execute: Boolean! 223 + $visibility: Visibility! 224 + ) { 225 + submit( 226 + manifest: $manifest 227 + tags: $tags 228 + note: $note 229 + secrets: $secrets 230 + execute: $execute 231 + visibility: $visibility 232 + ) { 233 + id 234 + } 235 + }`, Variables: buildsVariables{ 236 + Manifest: string(manifest), 237 + Tags: []string{repo.FullName, gitRef.RefType() + "/" + gitRef.ShortName(), pc.meta.ManifestPath}, 238 + Note: note, 239 + Secrets: pc.meta.Secrets && trusted, 240 + Execute: trusted, 241 + Visibility: cmp.Or(pc.meta.Visibility, "PRIVATE"), 242 + }, 243 + }, nil 244 + } 245 + 246 + // buildManifest adjusts the manifest to submit to the builds service 247 + // in case of an error the []byte might contain an error that can be displayed to the user 248 + func (pc sourcehutConvertor) buildManifest(repo *api.Repository, commitID, gitRef string) ([]byte, error) { 249 + gitRepo, err := gitrepo.OpenRepository(pc.ctx, repo) 250 + if err != nil { 251 + msg := "could not open repository" 252 + return []byte(msg), fmt.Errorf(msg+": %w", err) 253 + } 254 + defer gitRepo.Close() 255 + 256 + commit, err := gitRepo.GetCommit(commitID) 257 + if err != nil { 258 + msg := fmt.Sprintf("could not get commit %q", commitID) 259 + return []byte(msg), fmt.Errorf(msg+": %w", err) 260 + } 261 + entry, err := commit.GetTreeEntryByPath(pc.meta.ManifestPath) 262 + if err != nil { 263 + msg := fmt.Sprintf("could not open manifest %q", pc.meta.ManifestPath) 264 + return []byte(msg), fmt.Errorf(msg+": %w", err) 265 + } 266 + r, err := entry.Blob().DataAsync() 267 + if err != nil { 268 + msg := fmt.Sprintf("could not read manifest %q", pc.meta.ManifestPath) 269 + return []byte(msg), fmt.Errorf(msg+": %w", err) 270 + } 271 + defer r.Close() 272 + var manifest struct { 273 + Image string `yaml:"image"` 274 + Arch string `yaml:"arch,omitempty"` 275 + Packages []string `yaml:"packages,omitempty"` 276 + Repositories map[string]string `yaml:"repositories,omitempty"` 277 + Artifacts []string `yaml:"artifacts,omitempty"` 278 + Shell bool `yaml:"shell,omitempty"` 279 + Sources []string `yaml:"sources"` 280 + Tasks []map[string]string `yaml:"tasks"` 281 + Triggers []string `yaml:"triggers,omitempty"` 282 + Environment map[string]string `yaml:"environment"` 283 + Secrets []string `yaml:"secrets,omitempty"` 284 + Oauth string `yaml:"oauth,omitempty"` 285 + } 286 + if err := yaml.NewDecoder(r).Decode(&manifest); err != nil { 287 + msg := fmt.Sprintf("could not decode manifest %q", pc.meta.ManifestPath) 288 + return []byte(msg), fmt.Errorf(msg+": %w", err) 289 + } 290 + 291 + if manifest.Environment == nil { 292 + manifest.Environment = make(map[string]string) 293 + } 294 + manifest.Environment["BUILD_SUBMITTER"] = "forgejo" 295 + manifest.Environment["BUILD_SUBMITTER_URL"] = setting.AppURL 296 + manifest.Environment["GIT_REF"] = gitRef 297 + 298 + source := repo.CloneURL + "#" + commitID 299 + found := false 300 + for i, s := range manifest.Sources { 301 + if s == repo.CloneURL { 302 + manifest.Sources[i] = source 303 + found = true 304 + break 305 + } 306 + } 307 + if !found { 308 + manifest.Sources = append(manifest.Sources, source) 309 + } 310 + 311 + return yaml.Marshal(manifest) 312 + }
+440
services/webhook/sourcehut/builds_test.go
··· 1 + // Copyright 2024 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package sourcehut 5 + 6 + import ( 7 + "context" 8 + "testing" 9 + "time" 10 + 11 + "code.gitea.io/gitea/models/db" 12 + repo_model "code.gitea.io/gitea/models/repo" 13 + unit_model "code.gitea.io/gitea/models/unit" 14 + user_model "code.gitea.io/gitea/models/user" 15 + webhook_model "code.gitea.io/gitea/models/webhook" 16 + "code.gitea.io/gitea/modules/git" 17 + "code.gitea.io/gitea/modules/json" 18 + "code.gitea.io/gitea/modules/setting" 19 + api "code.gitea.io/gitea/modules/structs" 20 + "code.gitea.io/gitea/modules/test" 21 + webhook_module "code.gitea.io/gitea/modules/webhook" 22 + repo_service "code.gitea.io/gitea/services/repository" 23 + files_service "code.gitea.io/gitea/services/repository/files" 24 + "code.gitea.io/gitea/services/webhook/shared" 25 + 26 + "github.com/stretchr/testify/assert" 27 + "github.com/stretchr/testify/require" 28 + ) 29 + 30 + func gitInit(t testing.TB) { 31 + if setting.Git.HomePath != "" { 32 + return 33 + } 34 + t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) 35 + assert.NoError(t, git.InitSimple(context.Background())) 36 + } 37 + 38 + func TestSourcehutBuildsPayload(t *testing.T) { 39 + gitInit(t) 40 + defer test.MockVariableValue(&setting.RepoRootPath, ".")() 41 + defer test.MockVariableValue(&setting.AppURL, "https://example.forgejo.org/")() 42 + 43 + repo := &api.Repository{ 44 + HTMLURL: "http://localhost:3000/testdata/repo", 45 + Name: "repo", 46 + FullName: "testdata/repo", 47 + Owner: &api.User{ 48 + UserName: "testdata", 49 + }, 50 + CloneURL: "http://localhost:3000/testdata/repo.git", 51 + } 52 + 53 + pc := sourcehutConvertor{ 54 + ctx: git.DefaultContext, 55 + meta: BuildsMeta{ 56 + ManifestPath: "adjust me in each test", 57 + Visibility: "UNLISTED", 58 + Secrets: true, 59 + }, 60 + } 61 + t.Run("Create/branch", func(t *testing.T) { 62 + p := &api.CreatePayload{ 63 + Sha: "58771003157b81abc6bf41df0c5db4147a3e3c83", 64 + Ref: "refs/heads/test", 65 + RefType: "branch", 66 + Repo: repo, 67 + } 68 + 69 + pc.meta.ManifestPath = "simple.yml" 70 + pl, err := pc.Create(p) 71 + require.NoError(t, err) 72 + assert.Equal(t, buildsVariables{ 73 + Manifest: `image: alpine/edge 74 + sources: 75 + - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 76 + tasks: 77 + - say-hello: | 78 + echo hello 79 + - say-world: echo world 80 + environment: 81 + BUILD_SUBMITTER: forgejo 82 + BUILD_SUBMITTER_URL: https://example.forgejo.org/ 83 + GIT_REF: refs/heads/test 84 + `, 85 + Note: "branch test created", 86 + Tags: []string{"testdata/repo", "branch/test", "simple.yml"}, 87 + Secrets: true, 88 + Execute: true, 89 + Visibility: "UNLISTED", 90 + }, pl.Variables) 91 + }) 92 + t.Run("Create/tag", func(t *testing.T) { 93 + p := &api.CreatePayload{ 94 + Sha: "58771003157b81abc6bf41df0c5db4147a3e3c83", 95 + Ref: "refs/tags/v1.0.0", 96 + RefType: "tag", 97 + Repo: repo, 98 + } 99 + 100 + pc.meta.ManifestPath = "simple.yml" 101 + pl, err := pc.Create(p) 102 + require.NoError(t, err) 103 + assert.Equal(t, buildsVariables{ 104 + Manifest: `image: alpine/edge 105 + sources: 106 + - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 107 + tasks: 108 + - say-hello: | 109 + echo hello 110 + - say-world: echo world 111 + environment: 112 + BUILD_SUBMITTER: forgejo 113 + BUILD_SUBMITTER_URL: https://example.forgejo.org/ 114 + GIT_REF: refs/tags/v1.0.0 115 + `, 116 + Note: "tag v1.0.0 created", 117 + Tags: []string{"testdata/repo", "tag/v1.0.0", "simple.yml"}, 118 + Secrets: true, 119 + Execute: true, 120 + Visibility: "UNLISTED", 121 + }, pl.Variables) 122 + }) 123 + 124 + t.Run("Delete", func(t *testing.T) { 125 + p := &api.DeletePayload{} 126 + 127 + pl, err := pc.Delete(p) 128 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 129 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 130 + }) 131 + 132 + t.Run("Fork", func(t *testing.T) { 133 + p := &api.ForkPayload{} 134 + 135 + pl, err := pc.Fork(p) 136 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 137 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 138 + }) 139 + 140 + t.Run("Push/simple", func(t *testing.T) { 141 + p := &api.PushPayload{ 142 + Ref: "refs/heads/main", 143 + HeadCommit: &api.PayloadCommit{ 144 + ID: "58771003157b81abc6bf41df0c5db4147a3e3c83", 145 + Message: "add simple", 146 + }, 147 + Repo: repo, 148 + } 149 + 150 + pc.meta.ManifestPath = "simple.yml" 151 + pl, err := pc.Push(p) 152 + require.NoError(t, err) 153 + 154 + assert.Equal(t, buildsVariables{ 155 + Manifest: `image: alpine/edge 156 + sources: 157 + - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 158 + tasks: 159 + - say-hello: | 160 + echo hello 161 + - say-world: echo world 162 + environment: 163 + BUILD_SUBMITTER: forgejo 164 + BUILD_SUBMITTER_URL: https://example.forgejo.org/ 165 + GIT_REF: refs/heads/main 166 + `, 167 + Note: "add simple", 168 + Tags: []string{"testdata/repo", "branch/main", "simple.yml"}, 169 + Secrets: true, 170 + Execute: true, 171 + Visibility: "UNLISTED", 172 + }, pl.Variables) 173 + }) 174 + t.Run("Push/complex", func(t *testing.T) { 175 + p := &api.PushPayload{ 176 + Ref: "refs/heads/main", 177 + HeadCommit: &api.PayloadCommit{ 178 + ID: "69b217caa89166a02b8cd368b64fb83a44720e14", 179 + Message: "replace simple with complex", 180 + }, 181 + Repo: repo, 182 + } 183 + 184 + pc.meta.ManifestPath = "complex.yaml" 185 + pc.meta.Visibility = "PRIVATE" 186 + pc.meta.Secrets = false 187 + pl, err := pc.Push(p) 188 + require.NoError(t, err) 189 + 190 + assert.Equal(t, buildsVariables{ 191 + Manifest: `image: archlinux 192 + packages: 193 + - nodejs 194 + - npm 195 + - rsync 196 + sources: 197 + - http://localhost:3000/testdata/repo.git#69b217caa89166a02b8cd368b64fb83a44720e14 198 + tasks: [] 199 + environment: 200 + BUILD_SUBMITTER: forgejo 201 + BUILD_SUBMITTER_URL: https://example.forgejo.org/ 202 + GIT_REF: refs/heads/main 203 + deploy: synapse@synapse-bt.org 204 + secrets: 205 + - 7ebab768-e5e4-4c9d-ba57-ec41a72c5665 206 + `, 207 + Note: "replace simple with complex", 208 + Tags: []string{"testdata/repo", "branch/main", "complex.yaml"}, 209 + Secrets: false, 210 + Execute: true, 211 + Visibility: "PRIVATE", 212 + }, pl.Variables) 213 + }) 214 + 215 + t.Run("Push/error", func(t *testing.T) { 216 + p := &api.PushPayload{ 217 + Ref: "refs/heads/main", 218 + HeadCommit: &api.PayloadCommit{ 219 + ID: "58771003157b81abc6bf41df0c5db4147a3e3c83", 220 + Message: "add simple", 221 + }, 222 + Repo: repo, 223 + } 224 + 225 + pc.meta.ManifestPath = "non-existing.yml" 226 + pl, err := pc.Push(p) 227 + require.NoError(t, err) 228 + 229 + assert.Equal(t, graphqlPayload[buildsVariables]{ 230 + Error: "testdata/repo:refs/heads/main could not open manifest \"non-existing.yml\"", 231 + }, pl) 232 + }) 233 + 234 + t.Run("Issue", func(t *testing.T) { 235 + p := &api.IssuePayload{} 236 + 237 + p.Action = api.HookIssueOpened 238 + pl, err := pc.Issue(p) 239 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 240 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 241 + 242 + p.Action = api.HookIssueClosed 243 + pl, err = pc.Issue(p) 244 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 245 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 246 + }) 247 + 248 + t.Run("IssueComment", func(t *testing.T) { 249 + p := &api.IssueCommentPayload{} 250 + 251 + pl, err := pc.IssueComment(p) 252 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 253 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 254 + }) 255 + 256 + t.Run("PullRequest", func(t *testing.T) { 257 + p := &api.PullRequestPayload{} 258 + 259 + pl, err := pc.PullRequest(p) 260 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 261 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 262 + }) 263 + 264 + t.Run("PullRequestComment", func(t *testing.T) { 265 + p := &api.IssueCommentPayload{ 266 + IsPull: true, 267 + } 268 + 269 + pl, err := pc.IssueComment(p) 270 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 271 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 272 + }) 273 + 274 + t.Run("Review", func(t *testing.T) { 275 + p := &api.PullRequestPayload{} 276 + p.Action = api.HookIssueReviewed 277 + 278 + pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) 279 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 280 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 281 + }) 282 + 283 + t.Run("Repository", func(t *testing.T) { 284 + p := &api.RepositoryPayload{} 285 + 286 + pl, err := pc.Repository(p) 287 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 288 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 289 + }) 290 + 291 + t.Run("Package", func(t *testing.T) { 292 + p := &api.PackagePayload{} 293 + 294 + pl, err := pc.Package(p) 295 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 296 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 297 + }) 298 + 299 + t.Run("Wiki", func(t *testing.T) { 300 + p := &api.WikiPayload{} 301 + 302 + p.Action = api.HookWikiCreated 303 + pl, err := pc.Wiki(p) 304 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 305 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 306 + 307 + p.Action = api.HookWikiEdited 308 + pl, err = pc.Wiki(p) 309 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 310 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 311 + 312 + p.Action = api.HookWikiDeleted 313 + pl, err = pc.Wiki(p) 314 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 315 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 316 + }) 317 + 318 + t.Run("Release", func(t *testing.T) { 319 + p := &api.ReleasePayload{} 320 + 321 + pl, err := pc.Release(p) 322 + require.Equal(t, err, shared.ErrPayloadTypeNotSupported) 323 + require.Equal(t, pl, graphqlPayload[buildsVariables]{}) 324 + }) 325 + } 326 + 327 + func TestSourcehutJSONPayload(t *testing.T) { 328 + gitInit(t) 329 + defer test.MockVariableValue(&setting.RepoRootPath, ".")() 330 + defer test.MockVariableValue(&setting.AppURL, "https://example.forgejo.org/")() 331 + 332 + repo := &api.Repository{ 333 + HTMLURL: "http://localhost:3000/testdata/repo", 334 + Name: "repo", 335 + FullName: "testdata/repo", 336 + Owner: &api.User{ 337 + UserName: "testdata", 338 + }, 339 + CloneURL: "http://localhost:3000/testdata/repo.git", 340 + } 341 + 342 + p := &api.PushPayload{ 343 + Ref: "refs/heads/main", 344 + HeadCommit: &api.PayloadCommit{ 345 + ID: "58771003157b81abc6bf41df0c5db4147a3e3c83", 346 + Message: "json test", 347 + }, 348 + Repo: repo, 349 + } 350 + data, err := p.JSONPayload() 351 + require.NoError(t, err) 352 + 353 + hook := &webhook_model.Webhook{ 354 + RepoID: 3, 355 + IsActive: true, 356 + Type: webhook_module.MATRIX, 357 + URL: "https://sourcehut.example.com/api/jobs", 358 + Meta: `{"manifest_path":"simple.yml"}`, 359 + } 360 + task := &webhook_model.HookTask{ 361 + HookID: hook.ID, 362 + EventType: webhook_module.HookEventPush, 363 + PayloadContent: string(data), 364 + PayloadVersion: 2, 365 + } 366 + 367 + req, reqBody, err := BuildsHandler{}.NewRequest(context.Background(), hook, task) 368 + require.NoError(t, err) 369 + require.NotNil(t, req) 370 + require.NotNil(t, reqBody) 371 + 372 + assert.Equal(t, "POST", req.Method) 373 + assert.Equal(t, "/api/jobs", req.URL.Path) 374 + assert.Equal(t, "application/json", req.Header.Get("Content-Type")) 375 + var body graphqlPayload[buildsVariables] 376 + err = json.NewDecoder(req.Body).Decode(&body) 377 + assert.NoError(t, err) 378 + assert.Equal(t, "json test", body.Variables.Note) 379 + } 380 + 381 + func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string) { 382 + t.Helper() 383 + 384 + // Create a new repository 385 + repo, err := repo_service.CreateRepository(db.DefaultContext, owner, owner, repo_service.CreateRepoOptions{ 386 + Name: name, 387 + Description: "Temporary Repo", 388 + AutoInit: true, 389 + Gitignores: "", 390 + License: "WTFPL", 391 + Readme: "Default", 392 + DefaultBranch: "main", 393 + }) 394 + assert.NoError(t, err) 395 + assert.NotEmpty(t, repo) 396 + t.Cleanup(func() { 397 + repo_service.DeleteRepository(db.DefaultContext, owner, repo, false) 398 + }) 399 + 400 + if enabledUnits != nil || disabledUnits != nil { 401 + units := make([]repo_model.RepoUnit, len(enabledUnits)) 402 + for i, unitType := range enabledUnits { 403 + units[i] = repo_model.RepoUnit{ 404 + RepoID: repo.ID, 405 + Type: unitType, 406 + } 407 + } 408 + 409 + err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits) 410 + assert.NoError(t, err) 411 + } 412 + 413 + var sha string 414 + if len(files) > 0 { 415 + resp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{ 416 + Files: files, 417 + Message: "add files", 418 + OldBranch: "main", 419 + NewBranch: "main", 420 + Author: &files_service.IdentityOptions{ 421 + Name: owner.Name, 422 + Email: owner.Email, 423 + }, 424 + Committer: &files_service.IdentityOptions{ 425 + Name: owner.Name, 426 + Email: owner.Email, 427 + }, 428 + Dates: &files_service.CommitDateOptions{ 429 + Author: time.Now(), 430 + Committer: time.Now(), 431 + }, 432 + }) 433 + assert.NoError(t, err) 434 + assert.NotEmpty(t, resp) 435 + 436 + sha = resp.Commit.SHA 437 + } 438 + 439 + return repo, sha 440 + }
+1
services/webhook/sourcehut/testdata/repo.git/HEAD
··· 1 + ref: refs/heads/main
+4
services/webhook/sourcehut/testdata/repo.git/config
··· 1 + [core] 2 + repositoryformatversion = 0 3 + filemode = true 4 + bare = true
+1
services/webhook/sourcehut/testdata/repo.git/description
··· 1 + Unnamed repository; edit this file 'description' to name the repository.
+6
services/webhook/sourcehut/testdata/repo.git/info/exclude
··· 1 + # git ls-files --others --exclude-from=.git/info/exclude 2 + # Lines that start with '#' are comments. 3 + # For a project mostly in C, the following would be a good set of 4 + # exclude patterns (uncomment them if you want to use them): 5 + # *.[oa] 6 + # *~
services/webhook/sourcehut/testdata/repo.git/objects/01/16b5e2279b70e5f5b98240e8896331248f4463

This is a binary file and will not be displayed.

services/webhook/sourcehut/testdata/repo.git/objects/3c/3d4b799b3933ba687b263eeef2034300a5315e

This is a binary file and will not be displayed.

+1
services/webhook/sourcehut/testdata/repo.git/objects/56/f276169b9766f805e5198fe7fb6698153fdb03
··· 1 + x�NKj�0�Z�x�B��ɶ���z���Q�[FQ�?"=A3�Ѳmk#���*@��L3&�)��'D$�#�Β� 搊�Ѽ,#/��8�Ov��zIN<�u'��[;�J��~���{�#'�e;.x����輋#[K��[k�y���ASq\DA��kƵ�������؝~P�k���VO�
+2
services/webhook/sourcehut/testdata/repo.git/objects/58/771003157b81abc6bf41df0c5db4147a3e3c83
··· 1 + x=���0D=�+�nB�X����h�Vk�%�?_P�m���̔b ��C�̠��D�{� 2 + ;��F�&��q���m���<�5e8�|�[����/��� O��5�� GYK)��\� iO�KJ3 �PƝ�j��U>��V���X���܃絈7\p;�
+1
services/webhook/sourcehut/testdata/repo.git/objects/69/b217caa89166a02b8cd368b64fb83a44720e14
··· 1 + x=��n� �{�)�^�Z ,EUN}��&T�A��y���6�a��T�=w�����̂�Ģ5��O� �\�m\�uFT��G�׈F;���NQ�^�[��֓a��Q��o��kiW~+p��p��u�i� h��a����3�J?�:7([��VK��|���͙�T��I�7�����u�İӑ��>s��P�� �=�C}ˢO�
services/webhook/sourcehut/testdata/repo.git/objects/9e/4b777f81b316a1c75a0797b33add68ee49b0d0

This is a binary file and will not be displayed.

services/webhook/sourcehut/testdata/repo.git/objects/aa/3905af404394f576f88f00e7f0919b4b97453f

This is a binary file and will not be displayed.

services/webhook/sourcehut/testdata/repo.git/objects/bd/d74dba3f34542e480d56c2a91c9e2463180d3c

This is a binary file and will not be displayed.

services/webhook/sourcehut/testdata/repo.git/objects/c2/30778a03511f546f532e79c8c14917c260e750

This is a binary file and will not be displayed.

services/webhook/sourcehut/testdata/repo.git/objects/c9/fa8be8c6f230e9529f8e546a3e6a354cbaf313

This is a binary file and will not be displayed.

+1
services/webhook/sourcehut/testdata/repo.git/objects/cb/80b2628b69c86b6baea464e5f9fc28405fde4b
··· 1 + x=�Kn�0 D��)�`�k�@Pd�{P2���-AQ���] �Y��Ie�sm�KoD��)�����8�p gg44�l��FQ����F9�˜��V�,�[�U�Τ`~�[i�Vڕ� ��������4�+(��0Y)�$��"���Ԡl��Z-e��5��w�ԦʸN���Y�?V4�&���t����C9�=a���� �,P�
+4
services/webhook/sourcehut/testdata/repo.git/objects/d2/e0862c8b8097ba4bdd72946c20479751d307a0
··· 1 + xENIn�0 �Y�����D��#ȁ� ۍ, 2 + "$�����\f��9ئ9~,+L�-�㒶�ɀ�=o�g��#�&�OU���o߷�j�U!�,��꺮�DGP� 3 + e>L����狡t[ 4 + ������#?���C�~� z�2!,��qCt�Q�Z<�.@78��������\�I
+1
services/webhook/sourcehut/testdata/repo.git/refs/heads/main
··· 1 + 69b217caa89166a02b8cd368b64fb83a44720e14
+12 -11
services/webhook/telegram.go
··· 18 18 api "code.gitea.io/gitea/modules/structs" 19 19 webhook_module "code.gitea.io/gitea/modules/webhook" 20 20 "code.gitea.io/gitea/services/forms" 21 + "code.gitea.io/gitea/services/webhook/shared" 21 22 ) 22 23 23 24 type telegramHandler struct{} 24 25 25 26 func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } 26 - func (telegramHandler) Icon(size int) template.HTML { return imgIcon("telegram.png", size) } 27 + func (telegramHandler) Icon(size int) template.HTML { return shared.ImgIcon("telegram.png", size) } 27 28 28 - func (telegramHandler) FormFields(bind func(any)) FormFields { 29 + func (telegramHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 29 30 var form struct { 30 - forms.WebhookForm 31 + forms.WebhookCoreForm 31 32 BotToken string `binding:"Required"` 32 33 ChatID string `binding:"Required"` 33 34 ThreadID string 34 35 } 35 36 bind(&form) 36 37 37 - return FormFields{ 38 - WebhookForm: form.WebhookForm, 39 - URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), 40 - ContentType: webhook_model.ContentTypeJSON, 41 - Secret: "", 42 - HTTPMethod: http.MethodPost, 38 + return forms.WebhookForm{ 39 + WebhookCoreForm: form.WebhookCoreForm, 40 + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s", url.PathEscape(form.BotToken), url.QueryEscape(form.ChatID), url.QueryEscape(form.ThreadID)), 41 + ContentType: webhook_model.ContentTypeJSON, 42 + Secret: "", 43 + HTTPMethod: http.MethodPost, 43 44 Metadata: &TelegramMeta{ 44 45 BotToken: form.BotToken, 45 46 ChatID: form.ChatID, ··· 220 221 221 222 type telegramConvertor struct{} 222 223 223 - var _ payloadConvertor[TelegramPayload] = telegramConvertor{} 224 + var _ shared.PayloadConvertor[TelegramPayload] = telegramConvertor{} 224 225 225 226 func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 226 - return newJSONRequest(telegramConvertor{}, w, t, true) 227 + return shared.NewJSONRequest(telegramConvertor{}, w, t, true) 227 228 }
+4 -11
services/webhook/webhook.go
··· 25 25 "code.gitea.io/gitea/modules/util" 26 26 webhook_module "code.gitea.io/gitea/modules/webhook" 27 27 "code.gitea.io/gitea/services/forms" 28 + "code.gitea.io/gitea/services/webhook/sourcehut" 28 29 29 30 "github.com/gobwas/glob" 30 31 ) ··· 32 33 type Handler interface { 33 34 Type() webhook_module.HookType 34 35 Metadata(*webhook_model.Webhook) any 35 - // FormFields provides a function to bind the request to the form. 36 + // UnmarshalForm provides a function to bind the request to the form. 36 37 // If form implements the [binding.Validator] interface, the Validate method will be called 37 - FormFields(bind func(form any)) FormFields 38 + UnmarshalForm(bind func(form any)) forms.WebhookForm 38 39 NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) 39 40 Icon(size int) template.HTML 40 41 } 41 42 42 - type FormFields struct { 43 - forms.WebhookForm 44 - URL string 45 - ContentType webhook_model.HookContentType 46 - Secret string 47 - HTTPMethod string 48 - Metadata any 49 - } 50 - 51 43 var webhookHandlers = []Handler{ 52 44 defaultHandler{true}, 53 45 defaultHandler{false}, ··· 62 54 matrixHandler{}, 63 55 wechatworkHandler{}, 64 56 packagistHandler{}, 57 + sourcehut.BuildsHandler{}, 65 58 } 66 59 67 60 // GetWebhookHandler return the handler for a given webhook type (nil if not found)
+13 -12
services/webhook/wechatwork.go
··· 15 15 api "code.gitea.io/gitea/modules/structs" 16 16 webhook_module "code.gitea.io/gitea/modules/webhook" 17 17 "code.gitea.io/gitea/services/forms" 18 + "code.gitea.io/gitea/services/webhook/shared" 18 19 ) 19 20 20 21 type wechatworkHandler struct{} ··· 23 24 func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } 24 25 25 26 func (wechatworkHandler) Icon(size int) template.HTML { 26 - return imgIcon("wechatwork.png", size) 27 + return shared.ImgIcon("wechatwork.png", size) 27 28 } 28 29 29 - func (wechatworkHandler) FormFields(bind func(any)) FormFields { 30 + func (wechatworkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { 30 31 var form struct { 31 - forms.WebhookForm 32 + forms.WebhookCoreForm 32 33 PayloadURL string `binding:"Required;ValidUrl"` 33 34 } 34 35 bind(&form) 35 36 36 - return FormFields{ 37 - WebhookForm: form.WebhookForm, 38 - URL: form.PayloadURL, 39 - ContentType: webhook_model.ContentTypeJSON, 40 - Secret: "", 41 - HTTPMethod: http.MethodPost, 42 - Metadata: nil, 37 + return forms.WebhookForm{ 38 + WebhookCoreForm: form.WebhookCoreForm, 39 + URL: form.PayloadURL, 40 + ContentType: webhook_model.ContentTypeJSON, 41 + Secret: "", 42 + HTTPMethod: http.MethodPost, 43 + Metadata: nil, 43 44 } 44 45 } 45 46 ··· 203 204 204 205 type wechatworkConvertor struct{} 205 206 206 - var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} 207 + var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{} 207 208 208 209 func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { 209 - return newJSONRequest(wechatworkConvertor{}, w, t, true) 210 + return shared.NewJSONRequest(wechatworkConvertor{}, w, t, true) 210 211 }
+2
templates/webhook/new.tmpl
··· 36 36 {{template "webhook/new/wechatwork" .}} 37 37 {{else if eq .HookType "packagist"}} 38 38 {{template "webhook/new/packagist" .}} 39 + {{else if eq .HookType "sourcehut_builds"}} 40 + {{template "webhook/new/sourcehut_builds" .}} 39 41 {{end}} 40 42 {{end}} 41 43 </div>
+33
templates/webhook/new/sourcehut_builds.tmpl
··· 1 + <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://sourcehut.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_sourcehut_builds")}}</p> 2 + <form class="ui form" action="{{.BaseLink}}/{{or .Webhook.ID "sourcehut_builds/new"}}" method="post"> 3 + {{.CsrfTokenHtml}} 4 + <div class="required field {{if .Err_PayloadURL}}error{{end}}"> 5 + <label for="payload_url">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.graphql_url"}}</label> 6 + <input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> 7 + </div> 8 + <div class="required field {{if .Err_ManifestPath}}error{{end}}"> 9 + <label for="manifest_path">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.manifest_path"}}</label> 10 + <input id="manifest_path" name="manifest_path" type="text" value="{{.HookMetadata.ManifestPath}}" required> 11 + </div> 12 + <div class="field"> 13 + <label>{{ctx.Locale.Tr "repo.settings.sourcehut_builds.visibility"}}</label> 14 + <div class="ui selection dropdown"> 15 + <input type="hidden" id="visibility" name="visibility" value="{{if .HookMetadata.Visibility}}{{.HookMetadata.Visibility}}{{else}}PRIVATE{{end}}"> 16 + <div class="default text"></div> 17 + {{svg "octicon-triangle-down" 14 "dropdown icon"}} 18 + <div class="menu"> 19 + <div class="item" data-value="PUBLIC">PUBLIC</div> 20 + <div class="item" data-value="UNLISTED">UNLISTED</div> 21 + <div class="item" data-value="PRIVATE">PRIVATE</div> 22 + </div> 23 + </div> 24 + </div> 25 + <div class="field"> 26 + <div class="ui checkbox"> 27 + <input name="secrets" type="checkbox" {{if .HookMetadata.Secrets}}checked{{end}}> 28 + <label>{{ctx.Locale.Tr "repo.settings.sourcehut_builds.secrets"}}</label> 29 + <span class="help">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.secrets_helper"}}</span> 30 + </div> 31 + </div> 32 + {{template "repo/settings/webhook/settings" .}} 33 + </form>
+37 -1
tests/integration/repo_webhook_test.go
··· 238 238 "branch_filter": "packagist/*", 239 239 "authorization_header": "Bearer 123456", 240 240 })) 241 + 242 + t.Run("sourcehut_builds/required", testWebhookForms("sourcehut_builds", session, map[string]string{ 243 + "payload_url": "https://sourcehut_builds.example.com", 244 + "manifest_path": ".build.yml", 245 + "visibility": "PRIVATE", 246 + "authorization_header": "Bearer 123456", 247 + }, map[string]string{ 248 + "authorization_header": "", 249 + }, map[string]string{ 250 + "authorization_header": "token ", 251 + }, map[string]string{ 252 + "manifest_path": "", 253 + }, map[string]string{ 254 + "manifest_path": "/absolute", 255 + }, map[string]string{ 256 + "visibility": "", 257 + }, map[string]string{ 258 + "visibility": "INVALID", 259 + })) 260 + t.Run("sourcehut_builds/optional", testWebhookForms("sourcehut_builds", session, map[string]string{ 261 + "payload_url": "https://sourcehut_builds.example.com", 262 + "manifest_path": ".build.yml", 263 + "visibility": "PRIVATE", 264 + "secrets": "on", 265 + 266 + "branch_filter": "srht/*", 267 + "authorization_header": "Bearer 123456", 268 + })) 241 269 } 242 270 243 271 func assertInput(t testing.TB, form *goquery.Selection, name string) string { ··· 247 275 t.Log(form.Html()) 248 276 t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length()) 249 277 } 250 - return input.AttrOr("value", "") 278 + switch input.AttrOr("type", "") { 279 + case "checkbox": 280 + if _, checked := input.Attr("checked"); checked { 281 + return "on" 282 + } 283 + return "" 284 + default: 285 + return input.AttrOr("value", "") 286 + } 251 287 } 252 288 253 289 func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) {