Merge pull request 'fix: webhook: send short ref on gitea create/delete payload' (#3558) from oliverpool/forgejo:webhook_gitea_fix into forgejo
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3558 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
This commit is contained in:
commit
6ba60f61cb
|
@ -12,6 +12,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
webhook_model "code.gitea.io/gitea/models/webhook"
|
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/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"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"
|
||||||
|
@ -67,6 +69,18 @@ func (defaultHandler) UnmarshalForm(bind func(any)) forms.WebhookForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (req *http.Request, body []byte, err error) {
|
||||||
|
payloadContent := t.PayloadContent
|
||||||
|
if w.Type == webhook_module.GITEA &&
|
||||||
|
(t.EventType == webhook_module.HookEventCreate || t.EventType == webhook_module.HookEventDelete) {
|
||||||
|
// Woodpecker expects the ref to be short on tag creation only
|
||||||
|
// https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134
|
||||||
|
// see https://codeberg.org/codeberg/community/issues/1556
|
||||||
|
payloadContent, err = substituteRefShortName(payloadContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("could not substiture ref: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch w.HTTPMethod {
|
switch w.HTTPMethod {
|
||||||
case "":
|
case "":
|
||||||
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
|
log.Info("HTTP Method for %s webhook %s [ID: %d] is not set, defaulting to POST", w.Type, w.URL, w.ID)
|
||||||
|
@ -74,7 +88,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
switch w.ContentType {
|
switch w.ContentType {
|
||||||
case webhook_model.ContentTypeJSON:
|
case webhook_model.ContentTypeJSON:
|
||||||
req, err = http.NewRequest("POST", w.URL, strings.NewReader(t.PayloadContent))
|
req, err = http.NewRequest("POST", w.URL, strings.NewReader(payloadContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -82,7 +96,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
case webhook_model.ContentTypeForm:
|
case webhook_model.ContentTypeForm:
|
||||||
forms := url.Values{
|
forms := url.Values{
|
||||||
"payload": []string{t.PayloadContent},
|
"payload": []string{payloadContent},
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
|
req, err = http.NewRequest("POST", w.URL, strings.NewReader(forms.Encode()))
|
||||||
|
@ -100,7 +114,7 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
return nil, nil, fmt.Errorf("invalid URL: %w", err)
|
return nil, nil, fmt.Errorf("invalid URL: %w", err)
|
||||||
}
|
}
|
||||||
vals := u.Query()
|
vals := u.Query()
|
||||||
vals["payload"] = []string{t.PayloadContent}
|
vals["payload"] = []string{payloadContent}
|
||||||
u.RawQuery = vals.Encode()
|
u.RawQuery = vals.Encode()
|
||||||
req, err = http.NewRequest("GET", u.String(), nil)
|
req, err = http.NewRequest("GET", u.String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -109,12 +123,12 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
switch w.Type {
|
switch w.Type {
|
||||||
case webhook_module.MATRIX: // used when t.Version == 1
|
case webhook_module.MATRIX: // used when t.Version == 1
|
||||||
txnID, err := getMatrixTxnID([]byte(t.PayloadContent))
|
txnID, err := getMatrixTxnID([]byte(payloadContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
|
url := fmt.Sprintf("%s/%s", w.URL, url.PathEscape(txnID))
|
||||||
req, err = http.NewRequest("PUT", url, strings.NewReader(t.PayloadContent))
|
req, err = http.NewRequest("PUT", url, strings.NewReader(payloadContent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -125,6 +139,22 @@ func (defaultHandler) NewRequest(ctx context.Context, w *webhook_model.Webhook,
|
||||||
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
return nil, nil, fmt.Errorf("invalid http method: %v", w.HTTPMethod)
|
||||||
}
|
}
|
||||||
|
|
||||||
body = []byte(t.PayloadContent)
|
body = []byte(payloadContent)
|
||||||
return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body)
|
return req, body, shared.AddDefaultHeaders(req, []byte(w.Secret), t, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func substituteRefShortName(body string) (string, error) {
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(body), &m); err != nil {
|
||||||
|
return body, err
|
||||||
|
}
|
||||||
|
ref, ok := m["ref"].(string)
|
||||||
|
if !ok {
|
||||||
|
return body, fmt.Errorf("expected string 'ref', got %T", m["ref"])
|
||||||
|
}
|
||||||
|
|
||||||
|
m["ref"] = git.RefName(ref).ShortName()
|
||||||
|
|
||||||
|
buf, err := json.Marshal(m)
|
||||||
|
return string(buf), err
|
||||||
|
}
|
||||||
|
|
224
services/webhook/default_test.go
Normal file
224
services/webhook/default_test.go
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
webhook_model "code.gitea.io/gitea/models/webhook"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGiteaPayload(t *testing.T) {
|
||||||
|
dh := defaultHandler{
|
||||||
|
forgejo: false,
|
||||||
|
}
|
||||||
|
hook := &webhook_model.Webhook{
|
||||||
|
RepoID: 3,
|
||||||
|
IsActive: true,
|
||||||
|
Type: webhook_module.GITEA,
|
||||||
|
URL: "https://gitea.example.com/",
|
||||||
|
Meta: ``,
|
||||||
|
HTTPMethod: "POST",
|
||||||
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Woodpecker expects the ref to be short on tag creation only
|
||||||
|
// https://github.com/woodpecker-ci/woodpecker/blob/00ccec078cdced80cf309cd4da460a5041d7991a/server/forge/gitea/helper.go#L134
|
||||||
|
// see https://codeberg.org/codeberg/community/issues/1556
|
||||||
|
t.Run("Create", func(t *testing.T) {
|
||||||
|
p := createTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventCreate,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://gitea.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "test", body.Ref) // short ref
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Push", func(t *testing.T) {
|
||||||
|
p := pushTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventPush,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://gitea.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "refs/heads/test", body.Ref) // full ref
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
p := deleteTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventDelete,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://gitea.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "test", body.Ref) // short ref
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForgejoPayload(t *testing.T) {
|
||||||
|
dh := defaultHandler{
|
||||||
|
forgejo: true,
|
||||||
|
}
|
||||||
|
hook := &webhook_model.Webhook{
|
||||||
|
RepoID: 3,
|
||||||
|
IsActive: true,
|
||||||
|
Type: webhook_module.FORGEJO,
|
||||||
|
URL: "https://forgejo.example.com/",
|
||||||
|
Meta: ``,
|
||||||
|
HTTPMethod: "POST",
|
||||||
|
ContentType: webhook_model.ContentTypeJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
// always return the full ref for consistency
|
||||||
|
t.Run("Create", func(t *testing.T) {
|
||||||
|
p := createTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventCreate,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://forgejo.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "refs/heads/test", body.Ref) // full ref
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Push", func(t *testing.T) {
|
||||||
|
p := pushTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventPush,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://forgejo.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "refs/heads/test", body.Ref) // full ref
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
p := deleteTestPayload()
|
||||||
|
data, err := p.JSONPayload()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
task := &webhook_model.HookTask{
|
||||||
|
HookID: hook.ID,
|
||||||
|
EventType: webhook_module.HookEventDelete,
|
||||||
|
PayloadContent: string(data),
|
||||||
|
PayloadVersion: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, reqBody, err := dh.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, "https://forgejo.example.com/", req.URL.String())
|
||||||
|
assert.Equal(t, "sha256=", req.Header.Get("X-Hub-Signature-256"))
|
||||||
|
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
|
||||||
|
var body struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
}
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "refs/heads/test", body.Ref) // full ref
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in a new issue