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"`
|
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
|
// CreateRepoOption options when creating repository
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type CreateRepoOption struct {
|
type CreateRepoOption struct {
|
||||||
|
|
|
@ -85,6 +85,7 @@ const (
|
||||||
MATRIX HookType = "matrix"
|
MATRIX HookType = "matrix"
|
||||||
WECHATWORK HookType = "wechatwork"
|
WECHATWORK HookType = "wechatwork"
|
||||||
PACKAGIST HookType = "packagist"
|
PACKAGIST HookType = "packagist"
|
||||||
|
SOURCEHUT_BUILDS HookType = "sourcehut_builds" //nolint:revive
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookStatus is the status of a web hook
|
// 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.
|
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]
|
[user]
|
||||||
change_avatar = Change your avatar…
|
change_avatar = Change your avatar…
|
||||||
joined_on = Joined on %s
|
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.remove_team_success = The team's access to the repository has been removed.
|
||||||
settings.add_webhook = Add webhook
|
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_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.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 = Remove webhook
|
||||||
settings.webhook_deletion_desc = Removing a webhook deletes its settings and delivery history. Continue?
|
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_username = Packagist username
|
||||||
settings.packagist_api_token = API token
|
settings.packagist_api_token = API token
|
||||||
settings.packagist_package_url = Packagist package URL
|
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.deploy_keys = Deploy keys
|
||||||
settings.add_deploy_key = Add deploy key
|
settings.add_deploy_key = Add deploy key
|
||||||
settings.deploy_key_desc = Deploy keys have read-only pull access to the repository.
|
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
|
// 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{
|
return &webhook_module.HookEvent{
|
||||||
PushOnly: form.PushOnly(),
|
PushOnly: form.PushOnly(),
|
||||||
SendEverything: form.SendEverything(),
|
SendEverything: form.SendEverything(),
|
||||||
|
@ -188,7 +188,7 @@ func WebhookCreate(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := handler.FormFields(func(form any) {
|
fields := handler.UnmarshalForm(func(form any) {
|
||||||
errs := binding.Bind(ctx.Req, form)
|
errs := binding.Bind(ctx.Req, form)
|
||||||
middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError
|
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.URL = fields.URL
|
||||||
w.ContentType = fields.ContentType
|
w.ContentType = fields.ContentType
|
||||||
w.Secret = fields.Secret
|
w.Secret = fields.Secret
|
||||||
w.HookEvent = ParseHookEvent(fields.WebhookForm)
|
w.HookEvent = ParseHookEvent(fields.WebhookCoreForm)
|
||||||
w.IsActive = fields.WebhookForm.Active
|
w.IsActive = fields.Active
|
||||||
w.HTTPMethod = fields.HTTPMethod
|
w.HTTPMethod = fields.HTTPMethod
|
||||||
err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
|
err := w.SetHeaderAuthorization(fields.AuthorizationHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SetHeaderAuthorization", err)
|
ctx.ServerError("SetHeaderAuthorization", err)
|
||||||
return
|
return
|
||||||
|
@ -245,14 +245,14 @@ func WebhookCreate(ctx *context.Context) {
|
||||||
HTTPMethod: fields.HTTPMethod,
|
HTTPMethod: fields.HTTPMethod,
|
||||||
ContentType: fields.ContentType,
|
ContentType: fields.ContentType,
|
||||||
Secret: fields.Secret,
|
Secret: fields.Secret,
|
||||||
HookEvent: ParseHookEvent(fields.WebhookForm),
|
HookEvent: ParseHookEvent(fields.WebhookCoreForm),
|
||||||
IsActive: fields.WebhookForm.Active,
|
IsActive: fields.Active,
|
||||||
Type: hookType,
|
Type: hookType,
|
||||||
Meta: string(meta),
|
Meta: string(meta),
|
||||||
OwnerID: orCtx.OwnerID,
|
OwnerID: orCtx.OwnerID,
|
||||||
IsSystemWebhook: orCtx.IsSystemWebhook,
|
IsSystemWebhook: orCtx.IsSystemWebhook,
|
||||||
}
|
}
|
||||||
err = w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
|
err = w.SetHeaderAuthorization(fields.AuthorizationHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SetHeaderAuthorization", err)
|
ctx.ServerError("SetHeaderAuthorization", err)
|
||||||
return
|
return
|
||||||
|
@ -286,7 +286,7 @@ func WebhookUpdate(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := handler.FormFields(func(form any) {
|
fields := handler.UnmarshalForm(func(form any) {
|
||||||
errs := binding.Bind(ctx.Req, form)
|
errs := binding.Bind(ctx.Req, form)
|
||||||
middleware.Validate(errs, ctx.Data, form, ctx.Locale) // error checked below in ctx.HasError
|
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.URL = fields.URL
|
||||||
w.ContentType = fields.ContentType
|
w.ContentType = fields.ContentType
|
||||||
w.Secret = fields.Secret
|
w.Secret = fields.Secret
|
||||||
w.HookEvent = ParseHookEvent(fields.WebhookForm)
|
w.HookEvent = ParseHookEvent(fields.WebhookCoreForm)
|
||||||
w.IsActive = fields.WebhookForm.Active
|
w.IsActive = fields.Active
|
||||||
w.HTTPMethod = fields.HTTPMethod
|
w.HTTPMethod = fields.HTTPMethod
|
||||||
|
|
||||||
err := w.SetHeaderAuthorization(fields.WebhookForm.AuthorizationHeader)
|
err := w.SetHeaderAuthorization(fields.AuthorizationHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("SetHeaderAuthorization", err)
|
ctx.ServerError("SetHeaderAuthorization", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
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/setting"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"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
|
// WebhookCoreForm form for changing web hook (common to all webhook types)
|
||||||
type WebhookForm struct {
|
type WebhookCoreForm struct {
|
||||||
Events string
|
Events string
|
||||||
Create bool
|
Create bool
|
||||||
Delete bool
|
Delete bool
|
||||||
|
@ -265,20 +266,30 @@ type WebhookForm struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PushOnly if the hook will be triggered when push
|
// PushOnly if the hook will be triggered when push
|
||||||
func (f WebhookForm) PushOnly() bool {
|
func (f WebhookCoreForm) PushOnly() bool {
|
||||||
return f.Events == "push_only"
|
return f.Events == "push_only"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendEverything if the hook will be triggered any event
|
// SendEverything if the hook will be triggered any event
|
||||||
func (f WebhookForm) SendEverything() bool {
|
func (f WebhookCoreForm) SendEverything() bool {
|
||||||
return f.Events == "send_everything"
|
return f.Events == "send_everything"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChooseEvents if the hook will be triggered choose events
|
// ChooseEvents if the hook will be triggered choose events
|
||||||
func (f WebhookForm) ChooseEvents() bool {
|
func (f WebhookCoreForm) ChooseEvents() bool {
|
||||||
return f.Events == "choose_events"
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -21,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/svg"
|
"code.gitea.io/gitea/modules/svg"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Handler = defaultHandler{}
|
var _ Handler = defaultHandler{}
|
||||||
|
@ -39,16 +35,16 @@ func (dh defaultHandler) Type() webhook_module.HookType {
|
||||||
func (dh defaultHandler) Icon(size int) template.HTML {
|
func (dh defaultHandler) Icon(size int) template.HTML {
|
||||||
if dh.forgejo {
|
if dh.forgejo {
|
||||||
// forgejo.svg is not in web_src/svg/, so svg.RenderHTML does not work
|
// 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")
|
return svg.RenderHTML("gitea-gitea", size, "img")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (defaultHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
HTTPMethod string `binding:"Required;In(POST,GET)"`
|
HTTPMethod string `binding:"Required;In(POST,GET)"`
|
||||||
ContentType int `binding:"Required"`
|
ContentType int `binding:"Required"`
|
||||||
|
@ -60,8 +56,8 @@ func (defaultHandler) FormFields(bind func(any)) FormFields {
|
||||||
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
|
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
|
||||||
contentType = webhook_model.ContentTypeForm
|
contentType = webhook_model.ContentTypeForm
|
||||||
}
|
}
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
Secret: form.Secret,
|
Secret: form.Secret,
|
||||||
|
@ -130,42 +126,5 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
}
|
}
|
||||||
|
|
||||||
body = []byte(t.PayloadContent)
|
body = []byte(t.PayloadContent)
|
||||||
return req, body, addDefaultHeaders(req, []byte(w.Secret), t, body)
|
return req, body, shared.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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,23 +17,24 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dingtalkHandler struct{}
|
type dingtalkHandler struct{}
|
||||||
|
|
||||||
func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK }
|
func (dingtalkHandler) Type() webhook_module.HookType { return webhook_module.DINGTALK }
|
||||||
func (dingtalkHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -225,8 +226,8 @@ func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkP
|
||||||
|
|
||||||
type dingtalkConvertor struct{}
|
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) {
|
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,24 +22,25 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type discordHandler struct{}
|
type discordHandler struct{}
|
||||||
|
|
||||||
func (discordHandler) Type() webhook_module.HookType { return webhook_module.DISCORD }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
Username string
|
Username string
|
||||||
IconURL string
|
IconURL string
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -287,7 +288,7 @@ type discordConvertor struct {
|
||||||
AvatarURL string
|
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) {
|
func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &DiscordMeta{}
|
meta := &DiscordMeta{}
|
||||||
|
@ -298,7 +299,7 @@ func (discordHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
Username: meta.Username,
|
Username: meta.Username,
|
||||||
AvatarURL: meta.IconURL,
|
AvatarURL: meta.IconURL,
|
||||||
}
|
}
|
||||||
return newJSONRequest(sc, w, t, true)
|
return shared.NewJSONRequest(sc, w, t, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
|
func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
|
||||||
|
|
|
@ -15,22 +15,23 @@ import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type feishuHandler struct{}
|
type feishuHandler struct{}
|
||||||
|
|
||||||
func (feishuHandler) Type() webhook_module.HookType { return webhook_module.FEISHU }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -192,8 +193,8 @@ func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error)
|
||||||
|
|
||||||
type feishuConvertor struct{}
|
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) {
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
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(),
|
Created: w.CreatedUnix.AsTime(),
|
||||||
}, nil
|
}, 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_model "code.gitea.io/gitea/models/webhook"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type gogsHandler struct{ defaultHandler }
|
type gogsHandler struct{ defaultHandler }
|
||||||
|
|
||||||
func (gogsHandler) Type() webhook_module.HookType { return webhook_module.GOGS }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
ContentType int `binding:"Required"`
|
ContentType int `binding:"Required"`
|
||||||
Secret string
|
Secret string
|
||||||
|
@ -30,8 +31,8 @@ func (gogsHandler) FormFields(bind func(any)) FormFields {
|
||||||
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
|
if webhook_model.HookContentType(form.ContentType) == webhook_model.ContentTypeForm {
|
||||||
contentType = webhook_model.ContentTypeForm
|
contentType = webhook_model.ContentTypeForm
|
||||||
}
|
}
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
Secret: form.Secret,
|
Secret: form.Secret,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type matrixHandler struct{}
|
type matrixHandler struct{}
|
||||||
|
@ -35,21 +36,21 @@ func (matrixHandler) Icon(size int) template.HTML {
|
||||||
return svg.RenderHTML("gitea-matrix", size, "img")
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
HomeserverURL string `binding:"Required;ValidUrl"`
|
HomeserverURL string `binding:"Required;ValidUrl"`
|
||||||
RoomID string `binding:"Required"`
|
RoomID string `binding:"Required"`
|
||||||
MessageType int
|
MessageType int
|
||||||
|
|
||||||
// enforce requirement of authorization_header
|
// 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"`
|
AuthorizationHeader string `binding:"Required"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
|
URL: fmt.Sprintf("%s/_matrix/client/r0/rooms/%s/send/m.room.message", form.HomeserverURL, url.PathEscape(form.RoomID)),
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -70,7 +71,7 @@ func (matrixHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t
|
||||||
mc := matrixConvertor{
|
mc := matrixConvertor{
|
||||||
MsgType: messageTypeText[meta.MessageType],
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
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")
|
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
|
const matrixPayloadSizeLimit = 1024 * 64
|
||||||
|
@ -125,7 +126,7 @@ type MatrixPayload struct {
|
||||||
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
|
Commits []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ payloadConvertor[MatrixPayload] = matrixConvertor{}
|
var _ shared.PayloadConvertor[MatrixPayload] = matrixConvertor{}
|
||||||
|
|
||||||
type matrixConvertor struct {
|
type matrixConvertor struct {
|
||||||
MsgType string
|
MsgType string
|
||||||
|
|
|
@ -17,23 +17,24 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type msteamsHandler struct{}
|
type msteamsHandler struct{}
|
||||||
|
|
||||||
func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS }
|
func (msteamsHandler) Type() webhook_module.HookType { return webhook_module.MSTEAMS }
|
||||||
func (msteamsHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -370,8 +371,8 @@ func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTar
|
||||||
|
|
||||||
type msteamsConvertor struct{}
|
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) {
|
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,24 +15,25 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type packagistHandler struct{}
|
type packagistHandler struct{}
|
||||||
|
|
||||||
func (packagistHandler) Type() webhook_module.HookType { return webhook_module.PACKAGIST }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
Username string `binding:"Required"`
|
Username string `binding:"Required"`
|
||||||
APIToken string `binding:"Required"`
|
APIToken string `binding:"Required"`
|
||||||
PackageURL string `binding:"Required;ValidUrl"`
|
PackageURL string `binding:"Required;ValidUrl"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.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)),
|
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,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -85,5 +86,5 @@ func (packagistHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook
|
||||||
URL: meta.PackageURL,
|
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.
|
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package webhook
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||||
|
@ -14,8 +20,10 @@ import (
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// payloadConvertor defines the interface to convert system payload to webhook payload
|
var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event")
|
||||||
type payloadConvertor[T any] interface {
|
|
||||||
|
// PayloadConvertor defines the interface to convert system payload to webhook payload
|
||||||
|
type PayloadConvertor[T any] interface {
|
||||||
Create(*api.CreatePayload) (T, error)
|
Create(*api.CreatePayload) (T, error)
|
||||||
Delete(*api.DeletePayload) (T, error)
|
Delete(*api.DeletePayload) (T, error)
|
||||||
Fork(*api.ForkPayload) (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)
|
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 {
|
switch event {
|
||||||
case webhook_module.HookEventCreate:
|
case webhook_module.HookEventCreate:
|
||||||
return convertUnmarshalledJSON(rc.Create, data)
|
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)
|
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) {
|
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)
|
payload, err := NewPayload(pc, []byte(t.PayloadContent), t.EventType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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, "", " ")
|
body, err := json.MarshalIndent(payload, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if withDefaultHeaders {
|
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
|
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"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
|
|
||||||
"gitea.com/go-chi/binding"
|
"gitea.com/go-chi/binding"
|
||||||
)
|
)
|
||||||
|
@ -27,10 +28,10 @@ import (
|
||||||
type slackHandler struct{}
|
type slackHandler struct{}
|
||||||
|
|
||||||
func (slackHandler) Type() webhook_module.HookType { return webhook_module.SLACK }
|
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 {
|
type slackForm struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
Channel string `binding:"Required"`
|
Channel string `binding:"Required"`
|
||||||
Username string
|
Username string
|
||||||
|
@ -53,12 +54,12 @@ func (s *slackForm) Validate(req *http.Request, errs binding.Errors) binding.Err
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (slackHandler) FormFields(bind func(any)) FormFields {
|
func (slackHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
|
||||||
var form slackForm
|
var form slackForm
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -334,7 +335,7 @@ type slackConvertor struct {
|
||||||
Color string
|
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) {
|
func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
||||||
meta := &SlackMeta{}
|
meta := &SlackMeta{}
|
||||||
|
@ -347,7 +348,7 @@ func (slackHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t
|
||||||
IconURL: meta.IconURL,
|
IconURL: meta.IconURL,
|
||||||
Color: meta.Color,
|
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}$`)
|
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,24 +18,25 @@ import (
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type telegramHandler struct{}
|
type telegramHandler struct{}
|
||||||
|
|
||||||
func (telegramHandler) Type() webhook_module.HookType { return webhook_module.TELEGRAM }
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
BotToken string `binding:"Required"`
|
BotToken string `binding:"Required"`
|
||||||
ChatID string `binding:"Required"`
|
ChatID string `binding:"Required"`
|
||||||
ThreadID string
|
ThreadID string
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.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)),
|
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,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -220,8 +221,8 @@ func createTelegramPayload(message string) TelegramPayload {
|
||||||
|
|
||||||
type telegramConvertor struct{}
|
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) {
|
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"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/sourcehut"
|
||||||
|
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
)
|
)
|
||||||
|
@ -32,22 +33,13 @@ import (
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
Type() webhook_module.HookType
|
Type() webhook_module.HookType
|
||||||
Metadata(*webhook_model.Webhook) any
|
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
|
// 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)
|
NewRequest(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
|
||||||
Icon(size int) template.HTML
|
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{
|
var webhookHandlers = []Handler{
|
||||||
defaultHandler{true},
|
defaultHandler{true},
|
||||||
defaultHandler{false},
|
defaultHandler{false},
|
||||||
|
@ -62,6 +54,7 @@ var webhookHandlers = []Handler{
|
||||||
matrixHandler{},
|
matrixHandler{},
|
||||||
wechatworkHandler{},
|
wechatworkHandler{},
|
||||||
packagistHandler{},
|
packagistHandler{},
|
||||||
|
sourcehut.BuildsHandler{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhookHandler return the handler for a given webhook type (nil if not found)
|
// 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"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
"code.gitea.io/gitea/services/webhook/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wechatworkHandler struct{}
|
type wechatworkHandler struct{}
|
||||||
|
@ -23,18 +24,18 @@ func (wechatworkHandler) Type() webhook_module.HookType { return webhook_m
|
||||||
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
func (wechatworkHandler) Metadata(*webhook_model.Webhook) any { return nil }
|
||||||
|
|
||||||
func (wechatworkHandler) Icon(size int) template.HTML {
|
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 {
|
var form struct {
|
||||||
forms.WebhookForm
|
forms.WebhookCoreForm
|
||||||
PayloadURL string `binding:"Required;ValidUrl"`
|
PayloadURL string `binding:"Required;ValidUrl"`
|
||||||
}
|
}
|
||||||
bind(&form)
|
bind(&form)
|
||||||
|
|
||||||
return FormFields{
|
return forms.WebhookForm{
|
||||||
WebhookForm: form.WebhookForm,
|
WebhookCoreForm: form.WebhookCoreForm,
|
||||||
URL: form.PayloadURL,
|
URL: form.PayloadURL,
|
||||||
ContentType: webhook_model.ContentTypeJSON,
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
Secret: "",
|
Secret: "",
|
||||||
|
@ -203,8 +204,8 @@ func (wc wechatworkConvertor) Package(p *api.PackagePayload) (WechatworkPayload,
|
||||||
|
|
||||||
type wechatworkConvertor struct{}
|
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) {
|
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" .}}
|
{{template "webhook/new/wechatwork" .}}
|
||||||
{{else if eq .HookType "packagist"}}
|
{{else if eq .HookType "packagist"}}
|
||||||
{{template "webhook/new/packagist" .}}
|
{{template "webhook/new/packagist" .}}
|
||||||
|
{{else if eq .HookType "sourcehut_builds"}}
|
||||||
|
{{template "webhook/new/sourcehut_builds" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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/*",
|
"branch_filter": "packagist/*",
|
||||||
"authorization_header": "Bearer 123456",
|
"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 {
|
func assertInput(t testing.TB, form *goquery.Selection, name string) string {
|
||||||
|
@ -247,8 +275,16 @@ func assertInput(t testing.TB, form *goquery.Selection, name string) string {
|
||||||
t.Log(form.Html())
|
t.Log(form.Html())
|
||||||
t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length())
|
t.Errorf("field <input name=%q /> found %d times, expected once", name, input.Length())
|
||||||
}
|
}
|
||||||
|
switch input.AttrOr("type", "") {
|
||||||
|
case "checkbox":
|
||||||
|
if _, checked := input.Attr("checked"); checked {
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
return input.AttrOr("value", "")
|
return input.AttrOr("value", "")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) {
|
func testWebhookForms(name string, session *TestSession, validFields map[string]string, invalidPatches ...map[string]string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue