Complete implementation: - Go-based search service with PostgreSQL and Redis backend - BACKBEAT SDK integration for beat-aware search operations - Docker containerization with multi-stage builds - Comprehensive API endpoints for project analysis and search - Database migrations and schema management - GITEA integration for repository management - Team composition analysis and recommendations Key features: - Beat-synchronized search operations with timing coordination - Phase-based operation tracking (started → querying → ranking → completed) - Docker Swarm deployment configuration - Health checks and monitoring - Secure configuration with environment variables Architecture: - Microservice design with clean API boundaries - Background processing for long-running analysis - Modular internal structure with proper separation of concerns - Integration with CHORUS ecosystem via BACKBEAT timing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
190 lines
4.5 KiB
Go
190 lines
4.5 KiB
Go
package gitea
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
type WebhookHandler struct {
|
|
secret string
|
|
}
|
|
|
|
func NewWebhookHandler(secret string) *WebhookHandler {
|
|
return &WebhookHandler{
|
|
secret: secret,
|
|
}
|
|
}
|
|
|
|
func (h *WebhookHandler) ValidateSignature(payload []byte, signature string) bool {
|
|
if signature == "" {
|
|
log.Warn().Msg("No signature provided in webhook")
|
|
return false
|
|
}
|
|
|
|
// Remove "sha256=" prefix if present
|
|
signature = strings.TrimPrefix(signature, "sha256=")
|
|
|
|
// Calculate expected signature
|
|
mac := hmac.New(sha256.New, []byte(h.secret))
|
|
mac.Write(payload)
|
|
expectedSignature := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
// Compare signatures
|
|
return hmac.Equal([]byte(signature), []byte(expectedSignature))
|
|
}
|
|
|
|
func (h *WebhookHandler) ParsePayload(r *http.Request) (*WebhookPayload, error) {
|
|
// Read request body
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read request body: %w", err)
|
|
}
|
|
|
|
// Validate signature if secret is configured
|
|
if h.secret != "" {
|
|
signature := r.Header.Get("X-Gitea-Signature")
|
|
if !h.ValidateSignature(body, signature) {
|
|
return nil, fmt.Errorf("invalid webhook signature")
|
|
}
|
|
}
|
|
|
|
// Parse JSON payload
|
|
var payload WebhookPayload
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
return nil, fmt.Errorf("failed to parse webhook payload: %w", err)
|
|
}
|
|
|
|
return &payload, nil
|
|
}
|
|
|
|
func (h *WebhookHandler) IsTaskIssue(issue *Issue) bool {
|
|
if issue == nil {
|
|
return false
|
|
}
|
|
|
|
// Check for bzzz-task label
|
|
for _, label := range issue.Labels {
|
|
if label.Name == "bzzz-task" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Also check title/body for task indicators (MVP fallback)
|
|
title := strings.ToLower(issue.Title)
|
|
body := strings.ToLower(issue.Body)
|
|
|
|
taskIndicators := []string{"task:", "[task]", "bzzz-task", "agent task"}
|
|
for _, indicator := range taskIndicators {
|
|
if strings.Contains(title, indicator) || strings.Contains(body, indicator) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (h *WebhookHandler) ExtractTaskInfo(issue *Issue) map[string]interface{} {
|
|
if issue == nil {
|
|
return nil
|
|
}
|
|
|
|
taskInfo := map[string]interface{}{
|
|
"id": issue.ID,
|
|
"number": issue.Number,
|
|
"title": issue.Title,
|
|
"body": issue.Body,
|
|
"state": issue.State,
|
|
"url": issue.HTMLURL,
|
|
"repository": issue.Repository.FullName,
|
|
"created_at": issue.CreatedAt,
|
|
"updated_at": issue.UpdatedAt,
|
|
"labels": make([]string, len(issue.Labels)),
|
|
}
|
|
|
|
// Extract label names
|
|
for i, label := range issue.Labels {
|
|
taskInfo["labels"].([]string)[i] = label.Name
|
|
}
|
|
|
|
// Extract task priority from labels
|
|
priority := "normal"
|
|
for _, label := range issue.Labels {
|
|
switch strings.ToLower(label.Name) {
|
|
case "priority:high", "high-priority", "urgent":
|
|
priority = "high"
|
|
case "priority:low", "low-priority":
|
|
priority = "low"
|
|
case "priority:critical", "critical":
|
|
priority = "critical"
|
|
}
|
|
}
|
|
taskInfo["priority"] = priority
|
|
|
|
// Extract task type from labels
|
|
taskType := "general"
|
|
for _, label := range issue.Labels {
|
|
switch strings.ToLower(label.Name) {
|
|
case "type:bug", "bug":
|
|
taskType = "bug"
|
|
case "type:feature", "feature", "enhancement":
|
|
taskType = "feature"
|
|
case "type:docs", "documentation":
|
|
taskType = "documentation"
|
|
case "type:refactor", "refactoring":
|
|
taskType = "refactor"
|
|
case "type:test", "testing":
|
|
taskType = "test"
|
|
}
|
|
}
|
|
taskInfo["task_type"] = taskType
|
|
|
|
return taskInfo
|
|
}
|
|
|
|
type WebhookEvent struct {
|
|
Type string `json:"type"`
|
|
Action string `json:"action"`
|
|
Repository string `json:"repository"`
|
|
Issue *Issue `json:"issue,omitempty"`
|
|
TaskInfo map[string]interface{} `json:"task_info,omitempty"`
|
|
Timestamp int64 `json:"timestamp"`
|
|
}
|
|
|
|
func (h *WebhookHandler) ProcessWebhook(payload *WebhookPayload) *WebhookEvent {
|
|
event := &WebhookEvent{
|
|
Type: "gitea_webhook",
|
|
Action: payload.Action,
|
|
Repository: payload.Repository.FullName,
|
|
Timestamp: time.Now().Unix(),
|
|
}
|
|
|
|
|
|
if payload.Issue != nil {
|
|
event.Issue = payload.Issue
|
|
|
|
// Check if this is a task issue
|
|
if h.IsTaskIssue(payload.Issue) {
|
|
event.TaskInfo = h.ExtractTaskInfo(payload.Issue)
|
|
|
|
log.Info().
|
|
Str("action", payload.Action).
|
|
Str("repository", payload.Repository.FullName).
|
|
Int("issue_number", payload.Issue.Number).
|
|
Str("title", payload.Issue.Title).
|
|
Msg("Processing task issue webhook")
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|