mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-31 14:31:02 +00:00 
			
		
		
		
	Merge pull request '[v7.0/forgejo] [FEAT] sourcehut webhooks' (#3065) from bp-v7.0/forgejo-ed9dd0e-04a398a into v7.0/forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3065 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
		
				commit
				
					
						7df46a9b34
					
				
			
		
					 43 changed files with 1122 additions and 224 deletions
				
			
		|  | @ -115,6 +115,16 @@ type Repository struct { | |||
| 	RepoTransfer  *RepoTransfer `json:"repo_transfer"` | ||||
| } | ||||
| 
 | ||||
| // GetName implements the gitrepo.Repository interface | ||||
| func (r Repository) GetName() string { | ||||
| 	return r.Name | ||||
| } | ||||
| 
 | ||||
| // GetOwnerName implements the gitrepo.Repository interface | ||||
| func (r Repository) GetOwnerName() string { | ||||
| 	return r.Owner.UserName | ||||
| } | ||||
| 
 | ||||
| // CreateRepoOption options when creating repository | ||||
| // swagger:model | ||||
| type CreateRepoOption struct { | ||||
|  |  | |||
|  | @ -73,18 +73,19 @@ type HookType = string | |||
| 
 | ||||
| // Types of webhooks | ||||
| const ( | ||||
| 	FORGEJO    HookType = "forgejo" | ||||
| 	GITEA      HookType = "gitea" | ||||
| 	GOGS       HookType = "gogs" | ||||
| 	SLACK      HookType = "slack" | ||||
| 	DISCORD    HookType = "discord" | ||||
| 	DINGTALK   HookType = "dingtalk" | ||||
| 	TELEGRAM   HookType = "telegram" | ||||
| 	MSTEAMS    HookType = "msteams" | ||||
| 	FEISHU     HookType = "feishu" | ||||
| 	MATRIX     HookType = "matrix" | ||||
| 	WECHATWORK HookType = "wechatwork" | ||||
| 	PACKAGIST  HookType = "packagist" | ||||
| 	FORGEJO          HookType = "forgejo" | ||||
| 	GITEA            HookType = "gitea" | ||||
| 	GOGS             HookType = "gogs" | ||||
| 	SLACK            HookType = "slack" | ||||
| 	DISCORD          HookType = "discord" | ||||
| 	DINGTALK         HookType = "dingtalk" | ||||
| 	TELEGRAM         HookType = "telegram" | ||||
| 	MSTEAMS          HookType = "msteams" | ||||
| 	FEISHU           HookType = "feishu" | ||||
| 	MATRIX           HookType = "matrix" | ||||
| 	WECHATWORK       HookType = "wechatwork" | ||||
| 	PACKAGIST        HookType = "packagist" | ||||
| 	SOURCEHUT_BUILDS HookType = "sourcehut_builds" //nolint:revive | ||||
| ) | ||||
| 
 | ||||
| // HookStatus is the status of a web hook | ||||
|  |  | |||
|  | @ -640,6 +640,8 @@ target_branch_not_exist = Target branch does not exist. | |||
| 
 | ||||
| admin_cannot_delete_self = You cannot delete yourself when you are an admin. Please remove your admin privileges first. | ||||
| 
 | ||||
| required_prefix = Input must start with "%s" | ||||
| 
 | ||||
| [user] | ||||
| change_avatar = Change your avatar… | ||||
| joined_on = Joined on %s | ||||
|  | @ -2269,6 +2271,7 @@ settings.delete_team_tip = This team has access to all repositories and can't be | |||
| settings.remove_team_success = The team's access to the repository has been removed. | ||||
| settings.add_webhook = Add webhook | ||||
| settings.add_webhook.invalid_channel_name = Webhook channel name cannot be empty and cannot contain only a # character. | ||||
| 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. | ||||
| 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>. | ||||
| settings.webhook_deletion = Remove webhook | ||||
| settings.webhook_deletion_desc = Removing a webhook deletes its settings and delivery history. Continue? | ||||
|  | @ -2384,6 +2387,12 @@ settings.web_hook_name_packagist = Packagist | |||
| settings.packagist_username = Packagist username | ||||
| settings.packagist_api_token = API token | ||||
| settings.packagist_package_url = Packagist package URL | ||||
| settings.web_hook_name_sourcehut_builds = SourceHut Builds | ||||
| settings.sourcehut_builds.manifest_path = Build manifest path | ||||
| settings.sourcehut_builds.graphql_url = GraphQL URL (e.g. https://builds.sr.ht/query) | ||||
| settings.sourcehut_builds.visibility = Job visibility | ||||
| settings.sourcehut_builds.secrets = Secrets | ||||
| settings.sourcehut_builds.secrets_helper = Give the job access to the build secrets (requires the SECRETS:RO grant) | ||||
| settings.deploy_keys = Deploy keys | ||||
| settings.add_deploy_key = Add deploy key | ||||
| settings.deploy_key_desc = Deploy keys have read-only pull access to the repository. | ||||
|  |  | |||
							
								
								
									
										7
									
								
								public/assets/img/sourcehut.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								public/assets/img/sourcehut.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"> | ||||
|   <style> | ||||
|   path { fill: black; } | ||||
|   @media (prefers-color-scheme: dark) { path { fill: white; } } | ||||
|   </style> | ||||
|   <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"/> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 345 B | 
|  | @ -148,7 +148,7 @@ func WebhookNew(ctx *context.Context) { | |||
| } | ||||
| 
 | ||||
| // ParseHookEvent convert web form content to webhook.HookEvent | ||||
| func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent { | ||||
| func ParseHookEvent(form forms.WebhookCoreForm) *webhook_module.HookEvent { | ||||
| 	return &webhook_module.HookEvent{ | ||||
| 		PushOnly:       form.PushOnly(), | ||||
| 		SendEverything: form.SendEverything(), | ||||
|  | @ -188,7 +188,7 @@ func WebhookCreate(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	fields := handler.FormFields(func(form any) { | ||||
| 	fields := handler.UnmarshalForm(func(form any) { | ||||
| 		errs := binding.Bind(ctx.Req, form) | ||||
| 		middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError | ||||
| 	}) | ||||
|  | @ -215,10 +215,10 @@ func WebhookCreate(ctx *context.Context) { | |||
| 		w.URL = fields.URL | ||||
| 		w.ContentType = fields.ContentType | ||||
| 		w.Secret = fields.Secret | ||||
| 		w.HookEvent = ParseHookEvent(fields.WebhookForm) | ||||
| 		w.IsActive = fields.WebhookForm.Active | ||||
| 		w.HookEvent = ParseHookEvent(fields.WebhookCoreForm) | ||||
| 		w.IsActive = fields.Active | ||||
| 		w.HTTPMethod = fields.HTTPMethod | ||||
| 		err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) | ||||
| 		err := w.SetHeaderAuthorization(fields.AuthorizationHeader) | ||||
| 		if err != nil { | ||||
| 			ctx.ServerError("SetHeaderAuthorization", err) | ||||
| 			return | ||||
|  | @ -245,14 +245,14 @@ func WebhookCreate(ctx *context.Context) { | |||
| 		HTTPMethod:      fields.HTTPMethod, | ||||
| 		ContentType:     fields.ContentType, | ||||
| 		Secret:          fields.Secret, | ||||
| 		HookEvent:       ParseHookEvent(fields.WebhookForm), | ||||
| 		IsActive:        fields.WebhookForm.Active, | ||||
| 		HookEvent:       ParseHookEvent(fields.WebhookCoreForm), | ||||
| 		IsActive:        fields.Active, | ||||
| 		Type:            hookType, | ||||
| 		Meta:            string(meta), | ||||
| 		OwnerID:         orCtx.OwnerID, | ||||
| 		IsSystemWebhook: orCtx.IsSystemWebhook, | ||||
| 	} | ||||
| 	err = w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) | ||||
| 	err = w.SetHeaderAuthorization(fields.AuthorizationHeader) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("SetHeaderAuthorization", err) | ||||
| 		return | ||||
|  | @ -286,7 +286,7 @@ func WebhookUpdate(ctx *context.Context) { | |||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	fields := handler.FormFields(func(form any) { | ||||
| 	fields := handler.UnmarshalForm(func(form any) { | ||||
| 		errs := binding.Bind(ctx.Req, form) | ||||
| 		middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError | ||||
| 	}) | ||||
|  | @ -295,11 +295,11 @@ func WebhookUpdate(ctx *context.Context) { | |||
| 	w.URL = fields.URL | ||||
| 	w.ContentType = fields.ContentType | ||||
| 	w.Secret = fields.Secret | ||||
| 	w.HookEvent = ParseHookEvent(fields.WebhookForm) | ||||
| 	w.IsActive = fields.WebhookForm.Active | ||||
| 	w.HookEvent = ParseHookEvent(fields.WebhookCoreForm) | ||||
| 	w.IsActive = fields.Active | ||||
| 	w.HTTPMethod = fields.HTTPMethod | ||||
| 
 | ||||
| 	err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader) | ||||
| 	err := w.SetHeaderAuthorization(fields.AuthorizationHeader) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("SetHeaderAuthorization", err) | ||||
| 		return | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import ( | |||
| 	"code.gitea.io/gitea/models" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	project_model "code.gitea.io/gitea/models/project" | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
|  | @ -235,8 +236,8 @@ func (f *ProtectBranchForm) Validate(req *http.Request, errs binding.Errors) bin | |||
| //   \__/\  /  \___  >___  /___|  /\____/ \____/|__|_ \ | ||||
| //        \/       \/    \/     \/                   \/ | ||||
| 
 | ||||
| // WebhookForm form for changing web hook | ||||
| type WebhookForm struct { | ||||
| // WebhookCoreForm form for changing web hook (common to all webhook types) | ||||
| type WebhookCoreForm struct { | ||||
| 	Events                   string | ||||
| 	Create                   bool | ||||
| 	Delete                   bool | ||||
|  | @ -265,20 +266,30 @@ type WebhookForm struct { | |||
| } | ||||
| 
 | ||||
| // PushOnly if the hook will be triggered when push | ||||
| func (f WebhookForm) PushOnly() bool { | ||||
| func (f WebhookCoreForm) PushOnly() bool { | ||||
| 	return f.Events == "push_only" | ||||
| } | ||||
| 
 | ||||
| // SendEverything if the hook will be triggered any event | ||||
| func (f WebhookForm) SendEverything() bool { | ||||
| func (f WebhookCoreForm) SendEverything() bool { | ||||
| 	return f.Events == "send_everything" | ||||
| } | ||||
| 
 | ||||
| // ChooseEvents if the hook will be triggered choose events | ||||
| func (f WebhookForm) ChooseEvents() bool { | ||||
| func (f WebhookCoreForm) ChooseEvents() bool { | ||||
| 	return f.Events == "choose_events" | ||||
| } | ||||
| 
 | ||||
| // WebhookForm form for changing web hook (specific handling depending on the webhook type) | ||||
| type WebhookForm struct { | ||||
| 	WebhookCoreForm | ||||
| 	URL         string | ||||
| 	ContentType webhook_model.HookContentType | ||||
| 	Secret      string | ||||
| 	HTTPMethod  string | ||||
| 	Metadata    any | ||||
| } | ||||
| 
 | ||||
| // .___ | ||||
| // |   | ______ ________ __   ____ | ||||
| // |   |/  ___//  ___/  |  \_/ __ \ | ||||
|  |  | |||
|  | @ -5,13 +5,8 @@ package webhook | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/hex" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"strings" | ||||
|  | @ -21,6 +16,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/svg" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| var _ Handler = defaultHandler{} | ||||
|  | @ -39,16 +35,16 @@ func (dh defaultHandler) Type() webhook_module.HookType { | |||
| func (dh defaultHandler) Icon(size int) template.HTML { | ||||
| 	if dh.forgejo { | ||||
| 		// forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work | ||||
| 		return imgIcon("forgejo.svg", size) | ||||
| 		return shared.ImgIcon("forgejo.svg", size) | ||||
| 	} | ||||
| 	return svg.RenderHTML("gitea-gitea", size, "img") | ||||
| } | ||||
| 
 | ||||
| func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil } | ||||
| 
 | ||||
| func (defaultHandler) FormFields(bind func(any)) FormFields { | ||||
| func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL  string `binding:"Required;ValidUrl"` | ||||
| 		HTTPMethod  string `binding:"Required;In(POST,GET)"` | ||||
| 		ContentType int    `binding:"Required"` | ||||
|  | @ -60,13 +56,13 @@ func (defaultHandler) FormFields(bind func(any)) FormFields { | |||
| 	if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { | ||||
| 		contentType = webhook_model.ContentTypeForm | ||||
| 	} | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: contentType, | ||||
| 		Secret:      form.Secret, | ||||
| 		HTTPMethod:  form.HTTPMethod, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     contentType, | ||||
| 		Secret:          form.Secret, | ||||
| 		HTTPMethod:      form.HTTPMethod, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -130,42 +126,5 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, | |||
| 	} | ||||
| 
 | ||||
| 	body = []byte(t.PayloadContent) | ||||
| 	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) | ||||
| } | ||||
| 
 | ||||
| func addDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { | ||||
| 	var signatureSHA1 string | ||||
| 	var signatureSHA256 string | ||||
| 	if len(secret) > 0 { | ||||
| 		sig1 := hmac.New(sha1.New, secret) | ||||
| 		sig256 := hmac.New(sha256.New, secret) | ||||
| 		_, err := io.MultiWriter(sig1, sig256).Write(payloadContent) | ||||
| 		if err != nil { | ||||
| 			// this error should never happen, since the hashes are writing to []byte and always return a nil error. | ||||
| 			return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) | ||||
| 		} | ||||
| 		signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) | ||||
| 		signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) | ||||
| 	} | ||||
| 
 | ||||
| 	event := t.EventType.Event() | ||||
| 	eventType := string(t.EventType) | ||||
| 	req.Header.Add("X-Forgejo-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Forgejo-Event", event) | ||||
| 	req.Header.Add("X-Forgejo-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Forgejo-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Gitea-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Gitea-Event", event) | ||||
| 	req.Header.Add("X-Gitea-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Gitea-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Gogs-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Gogs-Event", event) | ||||
| 	req.Header.Add("X-Gogs-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Gogs-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) | ||||
| 	req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) | ||||
| 	req.Header["X-GitHub-Delivery"] = []string{t.UUID} | ||||
| 	req.Header["X-GitHub-Event"] = []string{event} | ||||
| 	req.Header["X-GitHub-Event-Type"] = []string{eventType} | ||||
| 	return nil | ||||
| 	return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) | ||||
| } | ||||
|  |  | |||
|  | @ -17,28 +17,29 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type dingtalkHandler struct{} | ||||
| 
 | ||||
| func (dingtalkHandler) Type() webhook_module.HookType       { return webhook_module.DINGTALK } | ||||
| func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil } | ||||
| func (dingtalkHandler) Icon(size int) template.HTML         { return imgIcon("dingtalk.ico", size) } | ||||
| func (dingtalkHandler) Icon(size int) template.HTML         { return shared.ImgIcon("dingtalk.ico", size) } | ||||
| 
 | ||||
| func (dingtalkHandler) FormFields(bind func(any)) FormFields { | ||||
| func (dingtalkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -225,8 +226,8 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkP | |||
| 
 | ||||
| type dingtalkConvertor struct{} | ||||
| 
 | ||||
| var _ payloadConvertor[DingtalkPayload] = dingtalkConvertor{} | ||||
| var _ shared.PayloadConvertor[DingtalkPayload] = dingtalkConvertor{} | ||||
| 
 | ||||
| func (dingtalkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	return newJSONRequest(dingtalkConvertor{}, w, t, true) | ||||
| 	return shared.NewJSONRequest(dingtalkConvertor{}, w, t, true) | ||||
| } | ||||
|  |  | |||
|  | @ -22,28 +22,29 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type discordHandler struct{} | ||||
| 
 | ||||
| func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD } | ||||
| func (discordHandler) Icon(size int) template.HTML   { return imgIcon("discord.png", size) } | ||||
| func (discordHandler) Icon(size int) template.HTML   { return shared.ImgIcon("discord.png", size) } | ||||
| 
 | ||||
| func (discordHandler) FormFields(bind func(any)) FormFields { | ||||
| func (discordHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 		Username   string | ||||
| 		IconURL    string | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata: &DiscordMeta{ | ||||
| 			Username: form.Username, | ||||
| 			IconURL:  form.IconURL, | ||||
|  | @ -287,7 +288,7 @@ type discordConvertor struct { | |||
| 	AvatarURL string | ||||
| } | ||||
| 
 | ||||
| var _ payloadConvertor[DiscordPayload] = discordConvertor{} | ||||
| var _ shared.PayloadConvertor[DiscordPayload] = discordConvertor{} | ||||
| 
 | ||||
| func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	meta := &DiscordMeta{} | ||||
|  | @ -298,7 +299,7 @@ func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, | |||
| 		Username:  meta.Username, | ||||
| 		AvatarURL: meta.IconURL, | ||||
| 	} | ||||
| 	return newJSONRequest(sc, w, t, true) | ||||
| 	return shared.NewJSONRequest(sc, w, t, true) | ||||
| } | ||||
| 
 | ||||
| func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) { | ||||
|  |  | |||
|  | @ -15,27 +15,28 @@ import ( | |||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type feishuHandler struct{} | ||||
| 
 | ||||
| func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU } | ||||
| func (feishuHandler) Icon(size int) template.HTML   { return imgIcon("feishu.png", size) } | ||||
| func (feishuHandler) Icon(size int) template.HTML   { return shared.ImgIcon("feishu.png", size) } | ||||
| 
 | ||||
| func (feishuHandler) FormFields(bind func(any)) FormFields { | ||||
| func (feishuHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -192,8 +193,8 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) | |||
| 
 | ||||
| type feishuConvertor struct{} | ||||
| 
 | ||||
| var _ payloadConvertor[FeishuPayload] = feishuConvertor{} | ||||
| var _ shared.PayloadConvertor[FeishuPayload] = feishuConvertor{} | ||||
| 
 | ||||
| func (feishuHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	return newJSONRequest(feishuConvertor{}, w, t, true) | ||||
| 	return shared.NewJSONRequest(feishuConvertor{}, w, t, true) | ||||
| } | ||||
|  |  | |||
|  | @ -6,9 +6,7 @@ package webhook | |||
| import ( | ||||
| 	"fmt" | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
|  | @ -354,9 +352,3 @@ func ToHook(repoLink string, w *webhook_model.Webhook) (*api.Hook, error) { | |||
| 		Created:             w.CreatedUnix.AsTime(), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func imgIcon(name string, size int) template.HTML { | ||||
| 	s := strconv.Itoa(size) | ||||
| 	src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name) | ||||
| 	return template.HTML(`<img width="` + s + `" height="` + s + `" src="` + src + `">`) | ||||
| } | ||||
|  |  | |||
|  | @ -10,16 +10,17 @@ import ( | |||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type gogsHandler struct{ defaultHandler } | ||||
| 
 | ||||
| func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS } | ||||
| func (gogsHandler) Icon(size int) template.HTML   { return imgIcon("gogs.ico", size) } | ||||
| func (gogsHandler) Icon(size int) template.HTML   { return shared.ImgIcon("gogs.ico", size) } | ||||
| 
 | ||||
| func (gogsHandler) FormFields(bind func(any)) FormFields { | ||||
| func (gogsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL  string `binding:"Required;ValidUrl"` | ||||
| 		ContentType int    `binding:"Required"` | ||||
| 		Secret      string | ||||
|  | @ -30,12 +31,12 @@ func (gogsHandler) FormFields(bind func(any)) FormFields { | |||
| 	if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm { | ||||
| 		contentType = webhook_model.ContentTypeForm | ||||
| 	} | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: contentType, | ||||
| 		Secret:      form.Secret, | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     contentType, | ||||
| 		Secret:          form.Secret, | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type matrixHandler struct{} | ||||
|  | @ -35,25 +36,25 @@ func (matrixHandler) Icon(size int) template.HTML { | |||
| 	return svg.RenderHTML("gitea-matrix", size, "img") | ||||
| } | ||||
| 
 | ||||
| func (matrixHandler) FormFields(bind func(any)) FormFields { | ||||
| func (matrixHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		HomeserverURL string `binding:"Required;ValidUrl"` | ||||
| 		RoomID        string `binding:"Required"` | ||||
| 		MessageType   int | ||||
| 
 | ||||
| 		// enforce requirement of authorization_header | ||||
| 		// (value will still be set in the embedded WebhookForm) | ||||
| 		// (value will still be set in the embedded WebhookCoreForm) | ||||
| 		AuthorizationHeader string `binding:"Required"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPut, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)), | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPut, | ||||
| 		Metadata: &MatrixMeta{ | ||||
| 			HomeserverURL: form.HomeserverURL, | ||||
| 			Room:          form.RoomID, | ||||
|  | @ -70,7 +71,7 @@ func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t | |||
| 	mc := matrixConvertor{ | ||||
| 		MsgType: messageTypeText[meta.MessageType], | ||||
| 	} | ||||
| 	payload, err := newPayload(mc, []byte(t.PayloadContent), t.EventType) | ||||
| 	payload, err := shared.NewPayload(mc, []byte(t.PayloadContent), t.EventType) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | @ -90,7 +91,7 @@ func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t | |||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 
 | ||||
| 	return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially | ||||
| 	return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body) // likely useless, but has always been sent historially | ||||
| } | ||||
| 
 | ||||
| const matrixPayloadSizeLimit = 1024 * 64 | ||||
|  | @ -125,7 +126,7 @@ type MatrixPayload struct { | |||
| 	Commits       []*api.PayloadCommit `json:"io.gitea.commits,omitempty"` | ||||
| } | ||||
| 
 | ||||
| var _ payloadConvertor[MatrixPayload] = matrixConvertor{} | ||||
| var _ shared.PayloadConvertor[MatrixPayload] = matrixConvertor{} | ||||
| 
 | ||||
| type matrixConvertor struct { | ||||
| 	MsgType string | ||||
|  |  | |||
|  | @ -17,28 +17,29 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type msteamsHandler struct{} | ||||
| 
 | ||||
| func (msteamsHandler) Type() webhook_module.HookType       { return webhook_module.MSTEAMS } | ||||
| func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil } | ||||
| func (msteamsHandler) Icon(size int) template.HTML         { return imgIcon("msteams.png", size) } | ||||
| func (msteamsHandler) Icon(size int) template.HTML         { return shared.ImgIcon("msteams.png", size) } | ||||
| 
 | ||||
| func (msteamsHandler) FormFields(bind func(any)) FormFields { | ||||
| func (msteamsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -370,8 +371,8 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar | |||
| 
 | ||||
| type msteamsConvertor struct{} | ||||
| 
 | ||||
| var _ payloadConvertor[MSTeamsPayload] = msteamsConvertor{} | ||||
| var _ shared.PayloadConvertor[MSTeamsPayload] = msteamsConvertor{} | ||||
| 
 | ||||
| func (msteamsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	return newJSONRequest(msteamsConvertor{}, w, t, true) | ||||
| 	return shared.NewJSONRequest(msteamsConvertor{}, w, t, true) | ||||
| } | ||||
|  |  | |||
|  | @ -15,28 +15,29 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type packagistHandler struct{} | ||||
| 
 | ||||
| func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST } | ||||
| func (packagistHandler) Icon(size int) template.HTML   { return imgIcon("packagist.png", size) } | ||||
| func (packagistHandler) Icon(size int) template.HTML   { return shared.ImgIcon("packagist.png", size) } | ||||
| 
 | ||||
| func (packagistHandler) FormFields(bind func(any)) FormFields { | ||||
| func (packagistHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		Username   string `binding:"Required"` | ||||
| 		APIToken   string `binding:"Required"` | ||||
| 		PackageURL string `binding:"Required;ValidUrl"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             fmt.Sprintf("https://packagist.org/api/update-package?username=%s&apiToken=%s", url.QueryEscape(form.Username), url.QueryEscape(form.APIToken)), | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata: &PackagistMeta{ | ||||
| 			Username:   form.Username, | ||||
| 			APIToken:   form.APIToken, | ||||
|  | @ -85,5 +86,5 @@ func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook | |||
| 			URL: meta.PackageURL, | ||||
| 		}, | ||||
| 	} | ||||
| 	return newJSONRequestWithPayload(payload, w, t, false) | ||||
| 	return shared.NewJSONRequestWithPayload(payload, w, t, false) | ||||
| } | ||||
|  |  | |||
							
								
								
									
										15
									
								
								services/webhook/shared/img.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/webhook/shared/img.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| package shared | ||||
| 
 | ||||
| import ( | ||||
| 	"html" | ||||
| 	"html/template" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| ) | ||||
| 
 | ||||
| func ImgIcon(name string, size int) template.HTML { | ||||
| 	s := strconv.Itoa(size) | ||||
| 	src := html.EscapeString(setting.StaticURLPrefix + "/assets/img/" + name) | ||||
| 	return template.HTML(`<img width="` + s + `" height="` + s + `" src="` + src + `">`) | ||||
| } | ||||
|  | @ -1,11 +1,17 @@ | |||
| // Copyright 2020 The Gitea Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package webhook | ||||
| package shared | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/hex" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
|  | @ -14,8 +20,10 @@ import ( | |||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| ) | ||||
| 
 | ||||
| // payloadConvertor defines the interface to convert system payload to webhook payload | ||||
| type payloadConvertor[T any] interface { | ||||
| var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event") | ||||
| 
 | ||||
| // PayloadConvertor defines the interface to convert system payload to webhook payload | ||||
| type PayloadConvertor[T any] interface { | ||||
| 	Create(*api.CreatePayload) (T, error) | ||||
| 	Delete(*api.DeletePayload) (T, error) | ||||
| 	Fork(*api.ForkPayload) (T, error) | ||||
|  | @ -39,7 +47,7 @@ func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) | |||
| 	return convert(p) | ||||
| } | ||||
| 
 | ||||
| func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { | ||||
| func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) { | ||||
| 	switch event { | ||||
| 	case webhook_module.HookEventCreate: | ||||
| 		return convertUnmarshalledJSON(rc.Create, data) | ||||
|  | @ -83,15 +91,15 @@ func newPayload[T any](rc payloadConvertor[T], data []byte, event webhook_module | |||
| 	return t, fmt.Errorf("newPayload unsupported event: %s", event) | ||||
| } | ||||
| 
 | ||||
| func newJSONRequest[T any](pc payloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { | ||||
| 	payload, err := newPayload(pc, []byte(t.PayloadContent), t.EventType) | ||||
| func NewJSONRequest[T any](pc PayloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { | ||||
| 	payload, err := NewPayload(pc, []byte(t.PayloadContent), t.EventType) | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	return newJSONRequestWithPayload(payload, w, t, withDefaultHeaders) | ||||
| 	return NewJSONRequestWithPayload(payload, w, t, withDefaultHeaders) | ||||
| } | ||||
| 
 | ||||
| func newJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { | ||||
| func NewJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) { | ||||
| 	body, err := json.MarshalIndent(payload, "", "  ") | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
|  | @ -109,7 +117,45 @@ func newJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook | |||
| 	req.Header.Set("Content-Type", "application/json") | ||||
| 
 | ||||
| 	if withDefaultHeaders { | ||||
| 		return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body) | ||||
| 		return req, body, AddDefaultHeaders(req, []byte(w.Secret), t, body) | ||||
| 	} | ||||
| 	return req, body, nil | ||||
| } | ||||
| 
 | ||||
| // AddDefaultHeaders adds the X-Forgejo, X-Gitea, X-Gogs, X-Hub, X-GitHub headers to the given request | ||||
| func AddDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error { | ||||
| 	var signatureSHA1 string | ||||
| 	var signatureSHA256 string | ||||
| 	if len(secret) > 0 { | ||||
| 		sig1 := hmac.New(sha1.New, secret) | ||||
| 		sig256 := hmac.New(sha256.New, secret) | ||||
| 		_, err := io.MultiWriter(sig1, sig256).Write(payloadContent) | ||||
| 		if err != nil { | ||||
| 			// this error should never happen, since the hashes are writing to []byte and always return a nil error. | ||||
| 			return fmt.Errorf("prepareWebhooks.sigWrite: %w", err) | ||||
| 		} | ||||
| 		signatureSHA1 = hex.EncodeToString(sig1.Sum(nil)) | ||||
| 		signatureSHA256 = hex.EncodeToString(sig256.Sum(nil)) | ||||
| 	} | ||||
| 
 | ||||
| 	event := t.EventType.Event() | ||||
| 	eventType := string(t.EventType) | ||||
| 	req.Header.Add("X-Forgejo-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Forgejo-Event", event) | ||||
| 	req.Header.Add("X-Forgejo-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Forgejo-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Gitea-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Gitea-Event", event) | ||||
| 	req.Header.Add("X-Gitea-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Gitea-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Gogs-Delivery", t.UUID) | ||||
| 	req.Header.Add("X-Gogs-Event", event) | ||||
| 	req.Header.Add("X-Gogs-Event-Type", eventType) | ||||
| 	req.Header.Add("X-Gogs-Signature", signatureSHA256) | ||||
| 	req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1) | ||||
| 	req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256) | ||||
| 	req.Header["X-GitHub-Delivery"] = []string{t.UUID} | ||||
| 	req.Header["X-GitHub-Event"] = []string{event} | ||||
| 	req.Header["X-GitHub-Event-Type"] = []string{eventType} | ||||
| 	return nil | ||||
| } | ||||
|  | @ -20,6 +20,7 @@ import ( | |||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	gitea_context "code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| 
 | ||||
| 	"gitea.com/go-chi/binding" | ||||
| ) | ||||
|  | @ -27,10 +28,10 @@ import ( | |||
| type slackHandler struct{} | ||||
| 
 | ||||
| func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK } | ||||
| func (slackHandler) Icon(size int) template.HTML   { return imgIcon("slack.png", size) } | ||||
| func (slackHandler) Icon(size int) template.HTML   { return shared.ImgIcon("slack.png", size) } | ||||
| 
 | ||||
| type slackForm struct { | ||||
| 	forms.WebhookForm | ||||
| 	forms.WebhookCoreForm | ||||
| 	PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 	Channel    string `binding:"Required"` | ||||
| 	Username   string | ||||
|  | @ -53,16 +54,16 @@ func (s *slackForm) Validate(req *http.Request, errs binding.Errors) binding.Err | |||
| 	return errs | ||||
| } | ||||
| 
 | ||||
| func (slackHandler) FormFields(bind func(any)) FormFields { | ||||
| func (slackHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form slackForm | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata: &SlackMeta{ | ||||
| 			Channel:  strings.TrimSpace(form.Channel), | ||||
| 			Username: form.Username, | ||||
|  | @ -334,7 +335,7 @@ type slackConvertor struct { | |||
| 	Color    string | ||||
| } | ||||
| 
 | ||||
| var _ payloadConvertor[SlackPayload] = slackConvertor{} | ||||
| var _ shared.PayloadConvertor[SlackPayload] = slackConvertor{} | ||||
| 
 | ||||
| func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	meta := &SlackMeta{} | ||||
|  | @ -347,7 +348,7 @@ func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t | |||
| 		IconURL:  meta.IconURL, | ||||
| 		Color:    meta.Color, | ||||
| 	} | ||||
| 	return newJSONRequest(sc, w, t, true) | ||||
| 	return shared.NewJSONRequest(sc, w, t, true) | ||||
| } | ||||
| 
 | ||||
| var slackChannel = regexp.MustCompile(`^#?[a-z0-9_-]{1,80}$`) | ||||
|  |  | |||
							
								
								
									
										312
									
								
								services/webhook/sourcehut/builds.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								services/webhook/sourcehut/builds.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,312 @@ | |||
| // Copyright 2024 The Forgejo Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sourcehut | ||||
| 
 | ||||
| import ( | ||||
| 	"cmp" | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"io/fs" | ||||
| 	"net/http" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/gitrepo" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	gitea_context "code.gitea.io/gitea/services/context" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| 
 | ||||
| 	"gitea.com/go-chi/binding" | ||||
| 	"gopkg.in/yaml.v3" | ||||
| ) | ||||
| 
 | ||||
| type BuildsHandler struct{} | ||||
| 
 | ||||
| func (BuildsHandler) Type() webhook_module.HookType { return webhook_module.SOURCEHUT_BUILDS } | ||||
| func (BuildsHandler) Metadata(w *webhook_model.Webhook) any { | ||||
| 	s := &BuildsMeta{} | ||||
| 	if err := json.Unmarshal([]byte(w.Meta), s); err != nil { | ||||
| 		log.Error("sourcehut.BuildsHandler.Metadata(%d): %v", w.ID, err) | ||||
| 	} | ||||
| 	return s | ||||
| } | ||||
| 
 | ||||
| func (BuildsHandler) Icon(size int) template.HTML { | ||||
| 	return shared.ImgIcon("sourcehut.svg", size) | ||||
| } | ||||
| 
 | ||||
| type buildsForm struct { | ||||
| 	forms.WebhookCoreForm | ||||
| 	PayloadURL   string `binding:"Required;ValidUrl"` | ||||
| 	ManifestPath string `binding:"Required"` | ||||
| 	Visibility   string `binding:"Required;In(PUBLIC,UNLISTED,PRIVATE)"` | ||||
| 	Secrets      bool | ||||
| } | ||||
| 
 | ||||
| var _ binding.Validator = &buildsForm{} | ||||
| 
 | ||||
| // Validate implements binding.Validator. | ||||
| func (f *buildsForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { | ||||
| 	ctx := gitea_context.GetWebContext(req) | ||||
| 	if !fs.ValidPath(f.ManifestPath) { | ||||
| 		errs = append(errs, binding.Error{ | ||||
| 			FieldNames:     []string{"ManifestPath"}, | ||||
| 			Classification: "", | ||||
| 			Message:        ctx.Locale.TrString("repo.settings.add_webhook.invalid_path"), | ||||
| 		}) | ||||
| 	} | ||||
| 	if !strings.HasPrefix(f.AuthorizationHeader, "Bearer ") { | ||||
| 		errs = append(errs, binding.Error{ | ||||
| 			FieldNames:     []string{"AuthorizationHeader"}, | ||||
| 			Classification: "", | ||||
| 			Message:        ctx.Locale.TrString("form.required_prefix", "Bearer "), | ||||
| 		}) | ||||
| 	} | ||||
| 	return errs | ||||
| } | ||||
| 
 | ||||
| func (BuildsHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form buildsForm | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata: &BuildsMeta{ | ||||
| 			ManifestPath: form.ManifestPath, | ||||
| 			Visibility:   form.Visibility, | ||||
| 			Secrets:      form.Secrets, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type ( | ||||
| 	graphqlPayload[V any] struct { | ||||
| 		Query     string `json:"query,omitempty"` | ||||
| 		Error     string `json:"error,omitempty"` | ||||
| 		Variables V      `json:"variables,omitempty"` | ||||
| 	} | ||||
| 	// buildsVariables according to https://man.sr.ht/builds.sr.ht/graphql.md | ||||
| 	buildsVariables struct { | ||||
| 		Manifest   string   `json:"manifest"` | ||||
| 		Tags       []string `json:"tags"` | ||||
| 		Note       string   `json:"note"` | ||||
| 		Secrets    bool     `json:"secrets"` | ||||
| 		Execute    bool     `json:"execute"` | ||||
| 		Visibility string   `json:"visibility"` | ||||
| 	} | ||||
| 
 | ||||
| 	// BuildsMeta contains the metadata for the webhook | ||||
| 	BuildsMeta struct { | ||||
| 		ManifestPath string `json:"manifest_path"` | ||||
| 		Visibility   string `json:"visibility"` | ||||
| 		Secrets      bool   `json:"secrets"` | ||||
| 	} | ||||
| ) | ||||
| 
 | ||||
| type sourcehutConvertor struct { | ||||
| 	ctx  context.Context | ||||
| 	meta BuildsMeta | ||||
| } | ||||
| 
 | ||||
| var _ shared.PayloadConvertor[graphqlPayload[buildsVariables]] = sourcehutConvertor{} | ||||
| 
 | ||||
| func (BuildsHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	meta := BuildsMeta{} | ||||
| 	if err := json.Unmarshal([]byte(w.Meta), &meta); err != nil { | ||||
| 		return nil, nil, fmt.Errorf("newSourcehutRequest meta json: %w", err) | ||||
| 	} | ||||
| 	pc := sourcehutConvertor{ | ||||
| 		ctx:  ctx, | ||||
| 		meta: meta, | ||||
| 	} | ||||
| 	return shared.NewJSONRequest(pc, w, t, false) | ||||
| } | ||||
| 
 | ||||
| // Create implements PayloadConvertor Create method | ||||
| func (pc sourcehutConvertor) Create(p *api.CreatePayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return pc.newPayload(p.Repo, p.Sha, p.Ref, p.RefType+" "+git.RefName(p.Ref).ShortName()+" created", true) | ||||
| } | ||||
| 
 | ||||
| // Delete implements PayloadConvertor Delete method | ||||
| func (pc sourcehutConvertor) Delete(_ *api.DeletePayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Fork implements PayloadConvertor Fork method | ||||
| func (pc sourcehutConvertor) Fork(_ *api.ForkPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Push implements PayloadConvertor Push method | ||||
| func (pc sourcehutConvertor) Push(p *api.PushPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return pc.newPayload(p.Repo, p.HeadCommit.ID, p.Ref, p.HeadCommit.Message, true) | ||||
| } | ||||
| 
 | ||||
| // Issue implements PayloadConvertor Issue method | ||||
| func (pc sourcehutConvertor) Issue(_ *api.IssuePayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // IssueComment implements PayloadConvertor IssueComment method | ||||
| func (pc sourcehutConvertor) IssueComment(_ *api.IssueCommentPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // PullRequest implements PayloadConvertor PullRequest method | ||||
| func (pc sourcehutConvertor) PullRequest(_ *api.PullRequestPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	// TODO | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Review implements PayloadConvertor Review method | ||||
| func (pc sourcehutConvertor) Review(_ *api.PullRequestPayload, _ webhook_module.HookEventType) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Repository implements PayloadConvertor Repository method | ||||
| func (pc sourcehutConvertor) Repository(_ *api.RepositoryPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Wiki implements PayloadConvertor Wiki method | ||||
| func (pc sourcehutConvertor) Wiki(_ *api.WikiPayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // Release implements PayloadConvertor Release method | ||||
| func (pc sourcehutConvertor) Release(_ *api.ReleasePayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| func (pc sourcehutConvertor) Package(_ *api.PackagePayload) (graphqlPayload[buildsVariables], error) { | ||||
| 	return graphqlPayload[buildsVariables]{}, shared.ErrPayloadTypeNotSupported | ||||
| } | ||||
| 
 | ||||
| // mustBuildManifest adjusts the manifest to submit to the builds service | ||||
| // | ||||
| // in case of an error the Error field will be set, to be visible by the end-user under recent deliveries | ||||
| func (pc sourcehutConvertor) newPayload(repo *api.Repository, commitID, ref, note string, trusted bool) (graphqlPayload[buildsVariables], error) { | ||||
| 	manifest, err := pc.buildManifest(repo, commitID, ref) | ||||
| 	if err != nil { | ||||
| 		if len(manifest) == 0 { | ||||
| 			return graphqlPayload[buildsVariables]{}, err | ||||
| 		} | ||||
| 		// the manifest contains an error for the user: log the actual error and construct the payload | ||||
| 		// the error will be visible under the "recent deliveries" of the webhook settings. | ||||
| 		log.Warn("sourcehut.builds: could not construct manifest for %s: %v", repo.FullName, err) | ||||
| 		msg := fmt.Sprintf("%s:%s %s", repo.FullName, ref, manifest) | ||||
| 		return graphqlPayload[buildsVariables]{ | ||||
| 			Error: msg, | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	gitRef := git.RefName(ref) | ||||
| 	return graphqlPayload[buildsVariables]{ | ||||
| 		Query: `mutation ( | ||||
| 	$manifest: String! | ||||
| 	$tags: [String!] | ||||
| 	$note: String! | ||||
| 	$secrets: Boolean! | ||||
| 	$execute: Boolean! | ||||
| 	$visibility: Visibility! | ||||
| ) { | ||||
| 	submit( | ||||
| 		manifest: $manifest | ||||
| 		tags: $tags | ||||
| 		note: $note | ||||
| 		secrets: $secrets | ||||
| 		execute: $execute | ||||
| 		visibility: $visibility | ||||
| 	) { | ||||
| 		id | ||||
| 	} | ||||
| }`, Variables: buildsVariables{ | ||||
| 			Manifest:   string(manifest), | ||||
| 			Tags:       []string{repo.FullName, gitRef.RefType() + "/" + gitRef.ShortName(), pc.meta.ManifestPath}, | ||||
| 			Note:       note, | ||||
| 			Secrets:    pc.meta.Secrets && trusted, | ||||
| 			Execute:    trusted, | ||||
| 			Visibility: cmp.Or(pc.meta.Visibility, "PRIVATE"), | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // buildManifest adjusts the manifest to submit to the builds service | ||||
| // in case of an error the []byte might contain an error that can be displayed to the user | ||||
| func (pc sourcehutConvertor) buildManifest(repo *api.Repository, commitID, gitRef string) ([]byte, error) { | ||||
| 	gitRepo, err := gitrepo.OpenRepository(pc.ctx, repo) | ||||
| 	if err != nil { | ||||
| 		msg := "could not open repository" | ||||
| 		return []byte(msg), fmt.Errorf(msg+": %w", err) | ||||
| 	} | ||||
| 	defer gitRepo.Close() | ||||
| 
 | ||||
| 	commit, err := gitRepo.GetCommit(commitID) | ||||
| 	if err != nil { | ||||
| 		msg := fmt.Sprintf("could not get commit %q", commitID) | ||||
| 		return []byte(msg), fmt.Errorf(msg+": %w", err) | ||||
| 	} | ||||
| 	entry, err := commit.GetTreeEntryByPath(pc.meta.ManifestPath) | ||||
| 	if err != nil { | ||||
| 		msg := fmt.Sprintf("could not open manifest %q", pc.meta.ManifestPath) | ||||
| 		return []byte(msg), fmt.Errorf(msg+": %w", err) | ||||
| 	} | ||||
| 	r, err := entry.Blob().DataAsync() | ||||
| 	if err != nil { | ||||
| 		msg := fmt.Sprintf("could not read manifest %q", pc.meta.ManifestPath) | ||||
| 		return []byte(msg), fmt.Errorf(msg+": %w", err) | ||||
| 	} | ||||
| 	defer r.Close() | ||||
| 	var manifest struct { | ||||
| 		Image        string              `yaml:"image"` | ||||
| 		Arch         string              `yaml:"arch,omitempty"` | ||||
| 		Packages     []string            `yaml:"packages,omitempty"` | ||||
| 		Repositories map[string]string   `yaml:"repositories,omitempty"` | ||||
| 		Artifacts    []string            `yaml:"artifacts,omitempty"` | ||||
| 		Shell        bool                `yaml:"shell,omitempty"` | ||||
| 		Sources      []string            `yaml:"sources"` | ||||
| 		Tasks        []map[string]string `yaml:"tasks"` | ||||
| 		Triggers     []string            `yaml:"triggers,omitempty"` | ||||
| 		Environment  map[string]string   `yaml:"environment"` | ||||
| 		Secrets      []string            `yaml:"secrets,omitempty"` | ||||
| 		Oauth        string              `yaml:"oauth,omitempty"` | ||||
| 	} | ||||
| 	if err := yaml.NewDecoder(r).Decode(&manifest); err != nil { | ||||
| 		msg := fmt.Sprintf("could not decode manifest %q", pc.meta.ManifestPath) | ||||
| 		return []byte(msg), fmt.Errorf(msg+": %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if manifest.Environment == nil { | ||||
| 		manifest.Environment = make(map[string]string) | ||||
| 	} | ||||
| 	manifest.Environment["BUILD_SUBMITTER"] = "forgejo" | ||||
| 	manifest.Environment["BUILD_SUBMITTER_URL"] = setting.AppURL | ||||
| 	manifest.Environment["GIT_REF"] = gitRef | ||||
| 
 | ||||
| 	source := repo.CloneURL + "#" + commitID | ||||
| 	found := false | ||||
| 	for i, s := range manifest.Sources { | ||||
| 		if s == repo.CloneURL { | ||||
| 			manifest.Sources[i] = source | ||||
| 			found = true | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 	if !found { | ||||
| 		manifest.Sources = append(manifest.Sources, source) | ||||
| 	} | ||||
| 
 | ||||
| 	return yaml.Marshal(manifest) | ||||
| } | ||||
							
								
								
									
										440
									
								
								services/webhook/sourcehut/builds_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										440
									
								
								services/webhook/sourcehut/builds_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,440 @@ | |||
| // Copyright 2024 The Forgejo Authors. All rights reserved. | ||||
| // SPDX-License-Identifier: MIT | ||||
| 
 | ||||
| package sourcehut | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	unit_model "code.gitea.io/gitea/models/unit" | ||||
| 	user_model "code.gitea.io/gitea/models/user" | ||||
| 	webhook_model "code.gitea.io/gitea/models/webhook" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/test" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| 	files_service "code.gitea.io/gitea/services/repository/files" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
| 
 | ||||
| func gitInit(t testing.TB) { | ||||
| 	if setting.Git.HomePath != "" { | ||||
| 		return | ||||
| 	} | ||||
| 	t.Cleanup(test.MockVariableValue(&setting.Git.HomePath, t.TempDir())) | ||||
| 	assert.NoError(t, git.InitSimple(context.Background())) | ||||
| } | ||||
| 
 | ||||
| func TestSourcehutBuildsPayload(t *testing.T) { | ||||
| 	gitInit(t) | ||||
| 	defer test.MockVariableValue(&setting.RepoRootPath, ".")() | ||||
| 	defer test.MockVariableValue(&setting.AppURL, "https://example.forgejo.org/")() | ||||
| 
 | ||||
| 	repo := &api.Repository{ | ||||
| 		HTMLURL:  "http://localhost:3000/testdata/repo", | ||||
| 		Name:     "repo", | ||||
| 		FullName: "testdata/repo", | ||||
| 		Owner: &api.User{ | ||||
| 			UserName: "testdata", | ||||
| 		}, | ||||
| 		CloneURL: "http://localhost:3000/testdata/repo.git", | ||||
| 	} | ||||
| 
 | ||||
| 	pc := sourcehutConvertor{ | ||||
| 		ctx: git.DefaultContext, | ||||
| 		meta: BuildsMeta{ | ||||
| 			ManifestPath: "adjust me in each test", | ||||
| 			Visibility:   "UNLISTED", | ||||
| 			Secrets:      true, | ||||
| 		}, | ||||
| 	} | ||||
| 	t.Run("Create/branch", func(t *testing.T) { | ||||
| 		p := &api.CreatePayload{ | ||||
| 			Sha:     "58771003157b81abc6bf41df0c5db4147a3e3c83", | ||||
| 			Ref:     "refs/heads/test", | ||||
| 			RefType: "branch", | ||||
| 			Repo:    repo, | ||||
| 		} | ||||
| 
 | ||||
| 		pc.meta.ManifestPath = "simple.yml" | ||||
| 		pl, err := pc.Create(p) | ||||
| 		require.NoError(t, err) | ||||
| 		assert.Equal(t, buildsVariables{ | ||||
| 			Manifest: `image: alpine/edge | ||||
| sources: | ||||
|     - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 | ||||
| tasks: | ||||
|     - say-hello: | | ||||
|         echo hello | ||||
|     - say-world: echo world | ||||
| environment: | ||||
|     BUILD_SUBMITTER: forgejo | ||||
|     BUILD_SUBMITTER_URL: https://example.forgejo.org/ | ||||
|     GIT_REF: refs/heads/test | ||||
| `, | ||||
| 			Note:       "branch test created", | ||||
| 			Tags:       []string{"testdata/repo", "branch/test", "simple.yml"}, | ||||
| 			Secrets:    true, | ||||
| 			Execute:    true, | ||||
| 			Visibility: "UNLISTED", | ||||
| 		}, pl.Variables) | ||||
| 	}) | ||||
| 	t.Run("Create/tag", func(t *testing.T) { | ||||
| 		p := &api.CreatePayload{ | ||||
| 			Sha:     "58771003157b81abc6bf41df0c5db4147a3e3c83", | ||||
| 			Ref:     "refs/tags/v1.0.0", | ||||
| 			RefType: "tag", | ||||
| 			Repo:    repo, | ||||
| 		} | ||||
| 
 | ||||
| 		pc.meta.ManifestPath = "simple.yml" | ||||
| 		pl, err := pc.Create(p) | ||||
| 		require.NoError(t, err) | ||||
| 		assert.Equal(t, buildsVariables{ | ||||
| 			Manifest: `image: alpine/edge | ||||
| sources: | ||||
|     - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 | ||||
| tasks: | ||||
|     - say-hello: | | ||||
|         echo hello | ||||
|     - say-world: echo world | ||||
| environment: | ||||
|     BUILD_SUBMITTER: forgejo | ||||
|     BUILD_SUBMITTER_URL: https://example.forgejo.org/ | ||||
|     GIT_REF: refs/tags/v1.0.0 | ||||
| `, | ||||
| 			Note:       "tag v1.0.0 created", | ||||
| 			Tags:       []string{"testdata/repo", "tag/v1.0.0", "simple.yml"}, | ||||
| 			Secrets:    true, | ||||
| 			Execute:    true, | ||||
| 			Visibility: "UNLISTED", | ||||
| 		}, pl.Variables) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Delete", func(t *testing.T) { | ||||
| 		p := &api.DeletePayload{} | ||||
| 
 | ||||
| 		pl, err := pc.Delete(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Fork", func(t *testing.T) { | ||||
| 		p := &api.ForkPayload{} | ||||
| 
 | ||||
| 		pl, err := pc.Fork(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Push/simple", func(t *testing.T) { | ||||
| 		p := &api.PushPayload{ | ||||
| 			Ref: "refs/heads/main", | ||||
| 			HeadCommit: &api.PayloadCommit{ | ||||
| 				ID:      "58771003157b81abc6bf41df0c5db4147a3e3c83", | ||||
| 				Message: "add simple", | ||||
| 			}, | ||||
| 			Repo: repo, | ||||
| 		} | ||||
| 
 | ||||
| 		pc.meta.ManifestPath = "simple.yml" | ||||
| 		pl, err := pc.Push(p) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, buildsVariables{ | ||||
| 			Manifest: `image: alpine/edge | ||||
| sources: | ||||
|     - http://localhost:3000/testdata/repo.git#58771003157b81abc6bf41df0c5db4147a3e3c83 | ||||
| tasks: | ||||
|     - say-hello: | | ||||
|         echo hello | ||||
|     - say-world: echo world | ||||
| environment: | ||||
|     BUILD_SUBMITTER: forgejo | ||||
|     BUILD_SUBMITTER_URL: https://example.forgejo.org/ | ||||
|     GIT_REF: refs/heads/main | ||||
| `, | ||||
| 			Note:       "add simple", | ||||
| 			Tags:       []string{"testdata/repo", "branch/main", "simple.yml"}, | ||||
| 			Secrets:    true, | ||||
| 			Execute:    true, | ||||
| 			Visibility: "UNLISTED", | ||||
| 		}, pl.Variables) | ||||
| 	}) | ||||
| 	t.Run("Push/complex", func(t *testing.T) { | ||||
| 		p := &api.PushPayload{ | ||||
| 			Ref: "refs/heads/main", | ||||
| 			HeadCommit: &api.PayloadCommit{ | ||||
| 				ID:      "69b217caa89166a02b8cd368b64fb83a44720e14", | ||||
| 				Message: "replace simple with complex", | ||||
| 			}, | ||||
| 			Repo: repo, | ||||
| 		} | ||||
| 
 | ||||
| 		pc.meta.ManifestPath = "complex.yaml" | ||||
| 		pc.meta.Visibility = "PRIVATE" | ||||
| 		pc.meta.Secrets = false | ||||
| 		pl, err := pc.Push(p) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, buildsVariables{ | ||||
| 			Manifest: `image: archlinux | ||||
| packages: | ||||
|     - nodejs | ||||
|     - npm | ||||
|     - rsync | ||||
| sources: | ||||
|     - http://localhost:3000/testdata/repo.git#69b217caa89166a02b8cd368b64fb83a44720e14 | ||||
| tasks: [] | ||||
| environment: | ||||
|     BUILD_SUBMITTER: forgejo | ||||
|     BUILD_SUBMITTER_URL: https://example.forgejo.org/ | ||||
|     GIT_REF: refs/heads/main | ||||
|     deploy: synapse@synapse-bt.org | ||||
| secrets: | ||||
|     - 7ebab768-e5e4-4c9d-ba57-ec41a72c5665 | ||||
| `, | ||||
| 			Note:       "replace simple with complex", | ||||
| 			Tags:       []string{"testdata/repo", "branch/main", "complex.yaml"}, | ||||
| 			Secrets:    false, | ||||
| 			Execute:    true, | ||||
| 			Visibility: "PRIVATE", | ||||
| 		}, pl.Variables) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Push/error", func(t *testing.T) { | ||||
| 		p := &api.PushPayload{ | ||||
| 			Ref: "refs/heads/main", | ||||
| 			HeadCommit: &api.PayloadCommit{ | ||||
| 				ID:      "58771003157b81abc6bf41df0c5db4147a3e3c83", | ||||
| 				Message: "add simple", | ||||
| 			}, | ||||
| 			Repo: repo, | ||||
| 		} | ||||
| 
 | ||||
| 		pc.meta.ManifestPath = "non-existing.yml" | ||||
| 		pl, err := pc.Push(p) | ||||
| 		require.NoError(t, err) | ||||
| 
 | ||||
| 		assert.Equal(t, graphqlPayload[buildsVariables]{ | ||||
| 			Error: "testdata/repo:refs/heads/main could not open manifest \"non-existing.yml\"", | ||||
| 		}, pl) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Issue", func(t *testing.T) { | ||||
| 		p := &api.IssuePayload{} | ||||
| 
 | ||||
| 		p.Action = api.HookIssueOpened | ||||
| 		pl, err := pc.Issue(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 
 | ||||
| 		p.Action = api.HookIssueClosed | ||||
| 		pl, err = pc.Issue(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("IssueComment", func(t *testing.T) { | ||||
| 		p := &api.IssueCommentPayload{} | ||||
| 
 | ||||
| 		pl, err := pc.IssueComment(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("PullRequest", func(t *testing.T) { | ||||
| 		p := &api.PullRequestPayload{} | ||||
| 
 | ||||
| 		pl, err := pc.PullRequest(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("PullRequestComment", func(t *testing.T) { | ||||
| 		p := &api.IssueCommentPayload{ | ||||
| 			IsPull: true, | ||||
| 		} | ||||
| 
 | ||||
| 		pl, err := pc.IssueComment(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Review", func(t *testing.T) { | ||||
| 		p := &api.PullRequestPayload{} | ||||
| 		p.Action = api.HookIssueReviewed | ||||
| 
 | ||||
| 		pl, err := pc.Review(p, webhook_module.HookEventPullRequestReviewApproved) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Repository", func(t *testing.T) { | ||||
| 		p := &api.RepositoryPayload{} | ||||
| 
 | ||||
| 		pl, err := pc.Repository(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Package", func(t *testing.T) { | ||||
| 		p := &api.PackagePayload{} | ||||
| 
 | ||||
| 		pl, err := pc.Package(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Wiki", func(t *testing.T) { | ||||
| 		p := &api.WikiPayload{} | ||||
| 
 | ||||
| 		p.Action = api.HookWikiCreated | ||||
| 		pl, err := pc.Wiki(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 
 | ||||
| 		p.Action = api.HookWikiEdited | ||||
| 		pl, err = pc.Wiki(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 
 | ||||
| 		p.Action = api.HookWikiDeleted | ||||
| 		pl, err = pc.Wiki(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Release", func(t *testing.T) { | ||||
| 		p := &api.ReleasePayload{} | ||||
| 
 | ||||
| 		pl, err := pc.Release(p) | ||||
| 		require.Equal(t, err, shared.ErrPayloadTypeNotSupported) | ||||
| 		require.Equal(t, pl, graphqlPayload[buildsVariables]{}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestSourcehutJSONPayload(t *testing.T) { | ||||
| 	gitInit(t) | ||||
| 	defer test.MockVariableValue(&setting.RepoRootPath, ".")() | ||||
| 	defer test.MockVariableValue(&setting.AppURL, "https://example.forgejo.org/")() | ||||
| 
 | ||||
| 	repo := &api.Repository{ | ||||
| 		HTMLURL:  "http://localhost:3000/testdata/repo", | ||||
| 		Name:     "repo", | ||||
| 		FullName: "testdata/repo", | ||||
| 		Owner: &api.User{ | ||||
| 			UserName: "testdata", | ||||
| 		}, | ||||
| 		CloneURL: "http://localhost:3000/testdata/repo.git", | ||||
| 	} | ||||
| 
 | ||||
| 	p := &api.PushPayload{ | ||||
| 		Ref: "refs/heads/main", | ||||
| 		HeadCommit: &api.PayloadCommit{ | ||||
| 			ID:      "58771003157b81abc6bf41df0c5db4147a3e3c83", | ||||
| 			Message: "json test", | ||||
| 		}, | ||||
| 		Repo: repo, | ||||
| 	} | ||||
| 	data, err := p.JSONPayload() | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	hook := &webhook_model.Webhook{ | ||||
| 		RepoID:   3, | ||||
| 		IsActive: true, | ||||
| 		Type:     webhook_module.MATRIX, | ||||
| 		URL:      "https://sourcehut.example.com/api/jobs", | ||||
| 		Meta:     `{"manifest_path":"simple.yml"}`, | ||||
| 	} | ||||
| 	task := &webhook_model.HookTask{ | ||||
| 		HookID:         hook.ID, | ||||
| 		EventType:      webhook_module.HookEventPush, | ||||
| 		PayloadContent: string(data), | ||||
| 		PayloadVersion: 2, | ||||
| 	} | ||||
| 
 | ||||
| 	req, reqBody, err := BuildsHandler{}.NewRequest(context.Background(), hook, task) | ||||
| 	require.NoError(t, err) | ||||
| 	require.NotNil(t, req) | ||||
| 	require.NotNil(t, reqBody) | ||||
| 
 | ||||
| 	assert.Equal(t, "POST", req.Method) | ||||
| 	assert.Equal(t, "/api/jobs", req.URL.Path) | ||||
| 	assert.Equal(t, "application/json", req.Header.Get("Content-Type")) | ||||
| 	var body graphqlPayload[buildsVariables] | ||||
| 	err = json.NewDecoder(req.Body).Decode(&body) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, "json test", body.Variables.Note) | ||||
| } | ||||
| 
 | ||||
| func CreateDeclarativeRepo(t *testing.T, owner *user_model.User, name string, enabledUnits, disabledUnits []unit_model.Type, files []*files_service.ChangeRepoFile) (*repo_model.Repository, string) { | ||||
| 	t.Helper() | ||||
| 
 | ||||
| 	// Create a new repository | ||||
| 	repo, err := repo_service.CreateRepository(db.DefaultContext, owner, owner, repo_service.CreateRepoOptions{ | ||||
| 		Name:          name, | ||||
| 		Description:   "Temporary Repo", | ||||
| 		AutoInit:      true, | ||||
| 		Gitignores:    "", | ||||
| 		License:       "WTFPL", | ||||
| 		Readme:        "Default", | ||||
| 		DefaultBranch: "main", | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NotEmpty(t, repo) | ||||
| 	t.Cleanup(func() { | ||||
| 		repo_service.DeleteRepository(db.DefaultContext, owner, repo, false) | ||||
| 	}) | ||||
| 
 | ||||
| 	if enabledUnits != nil || disabledUnits != nil { | ||||
| 		units := make([]repo_model.RepoUnit, len(enabledUnits)) | ||||
| 		for i, unitType := range enabledUnits { | ||||
| 			units[i] = repo_model.RepoUnit{ | ||||
| 				RepoID: repo.ID, | ||||
| 				Type:   unitType, | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		err := repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, units, disabledUnits) | ||||
| 		assert.NoError(t, err) | ||||
| 	} | ||||
| 
 | ||||
| 	var sha string | ||||
| 	if len(files) > 0 { | ||||
| 		resp, err := files_service.ChangeRepoFiles(git.DefaultContext, repo, owner, &files_service.ChangeRepoFilesOptions{ | ||||
| 			Files:     files, | ||||
| 			Message:   "add files", | ||||
| 			OldBranch: "main", | ||||
| 			NewBranch: "main", | ||||
| 			Author: &files_service.IdentityOptions{ | ||||
| 				Name:  owner.Name, | ||||
| 				Email: owner.Email, | ||||
| 			}, | ||||
| 			Committer: &files_service.IdentityOptions{ | ||||
| 				Name:  owner.Name, | ||||
| 				Email: owner.Email, | ||||
| 			}, | ||||
| 			Dates: &files_service.CommitDateOptions{ | ||||
| 				Author:    time.Now(), | ||||
| 				Committer: time.Now(), | ||||
| 			}, | ||||
| 		}) | ||||
| 		assert.NoError(t, err) | ||||
| 		assert.NotEmpty(t, resp) | ||||
| 
 | ||||
| 		sha = resp.Commit.SHA | ||||
| 	} | ||||
| 
 | ||||
| 	return repo, sha | ||||
| } | ||||
							
								
								
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/HEAD
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/HEAD
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| ref: refs/heads/main | ||||
							
								
								
									
										4
									
								
								services/webhook/sourcehut/testdata/repo.git/config
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								services/webhook/sourcehut/testdata/repo.git/config
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | |||
| [core] | ||||
| 	repositoryformatversion = 0 | ||||
| 	filemode = true | ||||
| 	bare = true | ||||
							
								
								
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/description
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/description
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| Unnamed repository; edit this file 'description' to name the repository. | ||||
							
								
								
									
										6
									
								
								services/webhook/sourcehut/testdata/repo.git/info/exclude
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								services/webhook/sourcehut/testdata/repo.git/info/exclude
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| # git ls-files --others --exclude-from=.git/info/exclude | ||||
| # Lines that start with '#' are comments. | ||||
| # For a project mostly in C, the following would be a good set of | ||||
| # exclude patterns (uncomment them if you want to use them): | ||||
| # *.[oa] | ||||
| # *~ | ||||
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/01/16b5e2279b70e5f5b98240e8896331248f4463
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/01/16b5e2279b70e5f5b98240e8896331248f4463
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/3c/3d4b799b3933ba687b263eeef2034300a5315e
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/3c/3d4b799b3933ba687b263eeef2034300a5315e
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1 @@ | |||
| x•NKjÃ0ìZ§xûBÑçɶ ”¬z<C2AC>ççQã[FQÚ?"=A3óѲmk#ÏüÒ*@š²L3&²)ú”'D$ #²Î’ƒæ<>Š½Ñ¼,#/³„8²Ov‰ƒzIN<Áu'¨‘[;—JŸ¥~á»Ð{þ#'Üe;.xëòƒÜ輋#[K¯Ö[kôy¯áßASq\DA›ìkƵÑïÚÎÔûúØ<C3BA>~P¯kÙÍÂVO<56> | ||||
|  | @ -0,0 +1,2 @@ | |||
| x=<3D>Α‚0D=χ+φnBΊX<>ΓΙ<CE93>hιVk¨%¥?_PγmήαΝΜ”b°ΗCΙΜ Ή±Dδ{΄ | ||||
| ;ƒµF’&«”q®λ™m¥“Β<Κ5e8§|α[‚ΑΓΘ/—™«
O€„5¶¤	GYK)¦Ο\αiOΞKJ3—PΖ<50>ηjρΖU><3E>έVΣΟΫXΓήΡάƒηµ<CEB7>7\p;Ό | ||||
|  | @ -0,0 +1 @@ | |||
| x=ŽÍnà „{æ)ö^ÉZ,EUN}<7D>ï&T¶A„¶yüÒõ6ßa¾™Tö=w˜ê<7F>ˆÌ‚ŽÄ¢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» | ||||
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/9e/4b777f81b316a1c75a0797b33add68ee49b0d0
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/9e/4b777f81b316a1c75a0797b33add68ee49b0d0
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/aa/3905af404394f576f88f00e7f0919b4b97453f
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/aa/3905af404394f576f88f00e7f0919b4b97453f
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/bd/d74dba3f34542e480d56c2a91c9e2463180d3c
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/bd/d74dba3f34542e480d56c2a91c9e2463180d3c
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/c2/30778a03511f546f532e79c8c14917c260e750
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/c2/30778a03511f546f532e79c8c14917c260e750
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/c9/fa8be8c6f230e9529f8e546a3e6a354cbaf313
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								services/webhook/sourcehut/testdata/repo.git/objects/c9/fa8be8c6f230e9529f8e546a3e6a354cbaf313
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1 @@ | |||
| x=ЋKnГ0D»Ц)ё`иkЙ@PdХ{P2™Ё°-AQ›їк]АYјЗIeЯsmэKoDђЖ)¤™µ‘ґ8Ѕp gg44вlЉИFQ±ССБп•”F9ѓВ<D193>жИV,“[ЈUЦО¤`~ф[iрVЪ•ЮњщчёРчєС4к+(Їф0Y)б$µ”"эМлФ lщ“Z-eѓу5чЛwПФ¦КёNЬюЩY»?V4Є&‚ЏМtпрИэC9ю=aШо№в™,PЎ | ||||
|  | @ -0,0 +1,4 @@ | |||
| xENInÃ0ìY¯Ð»®—D§þ#È<>¢	Û<>, | ||||
| "$¿¯<C2BF>¦É\fÁ™9ئ9~,+Lä-œã’¶»É€×=oìg<13>ô#ÿ&¯OUä‘Ðoß·³jöU!Î,ê¿êº®”DGP¨ | ||||
| e>L‹¹Š·ç‹¡t[ | ||||
| §•’þŽ”#?¼ÝßCú~²zà2!,¤¯qCtÔQëZ<<3C>.@78Âö†»¾ïŒù\«I | ||||
							
								
								
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/refs/heads/main
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								services/webhook/sourcehut/testdata/repo.git/refs/heads/main
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| 69b217caa89166a02b8cd368b64fb83a44720e14 | ||||
|  | @ -18,28 +18,29 @@ import ( | |||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type telegramHandler struct{} | ||||
| 
 | ||||
| func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM } | ||||
| func (telegramHandler) Icon(size int) template.HTML   { return imgIcon("telegram.png", size) } | ||||
| func (telegramHandler) Icon(size int) template.HTML   { return shared.ImgIcon("telegram.png", size) } | ||||
| 
 | ||||
| func (telegramHandler) FormFields(bind func(any)) FormFields { | ||||
| func (telegramHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		BotToken string `binding:"Required"` | ||||
| 		ChatID   string `binding:"Required"` | ||||
| 		ThreadID string | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		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)), | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		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)), | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata: &TelegramMeta{ | ||||
| 			BotToken: form.BotToken, | ||||
| 			ChatID:   form.ChatID, | ||||
|  | @ -220,8 +221,8 @@ func createTelegramPayload(message string) TelegramPayload { | |||
| 
 | ||||
| type telegramConvertor struct{} | ||||
| 
 | ||||
| var _ payloadConvertor[TelegramPayload] = telegramConvertor{} | ||||
| var _ shared.PayloadConvertor[TelegramPayload] = telegramConvertor{} | ||||
| 
 | ||||
| func (telegramHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	return newJSONRequest(telegramConvertor{}, w, t, true) | ||||
| 	return shared.NewJSONRequest(telegramConvertor{}, w, t, true) | ||||
| } | ||||
|  |  | |||
|  | @ -25,6 +25,7 @@ import ( | |||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/sourcehut" | ||||
| 
 | ||||
| 	"github.com/gobwas/glob" | ||||
| ) | ||||
|  | @ -32,22 +33,13 @@ import ( | |||
| type Handler interface { | ||||
| 	Type() webhook_module.HookType | ||||
| 	Metadata(*webhook_model.Webhook) any | ||||
| 	// FormFields provides a function to bind the request to the form. | ||||
| 	// UnmarshalForm provides a function to bind the request to the form. | ||||
| 	// If form implements the [binding.Validator] interface, the Validate method will be called | ||||
| 	FormFields(bind func(form any)) FormFields | ||||
| 	UnmarshalForm(bind func(form any)) forms.WebhookForm | ||||
| 	NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error) | ||||
| 	Icon(size int) template.HTML | ||||
| } | ||||
| 
 | ||||
| type FormFields struct { | ||||
| 	forms.WebhookForm | ||||
| 	URL         string | ||||
| 	ContentType webhook_model.HookContentType | ||||
| 	Secret      string | ||||
| 	HTTPMethod  string | ||||
| 	Metadata    any | ||||
| } | ||||
| 
 | ||||
| var webhookHandlers = []Handler{ | ||||
| 	defaultHandler{true}, | ||||
| 	defaultHandler{false}, | ||||
|  | @ -62,6 +54,7 @@ var webhookHandlers = []Handler{ | |||
| 	matrixHandler{}, | ||||
| 	wechatworkHandler{}, | ||||
| 	packagistHandler{}, | ||||
| 	sourcehut.BuildsHandler{}, | ||||
| } | ||||
| 
 | ||||
| // GetWebhookHandler return the handler for a given webhook type (nil if not found) | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import ( | |||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 	webhook_module "code.gitea.io/gitea/modules/webhook" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
| 	"code.gitea.io/gitea/services/webhook/shared" | ||||
| ) | ||||
| 
 | ||||
| type wechatworkHandler struct{} | ||||
|  | @ -23,23 +24,23 @@ func (wechatworkHandler) Type() webhook_module.HookType       { return webhook_m | |||
| func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil } | ||||
| 
 | ||||
| func (wechatworkHandler) Icon(size int) template.HTML { | ||||
| 	return imgIcon("wechatwork.png", size) | ||||
| 	return shared.ImgIcon("wechatwork.png", size) | ||||
| } | ||||
| 
 | ||||
| func (wechatworkHandler) FormFields(bind func(any)) FormFields { | ||||
| func (wechatworkHandler) UnmarshalForm(bind func(any)) forms.WebhookForm { | ||||
| 	var form struct { | ||||
| 		forms.WebhookForm | ||||
| 		forms.WebhookCoreForm | ||||
| 		PayloadURL string `binding:"Required;ValidUrl"` | ||||
| 	} | ||||
| 	bind(&form) | ||||
| 
 | ||||
| 	return FormFields{ | ||||
| 		WebhookForm: form.WebhookForm, | ||||
| 		URL:         form.PayloadURL, | ||||
| 		ContentType: webhook_model.ContentTypeJSON, | ||||
| 		Secret:      "", | ||||
| 		HTTPMethod:  http.MethodPost, | ||||
| 		Metadata:    nil, | ||||
| 	return forms.WebhookForm{ | ||||
| 		WebhookCoreForm: form.WebhookCoreForm, | ||||
| 		URL:             form.PayloadURL, | ||||
| 		ContentType:     webhook_model.ContentTypeJSON, | ||||
| 		Secret:          "", | ||||
| 		HTTPMethod:      http.MethodPost, | ||||
| 		Metadata:        nil, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -203,8 +204,8 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload, | |||
| 
 | ||||
| type wechatworkConvertor struct{} | ||||
| 
 | ||||
| var _ payloadConvertor[WechatworkPayload] = wechatworkConvertor{} | ||||
| var _ shared.PayloadConvertor[WechatworkPayload] = wechatworkConvertor{} | ||||
| 
 | ||||
| func (wechatworkHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) { | ||||
| 	return newJSONRequest(wechatworkConvertor{}, w, t, true) | ||||
| 	return shared.NewJSONRequest(wechatworkConvertor{}, w, t, true) | ||||
| } | ||||
|  |  | |||
|  | @ -36,6 +36,8 @@ | |||
| 			{{template "webhook/new/wechatwork" .}} | ||||
| 		{{else if eq .HookType "packagist"}} | ||||
| 			{{template "webhook/new/packagist" .}} | ||||
| 		{{else if eq .HookType "sourcehut_builds"}} | ||||
| 			{{template "webhook/new/sourcehut_builds" .}} | ||||
| 		{{end}} | ||||
| 	{{end}} | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										33
									
								
								templates/webhook/new/sourcehut_builds.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								templates/webhook/new/sourcehut_builds.tmpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| <p>{{ctx.Locale.Tr "repo.settings.add_web_hook_desc" "https://sourcehut.org/" (ctx.Locale.Tr "repo.settings.web_hook_name_sourcehut_builds")}}</p> | ||||
| <form class="ui form" action="{{.BaseLink}}/{{or .Webhook.ID "sourcehut_builds/new"}}" method="post"> | ||||
| 	{{.CsrfTokenHtml}} | ||||
| 	<div class="required field {{if .Err_PayloadURL}}error{{end}}"> | ||||
| 		<label for="payload_url">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.graphql_url"}}</label> | ||||
| 		<input id="payload_url" name="payload_url" type="url" value="{{.Webhook.URL}}" autofocus required> | ||||
| 	</div> | ||||
| 	<div class="required field {{if .Err_ManifestPath}}error{{end}}"> | ||||
| 		<label for="manifest_path">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.manifest_path"}}</label> | ||||
| 		<input id="manifest_path" name="manifest_path" type="text" value="{{.HookMetadata.ManifestPath}}" required> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<label>{{ctx.Locale.Tr "repo.settings.sourcehut_builds.visibility"}}</label> | ||||
| 		<div class="ui selection dropdown"> | ||||
| 			<input type="hidden" id="visibility" name="visibility" value="{{if .HookMetadata.Visibility}}{{.HookMetadata.Visibility}}{{else}}PRIVATE{{end}}"> | ||||
| 			<div class="default text"></div> | ||||
| 			{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||
| 			<div class="menu"> | ||||
| 				<div class="item" data-value="PUBLIC">PUBLIC</div> | ||||
| 				<div class="item" data-value="UNLISTED">UNLISTED</div> | ||||
| 				<div class="item" data-value="PRIVATE">PRIVATE</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="field"> | ||||
| 		<div class="ui checkbox"> | ||||
| 			<input name="secrets" type="checkbox" {{if .HookMetadata.Secrets}}checked{{end}}> | ||||
| 			<label>{{ctx.Locale.Tr "repo.settings.sourcehut_builds.secrets"}}</label> | ||||
| 			<span class="help">{{ctx.Locale.Tr "repo.settings.sourcehut_builds.secrets_helper"}}</span> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	{{template "repo/settings/webhook/settings" .}} | ||||
| </form> | ||||
|  | @ -238,6 +238,34 @@ func TestWebhookForms(t *testing.T) { | |||
| 		"branch_filter":        "packagist/*", | ||||
| 		"authorization_header": "Bearer 123456", | ||||
| 	})) | ||||
| 
 | ||||
| 	t.Run("sourcehut_builds/required", testWebhookForms("sourcehut_builds", session, map[string]string{ | ||||
| 		"payload_url":          "https://sourcehut_builds.example.com", | ||||
| 		"manifest_path":        ".build.yml", | ||||
| 		"visibility":           "PRIVATE", | ||||
| 		"authorization_header": "Bearer 123456", | ||||
| 	}, map[string]string{ | ||||
| 		"authorization_header": "", | ||||
| 	}, map[string]string{ | ||||
| 		"authorization_header": "token ", | ||||
| 	}, map[string]string{ | ||||
| 		"manifest_path": "", | ||||
| 	}, map[string]string{ | ||||
| 		"manifest_path": "/absolute", | ||||
| 	}, map[string]string{ | ||||
| 		"visibility": "", | ||||
| 	}, map[string]string{ | ||||
| 		"visibility": "INVALID", | ||||
| 	})) | ||||
| 	t.Run("sourcehut_builds/optional", testWebhookForms("sourcehut_builds", session, map[string]string{ | ||||
| 		"payload_url":   "https://sourcehut_builds.example.com", | ||||
| 		"manifest_path": ".build.yml", | ||||
| 		"visibility":    "PRIVATE", | ||||
| 		"secrets":       "on", | ||||
| 
 | ||||
| 		"branch_filter":        "srht/*", | ||||
| 		"authorization_header": "Bearer 123456", | ||||
| 	})) | ||||
| } | ||||
| 
 | ||||
| func assertInput(t testing.TB, form *goquery.Selection, name string) string { | ||||
|  | @ -247,7 +275,15 @@ func assertInput(t testing.TB, form *goquery.Selection, name string) string { | |||
| 		t.Log(form.Html()) | ||||
| 		t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length()) | ||||
| 	} | ||||
| 	return input.AttrOr("value", "") | ||||
| 	switch input.AttrOr("type", "") { | ||||
| 	case "checkbox": | ||||
| 		if _, checked := input.Attr("checked"); checked { | ||||
| 			return "on" | ||||
| 		} | ||||
| 		return "" | ||||
| 	default: | ||||
| 		return input.AttrOr("value", "") | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue