Files
upaas/internal/service/webhook/webhook.go
user 0c5c727e01
All checks were successful
Check / check (pull_request) Successful in 1m50s
feat: add GitHub and GitLab webhook support
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.
2026-03-17 02:37:29 -07:00

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: &params,
}, 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)
}()
}