feat(webhook): Forgejo package webhook → ArgoCD image promotion (#55)
CI / CI (push) Successful in 3m21s
CI / CI (push) Successful in 3m21s
Closes #54 Replaces ArgoCD Image Updater with webhook-driven promotion. ## What changed | File | Change | |---|---| | `internal/webhook/promoter.go` | NEW — ImagePromoter + promotion table (10 entries) | | `internal/webhook/promoter_test.go` | NEW — 13 tests | | `internal/webhook/forgejo.go` | package event routed to ImagePromoter | | `k8s/rbac.yaml` | NEW — ClusterRole image-promoter + ClusterRoleBinding | | `k8s/deployment.yaml` | REGISTRY_PAT env var | | `k8s/network-policy.yaml` | egress cicd:3000 + default:443 | | `k8s/kustomization.yaml` | rbac.yaml added | ## homelab repo changes needed See comment on issue #54 for exact YAML. --------- Co-authored-by: Admin Bot <[email protected]> Reviewed-on: #55
This commit was merged in pull request #55.
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/notification"
|
||||
)
|
||||
@@ -18,8 +19,9 @@ import (
|
||||
// ForgejoHandler receives Forgejo webhook payloads and forwards them to Gotify.
|
||||
// Forgejo sends a Gitea-compatible JSON payload with X-Gitea-Signature-256 header.
|
||||
type ForgejoHandler struct {
|
||||
secret string
|
||||
gotify *notification.GotifyClient
|
||||
secret string
|
||||
gotify *notification.GotifyClient
|
||||
promoter *ImagePromoter
|
||||
}
|
||||
|
||||
// NewForgejoHandler creates a handler from environment variables.
|
||||
@@ -35,8 +37,9 @@ func NewForgejoHandler() *ForgejoHandler {
|
||||
}
|
||||
|
||||
return &ForgejoHandler{
|
||||
secret: os.Getenv("FORGEJO_WEBHOOK_SECRET"),
|
||||
gotify: client,
|
||||
secret: os.Getenv("FORGEJO_WEBHOOK_SECRET"),
|
||||
gotify: client,
|
||||
promoter: NewImagePromoter(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,17 +68,23 @@ func (h *ForgejoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
event := r.Header.Get("X-Gitea-Event")
|
||||
if event == "" {
|
||||
event = r.Header.Get("X-GitHub-Event")
|
||||
}
|
||||
|
||||
// package events trigger image promotion; handled independently of Gotify
|
||||
if event == "package" {
|
||||
h.handlePackagePromotion(w, r, body)
|
||||
return
|
||||
}
|
||||
|
||||
if h.gotify == nil {
|
||||
log.Printf("forgejo webhook received but Gotify not configured (GOTIFY_URL/GOTIFY_APP_TOKEN missing)")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
event := r.Header.Get("X-Gitea-Event")
|
||||
if event == "" {
|
||||
event = r.Header.Get("X-GitHub-Event")
|
||||
}
|
||||
|
||||
title, message, priority := h.formatMessage(event, body)
|
||||
if title == "" {
|
||||
// Unhandled event type — ack and ignore
|
||||
@@ -99,6 +108,81 @@ func (h *ForgejoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// handlePackagePromotion processes a Forgejo package webhook event.
|
||||
// It promotes container images by patching ArgoCD Application CRs.
|
||||
// Sends a Gotify notification with the result (success or failure).
|
||||
func (h *ForgejoHandler) handlePackagePromotion(w http.ResponseWriter, r *http.Request, body []byte) {
|
||||
// Generate a short request ID for correlating log lines from concurrent promotions.
|
||||
reqID := fmt.Sprintf("%08x", time.Now().UnixNano()&0xFFFFFFFF)
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
log.Printf("forgejo webhook [%s]: package event: invalid JSON: %v", reqID, err)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
action := fmt.Sprintf("%v", payload["action"])
|
||||
if action != "created" {
|
||||
// Ignore deletions and other actions
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
pkg, _ := payload["package"].(map[string]interface{})
|
||||
pkgType := fmt.Sprintf("%v", pkg["type"])
|
||||
if pkgType != "container" {
|
||||
// Only promote container images
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
owner := jsonStr(pkg, "owner", "login")
|
||||
name, _ := pkg["name"].(string)
|
||||
version, _ := pkg["version"].(string)
|
||||
|
||||
if owner == "" || name == "" || version == "" {
|
||||
log.Printf("forgejo webhook [%s]: package event: missing owner/name/version in payload", reqID)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
image := fmt.Sprintf("forgejo.riotpiao.com/%s/%s", owner, name)
|
||||
log.Printf("forgejo webhook [%s]: package event action=%s image=%s tag=%s", reqID, action, image, version)
|
||||
|
||||
var title, message string
|
||||
var priority int
|
||||
|
||||
if h.promoter == nil {
|
||||
title = fmt.Sprintf("📦 Image pushed: %s:%s", image, version)
|
||||
message = "Image promotion disabled — REGISTRY_PAT not configured"
|
||||
priority = 3
|
||||
log.Printf("forgejo webhook [%s]: promoter disabled, image=%s tag=%s", reqID, image, version)
|
||||
} else if err := h.promoter.Promote(r.Context(), image, version); err != nil {
|
||||
title = fmt.Sprintf("❌ Promotion failed: %s:%s", image, version)
|
||||
message = err.Error()
|
||||
priority = 8
|
||||
log.Printf("forgejo webhook [%s]: image promotion failed: %v", reqID, err)
|
||||
} else {
|
||||
title = fmt.Sprintf("🚀 Promoted: %s:%s", image, version)
|
||||
message = "ArgoCD Application patched — redeployment triggered"
|
||||
priority = 5
|
||||
log.Printf("forgejo webhook [%s]: promoted image=%s tag=%s", reqID, image, version)
|
||||
}
|
||||
|
||||
if h.gotify != nil && title != "" {
|
||||
if _, err := h.gotify.SendMessage(notification.GotifyMessage{
|
||||
Title: title,
|
||||
Message: message,
|
||||
Priority: priority,
|
||||
}); err != nil {
|
||||
log.Printf("forgejo webhook: failed to send gotify notification: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// verifySignature checks X-Gitea-Signature-256: sha256=<hex>
|
||||
func (h *ForgejoHandler) verifySignature(body []byte, sig string) bool {
|
||||
sig = strings.TrimPrefix(sig, "sha256=")
|
||||
|
||||
Reference in New Issue
Block a user