Initial import with modified Dockerfile for env-based config generation

This commit is contained in:
2026-06-22 11:56:10 +03:00
parent 28fa7537ac
commit 6bf27aa40e
25 changed files with 3228 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
package webhook
import "time"
// GiteaEventType represents the type of Gitea webhook event
type GiteaEventType string
const (
EventPullRequest GiteaEventType = "pull_request"
EventPullRequestReview GiteaEventType = "pull_request_review"
EventPullRequestComment GiteaEventType = "pull_request_comment"
EventIssues GiteaEventType = "issues"
EventIssueComment GiteaEventType = "issue_comment"
)
// GiteaUser represents a Gitea user in webhook payloads
type GiteaUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
FullName string `json:"full_name"`
Email string `json:"email"`
Username string `json:"username"`
}
// Repository represents a Gitea repository
type Repository struct {
ID int64 `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
HTMLURL string `json:"html_url"`
}
// PullRequest represents a Gitea pull request
type PullRequest struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
HTMLURL string `json:"html_url"`
User GiteaUser `json:"user"`
Assignees []GiteaUser `json:"assignees"`
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
Merged bool `json:"merged"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Issue represents a Gitea issue
type Issue struct {
ID int64 `json:"id"`
Number int64 `json:"number"`
Title string `json:"title"`
Body string `json:"body"`
State string `json:"state"`
HTMLURL string `json:"html_url"`
User GiteaUser `json:"user"`
Assignees []GiteaUser `json:"assignees"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Comment represents a comment on PR or Issue
type Comment struct {
ID int64 `json:"id"`
Body string `json:"body"`
HTMLURL string `json:"html_url"`
User GiteaUser `json:"user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Review represents a PR review
type Review struct {
ID int64 `json:"id"`
Body string `json:"body"`
State string `json:"state"` // APPROVED, CHANGES_REQUESTED, COMMENT
HTMLURL string `json:"html_url"`
User GiteaUser `json:"user"`
SubmittedAt time.Time `json:"submitted_at"`
}
// Commit represents a Git commit in webhook payloads
type Commit struct {
ID string `json:"id"` // SHA
Message string `json:"message"`
URL string `json:"url"`
Author GitUser `json:"author"`
Committer GitUser `json:"committer"`
Timestamp time.Time `json:"timestamp"`
}
// GitUser represents a git author/committer (different from GiteaUser)
type GitUser struct {
Name string `json:"name"`
Email string `json:"email"`
Username string `json:"username"`
}
// PullRequestEvent is the payload for pull_request webhooks
type PullRequestEvent struct {
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, review_requested, synchronize, etc.
Number int64 `json:"number"`
PullRequest PullRequest `json:"pull_request"`
Repository Repository `json:"repository"`
Sender GiteaUser `json:"sender"`
RequestedReviewers []GiteaUser `json:"requested_reviewers"`
RequestedReviewer *GiteaUser `json:"requested_reviewer"` // Present on review_requested action (singular)
Assignee *GiteaUser `json:"assignee"` // Present on assigned/unassigned action
Commits []Commit `json:"commits"` // Present on synchronize action
}
// PullRequestReviewEvent is the payload for pull_request_review webhooks
type PullRequestReviewEvent struct {
Action string `json:"action"` // submitted
Review Review `json:"review"`
PullRequest PullRequest `json:"pull_request"`
Repository Repository `json:"repository"`
Sender GiteaUser `json:"sender"`
}
// PullRequestCommentEvent is the payload for pull_request_comment webhooks
type PullRequestCommentEvent struct {
Action string `json:"action"` // created, edited, deleted
Comment Comment `json:"comment"`
PullRequest PullRequest `json:"pull_request"`
Repository Repository `json:"repository"`
Sender GiteaUser `json:"sender"`
}
// IssueEvent is the payload for issues webhooks
type IssueEvent struct {
Action string `json:"action"` // opened, closed, reopened, edited, assigned, unassigned, etc.
Issue Issue `json:"issue"`
Repository Repository `json:"repository"`
Sender GiteaUser `json:"sender"`
}
// IssueCommentEvent is the payload for issue_comment webhooks
type IssueCommentEvent struct {
Action string `json:"action"` // created, edited, deleted
Comment Comment `json:"comment"`
Issue Issue `json:"issue"`
Repository Repository `json:"repository"`
Sender GiteaUser `json:"sender"`
}
+137
View File
@@ -0,0 +1,137 @@
package webhook
import (
"encoding/json"
"net/http"
"github.com/rs/zerolog"
)
// EventHandler processes webhook events
type EventHandler interface {
HandlePullRequest(event *PullRequestEvent)
HandlePullRequestReview(event *PullRequestReviewEvent)
HandlePullRequestComment(event *PullRequestCommentEvent)
HandleIssue(event *IssueEvent)
HandleIssueComment(event *IssueCommentEvent)
}
// Handler handles incoming Gitea webhooks
type Handler struct {
validator *Validator
eventHandler EventHandler
logger zerolog.Logger
}
// NewHandler creates a new webhook handler
func NewHandler(secret string, eventHandler EventHandler, logger zerolog.Logger) *Handler {
return &Handler{
validator: NewValidator(secret),
eventHandler: eventHandler,
logger: logger.With().Str("component", "webhook").Logger(),
}
}
// ServeHTTP handles the webhook HTTP request
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Only accept POST requests
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Get event metadata
eventType := GetEventType(r)
deliveryID := GetDeliveryID(r)
logger := h.logger.With().
Str("delivery_id", deliveryID).
Str("event_type", string(eventType)).
Logger()
// Validate signature and read body
body, err := h.validator.ValidateRequest(r)
if err != nil {
logger.Warn().Err(err).Msg("webhook validation failed")
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// Respond immediately with 200 OK
// Process the event asynchronously
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"accepted"}`))
// Process the event in a goroutine
go h.processEvent(logger, eventType, body)
}
// processEvent parses and routes the event to the appropriate handler
func (h *Handler) processEvent(logger zerolog.Logger, eventType GiteaEventType, body []byte) {
var err error
switch eventType {
case EventPullRequest:
var event PullRequestEvent
if err = json.Unmarshal(body, &event); err == nil {
logger.Info().
Str("action", event.Action).
Int64("pr_number", event.Number).
Str("repo", event.Repository.FullName).
Msg("processing pull request event")
h.eventHandler.HandlePullRequest(&event)
}
case EventPullRequestReview:
var event PullRequestReviewEvent
if err = json.Unmarshal(body, &event); err == nil {
logger.Info().
Str("action", event.Action).
Str("review_state", event.Review.State).
Str("repo", event.Repository.FullName).
Msg("processing pull request review event")
h.eventHandler.HandlePullRequestReview(&event)
}
case EventPullRequestComment:
var event PullRequestCommentEvent
if err = json.Unmarshal(body, &event); err == nil {
logger.Info().
Str("action", event.Action).
Int64("pr_number", event.PullRequest.Number).
Str("repo", event.Repository.FullName).
Msg("processing pull request comment event")
h.eventHandler.HandlePullRequestComment(&event)
}
case EventIssues:
var event IssueEvent
if err = json.Unmarshal(body, &event); err == nil {
logger.Info().
Str("action", event.Action).
Int64("issue_number", event.Issue.Number).
Str("repo", event.Repository.FullName).
Msg("processing issue event")
h.eventHandler.HandleIssue(&event)
}
case EventIssueComment:
var event IssueCommentEvent
if err = json.Unmarshal(body, &event); err == nil {
logger.Info().
Str("action", event.Action).
Int64("issue_number", event.Issue.Number).
Str("repo", event.Repository.FullName).
Msg("processing issue comment event")
h.eventHandler.HandleIssueComment(&event)
}
default:
logger.Debug().Msg("ignoring unknown event type")
return
}
if err != nil {
logger.Error().Err(err).Msg("failed to parse event payload")
}
}
+75
View File
@@ -0,0 +1,75 @@
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"strings"
)
var (
ErrMissingSignature = errors.New("missing X-Gitea-Signature header")
ErrInvalidSignature = errors.New("invalid webhook signature")
)
// Validator validates Gitea webhook signatures
type Validator struct {
secret []byte
}
// NewValidator creates a new webhook validator with the given secret
func NewValidator(secret string) *Validator {
return &Validator{
secret: []byte(secret),
}
}
// ValidateRequest validates the signature of an incoming webhook request
// It returns the request body if valid, or an error if validation fails
func (v *Validator) ValidateRequest(r *http.Request) ([]byte, error) {
// Read the body
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, err
}
// Get signature from header
signature := r.Header.Get("X-Gitea-Signature")
if signature == "" {
return nil, ErrMissingSignature
}
// Validate signature
if !v.validateSignature(body, signature) {
return nil, ErrInvalidSignature
}
return body, nil
}
// validateSignature checks if the HMAC-SHA256 signature matches
func (v *Validator) validateSignature(payload []byte, signature string) bool {
// Gitea sends the signature as a hex-encoded HMAC-SHA256
mac := hmac.New(sha256.New, v.secret)
mac.Write(payload)
expectedMAC := mac.Sum(nil)
expectedSignature := hex.EncodeToString(expectedMAC)
// Handle both with and without "sha256=" prefix
signature = strings.TrimPrefix(signature, "sha256=")
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
// GetEventType extracts the event type from the request headers
func GetEventType(r *http.Request) GiteaEventType {
return GiteaEventType(r.Header.Get("X-Gitea-Event"))
}
// GetDeliveryID extracts the unique delivery ID from the request headers
func GetDeliveryID(r *http.Request) string {
return r.Header.Get("X-Gitea-Delivery")
}