mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
No compat with old Gitea deployments, so the wire values that were held back
for it go too.
.gitea/* WorkflowDirs is now {.hanzo, .github}/workflows. Same for the
PR-template, ISSUE_TEMPLATE and repo-template lookups, which
each carried a .gitea variant. .github stays — GitHub compat
is worth having; Gitea compat is what we just decided not to
keep.
webhook type "gitea" -> "native", constant GITEA -> NATIVE, routes
/hooks/gitea/{new,id} -> /hooks/native/{new,id}, and the enum
in both swagger documents.
actions context gitea_runtime_token -> git_runtime_token,
gitea_default_actions_url -> git_default_actions_url.
models/migrations/* is untouched on purpose. A schema migration describes what
the database held AT THAT VERSION; v1_14/v162.go rewriting its own history to
say "native" would make it wrong about the thing it exists to migrate.
Three test fixtures needed aligning rather than renaming, all the same shape as
the bugs this cleanup keeps finding: WorkflowDirs expectations ended up with a
duplicated .hanzo entry, the repo-template test built its fixture under .gitea,
and five API tests still posted Type: "gitea" (422 now, correctly).
models, modules, services, routers green; integration compile gate green.
Co-authored-by: Hanzo Dev <[email protected]>
266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// Copyright 2019 The Gitea Authors. 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)
|
|
}
|