All checks were successful
Check / check (pull_request) Successful in 1m50s
Add auto-detection of webhook source (Gitea, GitHub, GitLab) by examining HTTP headers (X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event). Parse push webhook payloads from all three platforms into a normalized PushEvent type for unified processing. Each platform's payload format is handled by dedicated parser functions with correct field mapping and commit URL extraction. The webhook handler now detects the source automatically — existing Gitea webhooks continue to work unchanged, while GitHub and GitLab webhooks are parsed with their respective payload formats. Includes comprehensive tests for source detection, event type extraction, payload parsing for all three platforms, commit URL fallback logic, and integration tests via HandleWebhook.
135 lines
3.2 KiB
Go
135 lines
3.2 KiB
Go
// Package webhook provides webhook handling services.
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/upaas/internal/database"
|
|
|
|
"sneak.berlin/go/upaas/internal/logger"
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
"sneak.berlin/go/upaas/internal/service/deploy"
|
|
)
|
|
|
|
// ServiceParams contains dependencies for Service.
|
|
type ServiceParams struct {
|
|
fx.In
|
|
|
|
Logger *logger.Logger
|
|
Database *database.Database
|
|
Deploy *deploy.Service
|
|
}
|
|
|
|
// Service provides webhook handling functionality.
|
|
type Service struct {
|
|
log *slog.Logger
|
|
db *database.Database
|
|
deploy *deploy.Service
|
|
params *ServiceParams
|
|
}
|
|
|
|
// New creates a new webhook Service.
|
|
func New(_ fx.Lifecycle, params ServiceParams) (*Service, error) {
|
|
return &Service{
|
|
log: params.Logger.Get(),
|
|
db: params.Database,
|
|
deploy: params.Deploy,
|
|
params: ¶ms,
|
|
}, nil
|
|
}
|
|
|
|
// HandleWebhook processes a webhook request from any supported source
|
|
// (Gitea, GitHub, or GitLab). The source parameter determines which
|
|
// payload format to use for parsing.
|
|
func (svc *Service) HandleWebhook(
|
|
ctx context.Context,
|
|
app *models.App,
|
|
source Source,
|
|
eventType string,
|
|
payload []byte,
|
|
) error {
|
|
svc.log.Info("processing webhook",
|
|
"app", app.Name,
|
|
"source", source.String(),
|
|
"event", eventType,
|
|
)
|
|
|
|
// Parse payload into normalized push event
|
|
pushEvent, parseErr := ParsePushPayload(source, payload)
|
|
if parseErr != nil {
|
|
svc.log.Warn("failed to parse webhook payload",
|
|
"error", parseErr,
|
|
"source", source.String(),
|
|
)
|
|
// Continue with empty push event to still log the webhook
|
|
pushEvent = &PushEvent{Source: source}
|
|
}
|
|
|
|
// Check if branch matches
|
|
matched := pushEvent.Branch == app.Branch
|
|
|
|
// Create webhook event record
|
|
event := models.NewWebhookEvent(svc.db)
|
|
event.AppID = app.ID
|
|
event.EventType = eventType
|
|
event.Branch = pushEvent.Branch
|
|
event.CommitSHA = sql.NullString{String: pushEvent.After, Valid: pushEvent.After != ""}
|
|
event.CommitURL = sql.NullString{
|
|
String: pushEvent.CommitURL.String(),
|
|
Valid: pushEvent.CommitURL != "",
|
|
}
|
|
event.Payload = sql.NullString{String: string(payload), Valid: true}
|
|
event.Matched = matched
|
|
event.Processed = false
|
|
|
|
saveErr := event.Save(ctx)
|
|
if saveErr != nil {
|
|
return fmt.Errorf("failed to save webhook event: %w", saveErr)
|
|
}
|
|
|
|
svc.log.Info("webhook event recorded",
|
|
"app", app.Name,
|
|
"source", source.String(),
|
|
"branch", pushEvent.Branch,
|
|
"matched", matched,
|
|
"commit", pushEvent.After,
|
|
)
|
|
|
|
// If branch matches, trigger deployment
|
|
if matched {
|
|
svc.triggerDeployment(ctx, app, event)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (svc *Service) triggerDeployment(
|
|
ctx context.Context,
|
|
app *models.App,
|
|
event *models.WebhookEvent,
|
|
) {
|
|
// Capture values for goroutine
|
|
eventID := event.ID
|
|
appName := app.Name
|
|
|
|
go func() {
|
|
// Use context.WithoutCancel to ensure deployment completes
|
|
// even if the HTTP request context is cancelled.
|
|
deployCtx := context.WithoutCancel(ctx)
|
|
|
|
deployErr := svc.deploy.Deploy(deployCtx, app, &eventID, true)
|
|
if deployErr != nil {
|
|
svc.log.Error("deployment failed", "error", deployErr, "app", appName)
|
|
}
|
|
|
|
// Mark event as processed
|
|
event.Processed = true
|
|
_ = event.Save(deployCtx)
|
|
}()
|
|
}
|