Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2171424220 | ||
|
|
3c6c855761 | ||
|
|
7237e47854 | ||
|
|
f18e6331ea | ||
|
|
bb792d463f | ||
|
|
d439536ca9 |
@@ -1,4 +0,0 @@
|
|||||||
(apply,CacheStats{hitCount=337, missCount=199, loadSuccessCount=199, loadExceptionCount=0, totalLoadTime=581291927, evictionCount=0})
|
|
||||||
(tree,CacheStats{hitCount=986, missCount=352, loadSuccessCount=299, loadExceptionCount=0, totalLoadTime=821650758, evictionCount=0})
|
|
||||||
(commit,CacheStats{hitCount=108, missCount=107, loadSuccessCount=107, loadExceptionCount=0, totalLoadTime=78983052, evictionCount=0})
|
|
||||||
(tag,CacheStats{hitCount=0, missCount=2, loadSuccessCount=2, loadExceptionCount=0, totalLoadTime=319542, evictionCount=0})
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
e71e5b78236a67327c678490cb50b46981f19de0 bbcbb68b91e786eb71bbb0a4443d7b8a26140e1b .sops.yaml
|
|
||||||
4189696f5581ac0ffdc125c3bf9b9f664b3ddfb0 7cd3f1ee4865c563d141464f6fc185436993b84b .sops.yaml
|
|
||||||
635630e73152a5f22e6cbd42322ec55d79f8d9c0 297e94a89d73d18c4f47013bb0e8303f123715f3 configmap.yaml
|
|
||||||
29e515e7b46742fab8c3fcc2189af7010a6ccc62 6869fa11f96e03f7ec76a0ea14a4ddaf604004a4 gateway-config-secret.enc.yaml
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
0a95af80c0051bacbeb8483c1632e47acd3db5be 40207e487cfb63409a976fb2a0b9e1e62c8b1513
|
|
||||||
27428d910111299d0699f429190284a9ca6e50b7 3318daf758349402aef43b095482743ab96b37f9
|
|
||||||
329a495af4c935529fdae17229314101c0c77876 67f24ea76359c8dba4b56267790aad76bbc58464
|
|
||||||
4c8bc6c920b6b75399555827022f69ef0c4f7d15 1fa839b41975fa3f0ac9052355ffb625f5a8f324
|
|
||||||
528545f414c83217408edfea234dcd1f3edee0c2 b8f95506ca1545b876b5531cd385172e9ca5b4b0
|
|
||||||
81038e1cf7567a9133d7c233a97b1e2f19fa1c82 4a00312906ba725f3968187656fde2663b1763ab
|
|
||||||
a5b3b5c44a406896bcb414df6c6426c277715706 2ab47a9dbe5ba36dfa0e275991ef7b7656908410
|
|
||||||
ce27643667a0399115cd1f2b6d38123fdcf2b4f1 6ff0a50de8efbad105fa588245f22fdb26afddc4
|
|
||||||
d49756886a46542b38533b913a1f776b5145f5ec d82cc5a6970a1fb32e21dda9a737b987a8668111
|
|
||||||
db3a30fbcf1f139c667fb68a91762582c49b8cee 04619a269fed9eeea53ab4d4d73131e3713f40a0
|
|
||||||
eb54715e4dec0fb35402576fcc224a09808b00c1 d53b7632cf9646dda1c978a5f94615dc9eaed5e8
|
|
||||||
ef72b5bbccf2df89aa1c86dee29311c63f33bf62 ba55d184fefef1a73a50409ca4fb1f7b27f5b075
|
|
||||||
+5
-1
@@ -108,8 +108,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
_ = registry.Add(notifAdapter)
|
_ = registry.Add(notifAdapter)
|
||||||
|
|
||||||
// Add other adapters from config
|
// Add other adapters from config (skip if already registered in code)
|
||||||
for _, a := range cfg.Adapters {
|
for _, a := range cfg.Adapters {
|
||||||
|
if existing := registry.Get(a.ServiceName); existing != nil {
|
||||||
|
log.Printf("skip config adapter '%s': already registered with internal handler", a.ServiceName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
_ = registry.Add(a)
|
_ = registry.Add(a)
|
||||||
}
|
}
|
||||||
log.Printf("%d service adapters loaded", registry.Count())
|
log.Printf("%d service adapters loaded", registry.Count())
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/serviceadapter"
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Router implements an HTTP handler that routes health endpoints,
|
// Router implements an HTTP handler that routes health endpoints,
|
||||||
@@ -14,6 +15,7 @@ type Router struct {
|
|||||||
dispatcher *serviceadapter.Dispatcher
|
dispatcher *serviceadapter.Dispatcher
|
||||||
temporalHandler http.Handler
|
temporalHandler http.Handler
|
||||||
upstreamHandler http.Handler
|
upstreamHandler http.Handler
|
||||||
|
forgejoWebhook *webhook.ForgejoHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRouter creates a new router with health endpoints.
|
// NewRouter creates a new router with health endpoints.
|
||||||
@@ -23,10 +25,11 @@ type Router struct {
|
|||||||
// All other paths are passed to the upstream handler.
|
// All other paths are passed to the upstream handler.
|
||||||
func NewRouter(healthChecker *HealthChecker, dispatcher *serviceadapter.Dispatcher, temporalHandler http.Handler, upstreamHandler http.Handler) *Router {
|
func NewRouter(healthChecker *HealthChecker, dispatcher *serviceadapter.Dispatcher, temporalHandler http.Handler, upstreamHandler http.Handler) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
healthChecker: healthChecker,
|
healthChecker: healthChecker,
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
temporalHandler: temporalHandler,
|
temporalHandler: temporalHandler,
|
||||||
upstreamHandler: upstreamHandler,
|
upstreamHandler: upstreamHandler,
|
||||||
|
forgejoWebhook: webhook.NewForgejoHandler(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +60,12 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forgejo webhook receiver — no auth, HMAC-verified by handler
|
||||||
|
if req.URL.Path == "/v1/webhooks/forgejo" {
|
||||||
|
r.forgejoWebhook.ServeHTTP(w, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Workflow endpoints
|
// Workflow endpoints
|
||||||
// DEPRECATED: Path-based /workflow routing is legacy.
|
// DEPRECATED: Path-based /workflow routing is legacy.
|
||||||
// New clients should use X-Service: workflow header instead for consistent auth.
|
// New clients should use X-Service: workflow header instead for consistent auth.
|
||||||
|
|||||||
@@ -24,6 +24,66 @@ func NewWorkflowAdapter(handler *temporal.Handler) *WorkflowAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleStart handles workflow start requests.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "workflow_type": "...", "task_queue": "...", "input": {...} }
|
||||||
|
func (wa *WorkflowAdapter) HandleStart(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleDescribe handles workflow describe requests.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||||
|
func (wa *WorkflowAdapter) HandleDescribe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleList handles workflow list requests.
|
||||||
|
// Expects payload: { "namespace": "default", "query": "..." (optional) }
|
||||||
|
func (wa *WorkflowAdapter) HandleList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleHistory handles workflow history requests.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||||
|
func (wa *WorkflowAdapter) HandleHistory(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleTerminate handles workflow termination.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "reason": "..." }
|
||||||
|
func (wa *WorkflowAdapter) HandleTerminate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCancel handles workflow cancellation.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "..." }
|
||||||
|
func (wa *WorkflowAdapter) HandleCancel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleSignal handles workflow signal.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "signal_name": "...", "signal_data": {...} }
|
||||||
|
func (wa *WorkflowAdapter) HandleSignal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleQuery handles workflow query.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "query_type": "...", "query_data": {...} }
|
||||||
|
func (wa *WorkflowAdapter) HandleQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleReset handles workflow reset.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "reset_type": "..." }
|
||||||
|
func (wa *WorkflowAdapter) HandleReset(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleUpdate handles workflow update.
|
||||||
|
// Expects payload: { "namespace": "default", "workflow_id": "...", "update_data": {...} }
|
||||||
|
func (wa *WorkflowAdapter) HandleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
wa.forwardToTemporal(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// resourceToAction maps X-Resource names to Temporal action names.
|
// resourceToAction maps X-Resource names to Temporal action names.
|
||||||
var resourceToAction = map[string]string{
|
var resourceToAction = map[string]string{
|
||||||
"execute": "START_WORKFLOW",
|
"execute": "START_WORKFLOW",
|
||||||
@@ -74,8 +134,13 @@ func (wa *WorkflowAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Inject action into body for temporal handler
|
// Inject action into body for temporal handler
|
||||||
payload["action"] = action
|
payload["action"] = action
|
||||||
if _, ok := payload["namespace"]; !ok {
|
|
||||||
payload["namespace"] = "default"
|
// namespace is required for all workflow operations
|
||||||
|
if ns, ok := payload["namespace"].(string); !ok || ns == "" {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, `{"error":"namespace is required"}`)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody, _ := json.Marshal(payload)
|
newBody, _ := json.Marshal(payload)
|
||||||
@@ -86,17 +151,51 @@ func (wa *WorkflowAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
wa.temporalHandler.ServeHTTP(w, r)
|
wa.temporalHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWorkflowSpec returns the ServiceAdapter spec for workflow service.
|
// forwardToTemporal reads the request body, ensures namespace is specified,
|
||||||
|
// and forwards to the temporal handler.
|
||||||
|
func (wa *WorkflowAdapter) forwardToTemporal(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Read request body
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("failed to read request body: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// Parse JSON to check for namespace
|
||||||
|
var payload map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("invalid JSON payload: %v", err), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure namespace is specified (required for Temporal routing)
|
||||||
|
namespace, ok := payload["namespace"].(string)
|
||||||
|
if !ok || namespace == "" {
|
||||||
|
http.Error(w, `"namespace" field required in payload`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to temporal handler by calling it with the request
|
||||||
|
// Restore body for temporal handler
|
||||||
|
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||||
|
r.ContentLength = int64(len(body))
|
||||||
|
|
||||||
|
// Call temporal handler
|
||||||
|
wa.temporalHandler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpec returns the ServiceAdapter spec for workflow service.
|
||||||
// This defines the available resources and methods.
|
// This defines the available resources and methods.
|
||||||
func GetWorkflowSpec() *Spec {
|
func GetWorkflowSpec() *Spec {
|
||||||
return &Spec{
|
return &Spec{
|
||||||
ServiceName: "workflow",
|
ServiceName: "workflow",
|
||||||
Upstream: Upstream{
|
Upstream: Upstream{
|
||||||
URL: "grpc://temporal:7233",
|
URL: "grpc://temporal:7233", // gRPC endpoint
|
||||||
TimeoutSeconds: 30,
|
TimeoutSeconds: 30,
|
||||||
},
|
},
|
||||||
Auth: Auth{
|
Auth: Auth{
|
||||||
Required: true,
|
Required: false,
|
||||||
Capability: "workflow:execute",
|
Capability: "workflow:execute",
|
||||||
},
|
},
|
||||||
Retryable: true,
|
Retryable: true,
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"forgejo.riotpiao.com/rock/homelab-frontend/internal/notification"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewForgejoHandler creates a handler from environment variables.
|
||||||
|
// Required: GOTIFY_URL, GOTIFY_APP_TOKEN
|
||||||
|
// Optional: FORGEJO_WEBHOOK_SECRET (if empty, HMAC verification is skipped)
|
||||||
|
func NewForgejoHandler() *ForgejoHandler {
|
||||||
|
gotifyURL := os.Getenv("GOTIFY_URL")
|
||||||
|
appToken := os.Getenv("GOTIFY_APP_TOKEN")
|
||||||
|
|
||||||
|
var client *notification.GotifyClient
|
||||||
|
if gotifyURL != "" && appToken != "" {
|
||||||
|
client = notification.NewGotifyClient(gotifyURL, appToken, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ForgejoHandler{
|
||||||
|
secret: os.Getenv("FORGEJO_WEBHOOK_SECRET"),
|
||||||
|
gotify: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handles POST /v1/webhooks/forgejo
|
||||||
|
func (h *ForgejoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // 1MB limit
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify HMAC if secret is set
|
||||||
|
if h.secret != "" {
|
||||||
|
sig := r.Header.Get("X-Gitea-Signature-256")
|
||||||
|
if sig == "" {
|
||||||
|
sig = r.Header.Get("X-Hub-Signature-256")
|
||||||
|
}
|
||||||
|
if !h.verifySignature(body, sig) {
|
||||||
|
http.Error(w, "invalid signature", http.StatusForbidden)
|
||||||
|
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
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := notification.GotifyMessage{
|
||||||
|
Title: title,
|
||||||
|
Message: message,
|
||||||
|
Priority: priority,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.gotify.SendMessage(msg); err != nil {
|
||||||
|
log.Printf("forgejo webhook: failed to send gotify message: %v", err)
|
||||||
|
http.Error(w, "failed to send notification", http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("forgejo webhook: sent gotify notification event=%s title=%q", event, title)
|
||||||
|
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=")
|
||||||
|
if sig == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
mac := hmac.New(sha256.New, []byte(h.secret))
|
||||||
|
mac.Write(body)
|
||||||
|
expected := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
return hmac.Equal([]byte(sig), []byte(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatMessage converts a Forgejo event payload into a Gotify title + message.
|
||||||
|
// Returns empty title if the event should be ignored.
|
||||||
|
func (h *ForgejoHandler) formatMessage(event string, body []byte) (title, message string, priority int) {
|
||||||
|
var payload map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
return "", "", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := jsonStr(payload, "repository", "full_name")
|
||||||
|
sender := jsonStr(payload, "sender", "login")
|
||||||
|
|
||||||
|
switch event {
|
||||||
|
case "push":
|
||||||
|
ref := strings.TrimPrefix(fmt.Sprintf("%v", payload["ref"]), "refs/heads/")
|
||||||
|
commits, _ := payload["commits"].([]interface{})
|
||||||
|
count := len(commits)
|
||||||
|
commitMsg := ""
|
||||||
|
if count > 0 {
|
||||||
|
if c, ok := commits[0].(map[string]interface{}); ok {
|
||||||
|
commitMsg = fmt.Sprintf("%v", c["message"])
|
||||||
|
// truncate long commit messages
|
||||||
|
if len(commitMsg) > 80 {
|
||||||
|
commitMsg = commitMsg[:80] + "…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("📦 %s", repo),
|
||||||
|
fmt.Sprintf("%s pushed %d commit(s) to %s\n%s", sender, count, ref, commitMsg),
|
||||||
|
5
|
||||||
|
|
||||||
|
case "pull_request":
|
||||||
|
action := fmt.Sprintf("%v", payload["action"])
|
||||||
|
if action != "opened" && action != "closed" && action != "reopened" && action != "merged" {
|
||||||
|
return "", "", 0 // ignore noise (labeled, assigned, etc.)
|
||||||
|
}
|
||||||
|
pr, _ := payload["pull_request"].(map[string]interface{})
|
||||||
|
number := fmt.Sprintf("%v", pr["number"])
|
||||||
|
prTitle := fmt.Sprintf("%v", pr["title"])
|
||||||
|
merged, _ := pr["merged"].(bool)
|
||||||
|
if action == "closed" && merged {
|
||||||
|
action = "merged"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("🔀 PR #%s %s — %s", number, action, repo),
|
||||||
|
fmt.Sprintf("%s: %s\nby %s", action, prTitle, sender),
|
||||||
|
5
|
||||||
|
|
||||||
|
case "issues":
|
||||||
|
action := fmt.Sprintf("%v", payload["action"])
|
||||||
|
if action != "opened" && action != "closed" && action != "reopened" {
|
||||||
|
return "", "", 0
|
||||||
|
}
|
||||||
|
issue, _ := payload["issue"].(map[string]interface{})
|
||||||
|
number := fmt.Sprintf("%v", issue["number"])
|
||||||
|
issueTitle := fmt.Sprintf("%v", issue["title"])
|
||||||
|
return fmt.Sprintf("🐛 Issue #%s %s — %s", number, action, repo),
|
||||||
|
fmt.Sprintf("%s: %s\nby %s", action, issueTitle, sender),
|
||||||
|
4
|
||||||
|
|
||||||
|
case "issue_comment", "pull_request_review_comment":
|
||||||
|
issue, _ := payload["issue"].(map[string]interface{})
|
||||||
|
comment, _ := payload["comment"].(map[string]interface{})
|
||||||
|
number := fmt.Sprintf("%v", issue["number"])
|
||||||
|
body := fmt.Sprintf("%v", comment["body"])
|
||||||
|
if len(body) > 100 {
|
||||||
|
body = body[:100] + "…"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("💬 Comment on #%s — %s", number, repo),
|
||||||
|
fmt.Sprintf("%s: %s", sender, body),
|
||||||
|
3
|
||||||
|
|
||||||
|
case "release":
|
||||||
|
action := fmt.Sprintf("%v", payload["action"])
|
||||||
|
if action != "published" {
|
||||||
|
return "", "", 0
|
||||||
|
}
|
||||||
|
release, _ := payload["release"].(map[string]interface{})
|
||||||
|
tag := fmt.Sprintf("%v", release["tag_name"])
|
||||||
|
name := fmt.Sprintf("%v", release["name"])
|
||||||
|
return fmt.Sprintf("🚀 Release %s — %s", tag, repo),
|
||||||
|
fmt.Sprintf("%s published by %s", name, sender),
|
||||||
|
7
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "", "", 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// jsonStr safely traverses nested map keys.
|
||||||
|
func jsonStr(m map[string]interface{}, keys ...string) string {
|
||||||
|
cur := m
|
||||||
|
for i, k := range keys {
|
||||||
|
v, ok := cur[k]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if i == len(keys)-1 {
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
cur, ok = v.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -76,6 +76,22 @@ spec:
|
|||||||
value: "1.0.0"
|
value: "1.0.0"
|
||||||
- name: OTEL_ENVIRONMENT
|
- name: OTEL_ENVIRONMENT
|
||||||
value: "production"
|
value: "production"
|
||||||
|
# Gotify integration — Forgejo webhook → push notifications
|
||||||
|
- name: GOTIFY_URL
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-webhook-secret
|
||||||
|
key: gotify-url
|
||||||
|
- name: GOTIFY_APP_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-webhook-secret
|
||||||
|
key: gotify-app-token
|
||||||
|
- name: FORGEJO_WEBHOOK_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: gotify-webhook-secret
|
||||||
|
key: forgejo-webhook-secret
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: config
|
- name: config
|
||||||
mountPath: /etc/gateway
|
mountPath: /etc/gateway
|
||||||
|
|||||||
Reference in New Issue
Block a user