mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
265 lines
7.2 KiB
Go
265 lines
7.2 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
repo_model "github.com/hanzoai/git/models/repo"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
webhook_model "github.com/hanzoai/git/models/webhook"
|
|
"github.com/hanzoai/git/modules/git"
|
|
"github.com/hanzoai/git/modules/glob"
|
|
"github.com/hanzoai/git/modules/graceful"
|
|
"github.com/hanzoai/git/modules/log"
|
|
"github.com/hanzoai/git/modules/optional"
|
|
"github.com/hanzoai/git/modules/queue"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/util"
|
|
webhook_module "github.com/hanzoai/git/modules/webhook"
|
|
)
|
|
|
|
type Requester func(context.Context, *webhook_model.Webhook, *webhook_model.HookTask) (req *http.Request, body []byte, err error)
|
|
|
|
var webhookRequesters = map[webhook_module.HookType]Requester{}
|
|
|
|
func RegisterWebhookRequester(hookType webhook_module.HookType, requester Requester) {
|
|
webhookRequesters[hookType] = requester
|
|
}
|
|
|
|
// IsValidHookTaskType returns true if a webhook registered
|
|
func IsValidHookTaskType(name string) bool {
|
|
if name == webhook_module.NATIVE || name == webhook_module.GOGS {
|
|
return true
|
|
}
|
|
_, ok := webhookRequesters[name]
|
|
return ok
|
|
}
|
|
|
|
// hookQueue is a global queue of web hooks
|
|
var hookQueue *queue.WorkerPoolQueue[int64]
|
|
|
|
// getPayloadRef returns the full ref name for hook event, if applicable.
|
|
func getPayloadRef(p api.Payloader) git.RefName {
|
|
switch pp := p.(type) {
|
|
case *api.CreatePayload:
|
|
switch pp.RefType {
|
|
case "branch":
|
|
return git.RefNameFromBranch(pp.Ref)
|
|
case "tag":
|
|
return git.RefNameFromTag(pp.Ref)
|
|
}
|
|
case *api.DeletePayload:
|
|
switch pp.RefType {
|
|
case "branch":
|
|
return git.RefNameFromBranch(pp.Ref)
|
|
case "tag":
|
|
return git.RefNameFromTag(pp.Ref)
|
|
}
|
|
case *api.PushPayload:
|
|
return git.RefName(pp.Ref)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// EventSource represents the source of a webhook action. Repository and/or Owner must be set.
|
|
type EventSource struct {
|
|
Repository *repo_model.Repository
|
|
Owner *user_model.User
|
|
}
|
|
|
|
// handle delivers hook tasks
|
|
func handler(items ...int64) []int64 {
|
|
ctx := graceful.GetManager().HammerContext()
|
|
|
|
for _, taskID := range items {
|
|
task, err := webhook_model.GetHookTaskByID(ctx, taskID)
|
|
if err != nil {
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
log.Warn("GetHookTaskByID[%d] warn: %v", taskID, err)
|
|
} else {
|
|
log.Error("GetHookTaskByID[%d] failed: %v", taskID, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
if task.IsDelivered {
|
|
// Already delivered in the meantime
|
|
log.Trace("Task[%d] has already been delivered", task.ID)
|
|
continue
|
|
}
|
|
|
|
if err := Deliver(ctx, task); err != nil {
|
|
log.Error("Unable to deliver webhook task[%d]: %v", task.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func enqueueHookTask(taskID int64) error {
|
|
err := hookQueue.Push(taskID)
|
|
if err != nil && err != queue.ErrAlreadyInQueue {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkBranchFilter(branchFilter string, ref git.RefName) bool {
|
|
if branchFilter == "" || branchFilter == "*" || branchFilter == "**" {
|
|
return true
|
|
}
|
|
|
|
g, err := glob.Compile(branchFilter)
|
|
if err != nil {
|
|
// should not really happen as BranchFilter is validated
|
|
log.Debug("checkBranchFilter failed to compile filer %q, err: %s", branchFilter, err)
|
|
return false
|
|
}
|
|
|
|
if ref.IsBranch() && g.Match(ref.BranchName()) {
|
|
return true
|
|
}
|
|
return g.Match(ref.String())
|
|
}
|
|
|
|
// PrepareTestWebhook always creates and enqueues a hook task for manual testing.
|
|
// Unlike PrepareWebhook, it ignores event subscriptions and branch filters so the
|
|
// Test Push Event control can verify delivery even when those gates would suppress
|
|
// a real event.
|
|
func PrepareTestWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
|
|
if setting.DisableWebhooks {
|
|
return nil
|
|
}
|
|
|
|
payload, err := p.JSONPayload()
|
|
if err != nil {
|
|
return fmt.Errorf("JSONPayload for %s: %w", event, err)
|
|
}
|
|
|
|
task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
|
|
HookID: w.ID,
|
|
PayloadContent: string(payload),
|
|
EventType: event,
|
|
PayloadVersion: 2,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("CreateHookTask for %s: %w", event, err)
|
|
}
|
|
|
|
return enqueueHookTask(task.ID)
|
|
}
|
|
|
|
// PrepareWebhook creates a hook task and enqueues it for processing.
|
|
// The payload is saved as-is. The adjustments depending on the webhook type happen
|
|
// right before delivery, in the [Deliver] method.
|
|
func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook_module.HookEventType, p api.Payloader) error {
|
|
// Skip sending if webhooks are disabled.
|
|
if setting.DisableWebhooks {
|
|
return nil
|
|
}
|
|
|
|
if !w.HasEvent(event) {
|
|
return nil
|
|
}
|
|
|
|
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
|
|
// Integration webhooks (e.g. drone) still receive the required data.
|
|
if pushEvent, ok := p.(*api.PushPayload); ok &&
|
|
w.Type != webhook_module.NATIVE && w.Type != webhook_module.GOGS &&
|
|
len(pushEvent.Commits) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// If payload has no associated branch (e.g. it's a new tag, issue, etc.), branch filter has no effect.
|
|
if ref := getPayloadRef(p); ref != "" {
|
|
// Check the payload's git ref against the webhook's branch filter.
|
|
if !checkBranchFilter(w.BranchFilter, ref) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
payload, err := p.JSONPayload()
|
|
if err != nil {
|
|
return fmt.Errorf("JSONPayload for %s: %w", event, err)
|
|
}
|
|
|
|
task, err := webhook_model.CreateHookTask(ctx, &webhook_model.HookTask{
|
|
HookID: w.ID,
|
|
PayloadContent: string(payload),
|
|
EventType: event,
|
|
PayloadVersion: 2,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("CreateHookTask for %s: %w", event, err)
|
|
}
|
|
|
|
return enqueueHookTask(task.ID)
|
|
}
|
|
|
|
// PrepareWebhooks adds new webhooks to task queue for given payload.
|
|
func PrepareWebhooks(ctx context.Context, source EventSource, event webhook_module.HookEventType, p api.Payloader) error {
|
|
owner := source.Owner
|
|
|
|
var ws []*webhook_model.Webhook
|
|
|
|
if source.Repository != nil {
|
|
repoHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
|
|
RepoID: source.Repository.ID,
|
|
IsActive: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("ListWebhooksByOpts: %w", err)
|
|
}
|
|
ws = append(ws, repoHooks...)
|
|
|
|
owner = source.Repository.MustOwner(ctx)
|
|
}
|
|
|
|
// append additional webhooks of a user or organization
|
|
if owner != nil {
|
|
ownerHooks, err := db.Find[webhook_model.Webhook](ctx, webhook_model.ListWebhookOptions{
|
|
OwnerID: owner.ID,
|
|
IsActive: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("ListWebhooksByOpts: %w", err)
|
|
}
|
|
ws = append(ws, ownerHooks...)
|
|
}
|
|
|
|
// Add any admin-defined system webhooks
|
|
systemHooks, err := webhook_model.GetSystemWebhooks(ctx, optional.Some(true))
|
|
if err != nil {
|
|
return fmt.Errorf("GetSystemWebhooks: %w", err)
|
|
}
|
|
ws = append(ws, systemHooks...)
|
|
|
|
if len(ws) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, w := range ws {
|
|
if err := PrepareWebhook(ctx, w, event, p); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReplayHookTask replays a webhook task
|
|
func ReplayHookTask(ctx context.Context, w *webhook_model.Webhook, uuid string) error {
|
|
task, err := webhook_model.ReplayHookTask(ctx, w.ID, uuid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return enqueueHookTask(task.ID)
|
|
}
|