feat: add GitHub and GitLab webhook support
All checks were successful
Check / check (pull_request) Successful in 1m50s
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.
This commit is contained in:
@@ -4,7 +4,6 @@ package webhook
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
@@ -44,68 +43,46 @@ func New(_ fx.Lifecycle, params ServiceParams) (*Service, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GiteaPushPayload represents a Gitea push webhook payload.
|
||||
//
|
||||
//nolint:tagliatelle // Field names match Gitea API (snake_case)
|
||||
type GiteaPushPayload struct {
|
||||
Ref string `json:"ref"`
|
||||
Before string `json:"before"`
|
||||
After string `json:"after"`
|
||||
CompareURL UnparsedURL `json:"compare_url"`
|
||||
Repository struct {
|
||||
FullName string `json:"full_name"`
|
||||
CloneURL UnparsedURL `json:"clone_url"`
|
||||
SSHURL string `json:"ssh_url"`
|
||||
HTMLURL UnparsedURL `json:"html_url"`
|
||||
} `json:"repository"`
|
||||
Pusher struct {
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
} `json:"pusher"`
|
||||
Commits []struct {
|
||||
ID string `json:"id"`
|
||||
URL UnparsedURL `json:"url"`
|
||||
Message string `json:"message"`
|
||||
Author struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
} `json:"author"`
|
||||
} `json:"commits"`
|
||||
}
|
||||
|
||||
// HandleWebhook processes a webhook request.
|
||||
// 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, "event", eventType)
|
||||
svc.log.Info("processing webhook",
|
||||
"app", app.Name,
|
||||
"source", source.String(),
|
||||
"event", eventType,
|
||||
)
|
||||
|
||||
// Parse payload
|
||||
var pushPayload GiteaPushPayload
|
||||
|
||||
unmarshalErr := json.Unmarshal(payload, &pushPayload)
|
||||
if unmarshalErr != nil {
|
||||
svc.log.Warn("failed to parse webhook payload", "error", unmarshalErr)
|
||||
// Continue anyway to log the event
|
||||
// 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}
|
||||
}
|
||||
|
||||
// Extract branch from ref
|
||||
branch := extractBranch(pushPayload.Ref)
|
||||
commitSHA := pushPayload.After
|
||||
commitURL := extractCommitURL(pushPayload)
|
||||
|
||||
// Check if branch matches
|
||||
matched := branch == app.Branch
|
||||
matched := pushEvent.Branch == app.Branch
|
||||
|
||||
// Create webhook event record
|
||||
event := models.NewWebhookEvent(svc.db)
|
||||
event.AppID = app.ID
|
||||
event.EventType = eventType
|
||||
event.Branch = branch
|
||||
event.CommitSHA = sql.NullString{String: commitSHA, Valid: commitSHA != ""}
|
||||
event.CommitURL = sql.NullString{String: commitURL.String(), Valid: commitURL != ""}
|
||||
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
|
||||
@@ -117,9 +94,10 @@ func (svc *Service) HandleWebhook(
|
||||
|
||||
svc.log.Info("webhook event recorded",
|
||||
"app", app.Name,
|
||||
"branch", branch,
|
||||
"source", source.String(),
|
||||
"branch", pushEvent.Branch,
|
||||
"matched", matched,
|
||||
"commit", commitSHA,
|
||||
"commit", pushEvent.After,
|
||||
)
|
||||
|
||||
// If branch matches, trigger deployment
|
||||
@@ -154,33 +132,3 @@ func (svc *Service) triggerDeployment(
|
||||
_ = event.Save(deployCtx)
|
||||
}()
|
||||
}
|
||||
|
||||
// extractBranch extracts the branch name from a git ref.
|
||||
func extractBranch(ref string) string {
|
||||
// refs/heads/main -> main
|
||||
const prefix = "refs/heads/"
|
||||
|
||||
if len(ref) >= len(prefix) && ref[:len(prefix)] == prefix {
|
||||
return ref[len(prefix):]
|
||||
}
|
||||
|
||||
return ref
|
||||
}
|
||||
|
||||
// extractCommitURL extracts the commit URL from the webhook payload.
|
||||
// Prefers the URL from the head commit, falls back to constructing from repo URL.
|
||||
func extractCommitURL(payload GiteaPushPayload) UnparsedURL {
|
||||
// Try to find the URL from the head commit (matching After SHA)
|
||||
for _, commit := range payload.Commits {
|
||||
if commit.ID == payload.After && commit.URL != "" {
|
||||
return commit.URL
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to constructing URL from repo HTML URL
|
||||
if payload.Repository.HTMLURL != "" && payload.After != "" {
|
||||
return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user