forked from sneak/upaas
## Summary Adds GitHub and GitLab push webhook support alongside the existing Gitea support. closes #68 ## What Changed ### Auto-detection of webhook source The webhook handler now auto-detects which platform sent the webhook by examining HTTP headers: - **Gitea**: `X-Gitea-Event` - **GitHub**: `X-GitHub-Event` - **GitLab**: `X-Gitlab-Event` Existing Gitea webhooks continue to work unchanged. Unknown sources fall back to Gitea format for backward compatibility. ### Normalized push event All three payload formats are parsed into a unified `PushEvent` struct containing: - Source platform, ref, branch, commit SHA - Repository name, clone URL, HTML URL - Commit URL (with per-platform fallback logic) - Pusher username/name ### New files - **`internal/service/webhook/payloads.go`**: Source-specific payload structs (`GiteaPushPayload`, `GitHubPushPayload`, `GitLabPushPayload`), `ParsePushPayload()` dispatcher, per-platform parsers, branch extraction, and commit URL extraction functions. ### Modified files - **`internal/service/webhook/types.go`**: Added `Source` type (gitea/github/gitlab/unknown), `DetectWebhookSource()`, `DetectEventType()`, and `PushEvent` normalized type. Moved `GiteaPushPayload` to payloads.go. - **`internal/service/webhook/webhook.go`**: `HandleWebhook` now accepts a `Source` parameter and uses `ParsePushPayload()` for unified parsing instead of directly unmarshaling Gitea payloads. - **`internal/handlers/webhook.go`**: Calls `DetectWebhookSource()` and `DetectEventType()` to auto-detect the platform before delegating to the webhook service. - **`internal/service/webhook/webhook_test.go`**: Comprehensive tests for source detection, event type extraction, payload parsing (all 3 platforms), commit URL fallback paths, and integration tests through `HandleWebhook` for GitHub and GitLab sources. - **`README.md`**: Updated description, features, non-goals, and architecture to reflect multi-platform webhook support. ## Test coverage Webhook package: **96.9%** statement coverage. Tests cover: - `DetectWebhookSource` with all header combinations and precedence - `DetectEventType` for each platform - `ParsePushPayload` for Gitea, GitHub, GitLab, unknown source, invalid JSON, empty payloads - Commit URL extraction fallback paths for GitHub and GitLab - Direct struct deserialization for all three payload types - Full `HandleWebhook` integration tests with GitHub and GitLab sources Co-authored-by: user <user@Mac.lan guest wan> Reviewed-on: sneak/upaas#170 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
79 lines
1.9 KiB
Go
79 lines
1.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
"sneak.berlin/go/upaas/internal/service/webhook"
|
|
)
|
|
|
|
// maxWebhookBodySize is the maximum allowed size of a webhook request body (1MB).
|
|
const maxWebhookBodySize = 1 << 20
|
|
|
|
// HandleWebhook handles incoming webhooks from Gitea, GitHub, or GitLab.
|
|
// The webhook source is auto-detected from HTTP headers.
|
|
func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
secret := chi.URLParam(request, "secret")
|
|
if secret == "" {
|
|
http.NotFound(writer, request)
|
|
|
|
return
|
|
}
|
|
|
|
// Find app by webhook secret
|
|
application, findErr := models.FindAppByWebhookSecret(
|
|
request.Context(),
|
|
h.db,
|
|
secret,
|
|
)
|
|
if findErr != nil {
|
|
h.log.Error("failed to find app by webhook secret", "error", findErr)
|
|
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
if application == nil {
|
|
http.NotFound(writer, request)
|
|
|
|
return
|
|
}
|
|
|
|
// Read request body with size limit to prevent memory exhaustion
|
|
body, readErr := io.ReadAll(io.LimitReader(request.Body, maxWebhookBodySize))
|
|
if readErr != nil {
|
|
h.log.Error("failed to read webhook body", "error", readErr)
|
|
http.Error(writer, "Bad Request", http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
// Auto-detect webhook source from headers
|
|
source := webhook.DetectWebhookSource(request.Header)
|
|
|
|
// Extract event type based on detected source
|
|
eventType := webhook.DetectEventType(request.Header, source)
|
|
|
|
// Process webhook
|
|
webhookErr := h.webhook.HandleWebhook(
|
|
request.Context(),
|
|
application,
|
|
source,
|
|
eventType,
|
|
body,
|
|
)
|
|
if webhookErr != nil {
|
|
h.log.Error("failed to process webhook", "error", webhookErr)
|
|
http.Error(writer, "Internal Server Error", http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
writer.WriteHeader(http.StatusOK)
|
|
}
|
|
}
|