mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-06-22 05:00:03 +02:00

Implement Actions Success, Failure and Recover webhooks for Forgejo, Gitea, Gogs, Slack, Discord, DingTalk, Telegram, Microsoft Teams, Feishu / Lark Suite, Matrix, WeCom (Wechat Work), Packagist. Some of these webhooks have not been manually tested. Implement settings for these new webhooks. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [x] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Features - [PR](https://codeberg.org/forgejo/forgejo/pulls/7508): <!--number 7508 --><!--line 0 --><!--description QWN0aW9ucyBGYWlsdXJlLCBTdWNjZXMsIFJlY292ZXIgV2ViaG9va3M=-->Actions Failure, Succes, Recover Webhooks<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7508 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: christopher-besch <mail@chris-besch.com> Co-committed-by: christopher-besch <mail@chris-besch.com>
164 lines
6.6 KiB
Go
164 lines
6.6 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package shared
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
webhook_model "forgejo.org/models/webhook"
|
|
"forgejo.org/modules/json"
|
|
api "forgejo.org/modules/structs"
|
|
webhook_module "forgejo.org/modules/webhook"
|
|
)
|
|
|
|
var ErrPayloadTypeNotSupported = errors.New("unsupported webhook event")
|
|
|
|
// PayloadConvertor defines the interface to convert system payload to webhook payload
|
|
type PayloadConvertor[T any] interface {
|
|
Create(*api.CreatePayload) (T, error)
|
|
Delete(*api.DeletePayload) (T, error)
|
|
Fork(*api.ForkPayload) (T, error)
|
|
Issue(*api.IssuePayload) (T, error)
|
|
IssueComment(*api.IssueCommentPayload) (T, error)
|
|
Push(*api.PushPayload) (T, error)
|
|
PullRequest(*api.PullRequestPayload) (T, error)
|
|
Review(*api.PullRequestPayload, webhook_module.HookEventType) (T, error)
|
|
Repository(*api.RepositoryPayload) (T, error)
|
|
Release(*api.ReleasePayload) (T, error)
|
|
Wiki(*api.WikiPayload) (T, error)
|
|
Package(*api.PackagePayload) (T, error)
|
|
Action(*api.ActionPayload) (T, error)
|
|
}
|
|
|
|
func convertUnmarshalledJSON[T, P any](convert func(P) (T, error), data []byte) (T, error) {
|
|
var p P
|
|
if err := json.Unmarshal(data, &p); err != nil {
|
|
var t T
|
|
return t, fmt.Errorf("could not unmarshal payload: %w", err)
|
|
}
|
|
return convert(p)
|
|
}
|
|
|
|
func NewPayload[T any](rc PayloadConvertor[T], data []byte, event webhook_module.HookEventType) (T, error) {
|
|
switch event {
|
|
case webhook_module.HookEventCreate:
|
|
return convertUnmarshalledJSON(rc.Create, data)
|
|
case webhook_module.HookEventDelete:
|
|
return convertUnmarshalledJSON(rc.Delete, data)
|
|
case webhook_module.HookEventFork:
|
|
return convertUnmarshalledJSON(rc.Fork, data)
|
|
case webhook_module.HookEventIssues, webhook_module.HookEventIssueAssign, webhook_module.HookEventIssueLabel, webhook_module.HookEventIssueMilestone:
|
|
return convertUnmarshalledJSON(rc.Issue, data)
|
|
case webhook_module.HookEventIssueComment, webhook_module.HookEventPullRequestComment:
|
|
// previous code sometimes sent s.PullRequest(p.(*api.PullRequestPayload))
|
|
// however I couldn't find in notifier.go such a payload with an HookEvent***Comment event
|
|
|
|
// History (most recent first):
|
|
// - refactored in https://github.com/go-gitea/gitea/pull/12310
|
|
// - assertion added in https://github.com/go-gitea/gitea/pull/12046
|
|
// - issue raised in https://github.com/go-gitea/gitea/issues/11940#issuecomment-645713996
|
|
// > That's because for HookEventPullRequestComment event, some places use IssueCommentPayload and others use PullRequestPayload
|
|
|
|
// In modules/actions/workflows.go:183 the type assertion is always payload.(*api.IssueCommentPayload)
|
|
return convertUnmarshalledJSON(rc.IssueComment, data)
|
|
case webhook_module.HookEventPush:
|
|
return convertUnmarshalledJSON(rc.Push, data)
|
|
case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestAssign, webhook_module.HookEventPullRequestLabel,
|
|
webhook_module.HookEventPullRequestMilestone, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestReviewRequest:
|
|
return convertUnmarshalledJSON(rc.PullRequest, data)
|
|
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
|
|
return convertUnmarshalledJSON(func(p *api.PullRequestPayload) (T, error) {
|
|
return rc.Review(p, event)
|
|
}, data)
|
|
case webhook_module.HookEventRepository:
|
|
return convertUnmarshalledJSON(rc.Repository, data)
|
|
case webhook_module.HookEventRelease:
|
|
return convertUnmarshalledJSON(rc.Release, data)
|
|
case webhook_module.HookEventWiki:
|
|
return convertUnmarshalledJSON(rc.Wiki, data)
|
|
case webhook_module.HookEventPackage:
|
|
return convertUnmarshalledJSON(rc.Package, data)
|
|
case webhook_module.HookEventActionRunFailure, webhook_module.HookEventActionRunRecover, webhook_module.HookEventActionRunSuccess:
|
|
return convertUnmarshalledJSON(rc.Action, data)
|
|
}
|
|
var t T
|
|
return t, fmt.Errorf("newPayload unsupported event: %s", event)
|
|
}
|
|
|
|
func NewJSONRequest[T any](pc PayloadConvertor[T], w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
|
|
payload, err := NewPayload(pc, []byte(t.PayloadContent), t.EventType)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return NewJSONRequestWithPayload(payload, w, t, withDefaultHeaders)
|
|
}
|
|
|
|
func NewJSONRequestWithPayload(payload any, w *webhook_model.Webhook, t *webhook_model.HookTask, withDefaultHeaders bool) (*http.Request, []byte, error) {
|
|
body, err := json.MarshalIndent(payload, "", " ")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
method := w.HTTPMethod
|
|
if method == "" {
|
|
method = http.MethodPost
|
|
}
|
|
|
|
req, err := http.NewRequest(method, w.URL, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
if withDefaultHeaders {
|
|
return req, body, AddDefaultHeaders(req, []byte(w.Secret), t, body)
|
|
}
|
|
return req, body, nil
|
|
}
|
|
|
|
// AddDefaultHeaders adds the X-Forgejo, X-Gitea, X-Gogs, X-Hub, X-GitHub headers to the given request
|
|
func AddDefaultHeaders(req *http.Request, secret []byte, t *webhook_model.HookTask, payloadContent []byte) error {
|
|
var signatureSHA1 string
|
|
var signatureSHA256 string
|
|
if len(secret) > 0 {
|
|
sig1 := hmac.New(sha1.New, secret)
|
|
sig256 := hmac.New(sha256.New, secret)
|
|
_, err := io.MultiWriter(sig1, sig256).Write(payloadContent)
|
|
if err != nil {
|
|
// this error should never happen, since the hashes are writing to []byte and always return a nil error.
|
|
return fmt.Errorf("prepareWebhooks.sigWrite: %w", err)
|
|
}
|
|
signatureSHA1 = hex.EncodeToString(sig1.Sum(nil))
|
|
signatureSHA256 = hex.EncodeToString(sig256.Sum(nil))
|
|
}
|
|
|
|
event := t.EventType.Event()
|
|
eventType := string(t.EventType)
|
|
req.Header.Add("X-Forgejo-Delivery", t.UUID)
|
|
req.Header.Add("X-Forgejo-Event", event)
|
|
req.Header.Add("X-Forgejo-Event-Type", eventType)
|
|
req.Header.Add("X-Forgejo-Signature", signatureSHA256)
|
|
req.Header.Add("X-Gitea-Delivery", t.UUID)
|
|
req.Header.Add("X-Gitea-Event", event)
|
|
req.Header.Add("X-Gitea-Event-Type", eventType)
|
|
req.Header.Add("X-Gitea-Signature", signatureSHA256)
|
|
req.Header.Add("X-Gogs-Delivery", t.UUID)
|
|
req.Header.Add("X-Gogs-Event", event)
|
|
req.Header.Add("X-Gogs-Event-Type", eventType)
|
|
req.Header.Add("X-Gogs-Signature", signatureSHA256)
|
|
req.Header.Add("X-Hub-Signature", "sha1="+signatureSHA1)
|
|
req.Header.Add("X-Hub-Signature-256", "sha256="+signatureSHA256)
|
|
req.Header["X-GitHub-Delivery"] = []string{t.UUID}
|
|
req.Header["X-GitHub-Event"] = []string{event}
|
|
req.Header["X-GitHub-Event-Type"] = []string{eventType}
|
|
return nil
|
|
}
|