142 lines
3.5 KiB
Go
142 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
ListenAddr string
|
|
WorkspaceRoot string // mkdtemp parent. empty = os.TempDir()
|
|
JobTimeout time.Duration // max wall-clock for a single Claude session
|
|
}
|
|
|
|
func loadConfig() Config {
|
|
cfg := Config{
|
|
ListenAddr: envOr("KLAUS_LISTEN", ":8020"),
|
|
WorkspaceRoot: os.Getenv("KLAUS_WORKSPACE"),
|
|
JobTimeout: 20 * time.Minute,
|
|
}
|
|
if v := os.Getenv("KLAUS_JOB_TIMEOUT"); v != "" {
|
|
if d, err := time.ParseDuration(v); err == nil {
|
|
cfg.JobTimeout = d
|
|
} else {
|
|
log.Printf("ignoring invalid KLAUS_JOB_TIMEOUT=%q: %v", v, err)
|
|
}
|
|
}
|
|
if cfg.WorkspaceRoot != "" {
|
|
if err := os.MkdirAll(cfg.WorkspaceRoot, 0o755); err != nil {
|
|
log.Fatalf("create workspace root %s: %v", cfg.WorkspaceRoot, err)
|
|
}
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func envOr(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func main() {
|
|
cfg := loadConfig()
|
|
worker := NewWorker(cfg)
|
|
|
|
http.HandleFunc("/gitea-webhook", func(w http.ResponseWriter, r *http.Request) {
|
|
eventType := r.Header.Get("X-Gitea-Event")
|
|
if eventType == "" {
|
|
eventType = r.Header.Get("X-Gogs-Event")
|
|
}
|
|
log.Printf("%s %s from %s event=%s", r.Method, r.URL.Path, r.RemoteAddr, eventType)
|
|
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
if !json.Valid(body) {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
event, trigger, err := DecideTrigger(eventType, body)
|
|
if err != nil {
|
|
log.Printf("decode webhook: %v", err)
|
|
http.Error(w, "bad payload", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if trigger == nil {
|
|
log.Printf("[skip] %s(action=%s) on %s by @%s — no klaus trigger; %s",
|
|
eventType, event.Action, event.Repository.FullName, event.Sender.Name(),
|
|
skipHint(eventType, event))
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
log.Printf("[queue] %s on %s#%d — reason=%s (by @%s)",
|
|
eventType, event.Repository.FullName, trigger.Number, trigger.Reason, event.Sender.Name())
|
|
|
|
worker.Enqueue(Job{
|
|
EventType: eventType,
|
|
Event: event,
|
|
Trigger: trigger,
|
|
})
|
|
w.WriteHeader(http.StatusAccepted)
|
|
})
|
|
|
|
log.Printf("klaus-bot listening on %s (workspace=%s, timeout=%s)",
|
|
cfg.ListenAddr, displayPath(cfg.WorkspaceRoot), cfg.JobTimeout)
|
|
log.Fatal(http.ListenAndServe(cfg.ListenAddr, nil))
|
|
}
|
|
|
|
func displayPath(p string) string {
|
|
if p == "" {
|
|
return os.TempDir() + " (default)"
|
|
}
|
|
return p
|
|
}
|
|
|
|
// skipHint produces a short diagnostic suffix for the "[skip]" log line so
|
|
// operators can tell at a glance why a payload didn't trigger Klaus.
|
|
func skipHint(eventType string, ev *GiteaEvent) string {
|
|
snippet := func(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
s = strings.ReplaceAll(s, "\n", " ")
|
|
if len(s) > 120 {
|
|
s = s[:120] + "…"
|
|
}
|
|
return s
|
|
}
|
|
switch eventType {
|
|
case "issue_comment":
|
|
if ev.Comment != nil {
|
|
return fmt.Sprintf("comment=%q", snippet(ev.Comment.Body))
|
|
}
|
|
case "issues":
|
|
if ev.Issue != nil {
|
|
return fmt.Sprintf("issue=%q", snippet(ev.Issue.Body))
|
|
}
|
|
case "pull_request":
|
|
if ev.PullRequest != nil {
|
|
return fmt.Sprintf("pr=%q", snippet(ev.PullRequest.Body))
|
|
}
|
|
case "pull_request_review":
|
|
if ev.Review != nil {
|
|
return fmt.Sprintf("review=%q", snippet(ev.Review.Body))
|
|
}
|
|
}
|
|
return "(no body)"
|
|
}
|