summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-16 22:11:34 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-16 22:11:34 +0000
commitb0b79966e957bd5e4fc014978dbf7e9dbebeac61 (patch)
treef0bd8426b09d49bbbce81619aa1d7fed0243ab60 /internal/api
parent1b6b27357c817359574605b854f6468917da314d (diff)
fix: prefix SW registration path with BASE_PATH
The app is served at /claudomator/ so the SW and scope must use BASE_PATH + '/api/push/sw.js' and BASE_PATH + '/' respectively. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/webhook.go57
-rw-r--r--internal/api/webhook_test.go98
2 files changed, 142 insertions, 13 deletions
diff --git a/internal/api/webhook.go b/internal/api/webhook.go
index 8bf1676..0530f3e 100644
--- a/internal/api/webhook.go
+++ b/internal/api/webhook.go
@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
+ "log/slog"
"net/http"
"path/filepath"
"strings"
@@ -17,17 +18,26 @@ import (
"github.com/thepeterstone/claudomator/internal/task"
)
+// prRef is a minimal pull request entry used to extract branch names.
+type prRef struct {
+ Head struct {
+ Ref string `json:"ref"`
+ } `json:"head"`
+}
+
// checkRunPayload is the GitHub check_run webhook payload.
type checkRunPayload struct {
Action string `json:"action"`
CheckRun struct {
- Name string `json:"name"`
- Conclusion string `json:"conclusion"`
- HTMLURL string `json:"html_url"`
- HeadSHA string `json:"head_sha"`
- CheckSuite struct {
+ Name string `json:"name"`
+ Conclusion string `json:"conclusion"`
+ HTMLURL string `json:"html_url"`
+ DetailsURL string `json:"details_url"`
+ HeadSHA string `json:"head_sha"`
+ CheckSuite struct {
HeadBranch string `json:"head_branch"`
} `json:"check_suite"`
+ PullRequests []prRef `json:"pull_requests"`
} `json:"check_run"`
Repository struct {
Name string `json:"name"`
@@ -39,11 +49,12 @@ type checkRunPayload struct {
type workflowRunPayload struct {
Action string `json:"action"`
WorkflowRun struct {
- Name string `json:"name"`
- Conclusion string `json:"conclusion"`
- HTMLURL string `json:"html_url"`
- HeadSHA string `json:"head_sha"`
- HeadBranch string `json:"head_branch"`
+ Name string `json:"name"`
+ Conclusion string `json:"conclusion"`
+ HTMLURL string `json:"html_url"`
+ HeadSHA string `json:"head_sha"`
+ HeadBranch string `json:"head_branch"`
+ PullRequests []prRef `json:"pull_requests"`
} `json:"workflow_run"`
Repository struct {
Name string `json:"name"`
@@ -97,6 +108,7 @@ func (s *Server) handleGitHubWebhook(w http.ResponseWriter, r *http.Request) {
}
eventType := r.Header.Get("X-GitHub-Event")
+ slog.Info("github webhook received", "event", eventType, "bytes", len(body))
switch eventType {
case "check_run":
s.handleCheckRunEvent(w, body)
@@ -117,13 +129,22 @@ func (s *Server) handleCheckRunEvent(w http.ResponseWriter, body []byte) {
w.WriteHeader(http.StatusNoContent)
return
}
+ branch := p.CheckRun.CheckSuite.HeadBranch
+ if branch == "" && len(p.CheckRun.PullRequests) > 0 {
+ branch = p.CheckRun.PullRequests[0].Head.Ref
+ }
+ htmlURL := p.CheckRun.HTMLURL
+ if htmlURL == "" {
+ htmlURL = p.CheckRun.DetailsURL
+ }
+ slog.Info("check_run webhook", "repo", p.Repository.FullName, "conclusion", p.CheckRun.Conclusion, "branch", branch, "html_url", htmlURL)
s.createCIFailureTask(w,
p.Repository.Name,
p.Repository.FullName,
- p.CheckRun.CheckSuite.HeadBranch,
+ branch,
p.CheckRun.HeadSHA,
p.CheckRun.Name,
- p.CheckRun.HTMLURL,
+ htmlURL,
)
}
@@ -141,10 +162,15 @@ func (s *Server) handleWorkflowRunEvent(w http.ResponseWriter, body []byte) {
w.WriteHeader(http.StatusNoContent)
return
}
+ branch := p.WorkflowRun.HeadBranch
+ if branch == "" && len(p.WorkflowRun.PullRequests) > 0 {
+ branch = p.WorkflowRun.PullRequests[0].Head.Ref
+ }
+ slog.Info("workflow_run webhook", "repo", p.Repository.FullName, "conclusion", p.WorkflowRun.Conclusion, "branch", branch, "html_url", p.WorkflowRun.HTMLURL)
s.createCIFailureTask(w,
p.Repository.Name,
p.Repository.FullName,
- p.WorkflowRun.HeadBranch,
+ branch,
p.WorkflowRun.HeadSHA,
p.WorkflowRun.Name,
p.WorkflowRun.HTMLURL,
@@ -154,6 +180,10 @@ func (s *Server) handleWorkflowRunEvent(w http.ResponseWriter, body []byte) {
func (s *Server) createCIFailureTask(w http.ResponseWriter, repoName, fullName, branch, sha, checkName, htmlURL string) {
project := matchProject(s.projects, repoName)
+ if htmlURL == "" && fullName != "" && sha != "" {
+ htmlURL = fmt.Sprintf("https://github.com/%s/commit/%s", fullName, sha)
+ }
+
instructions := fmt.Sprintf(
"A CI failure has been detected and requires investigation.\n\n"+
"Repository: %s\n"+
@@ -175,6 +205,7 @@ func (s *Server) createCIFailureTask(w http.ResponseWriter, repoName, fullName,
Name: fmt.Sprintf("Fix CI failure: %s on %s", checkName, branch),
Agent: task.AgentConfig{
Type: "claude",
+ Model: "sonnet",
Instructions: instructions,
MaxBudgetUSD: 3.0,
AllowedTools: []string{"Read", "Edit", "Bash", "Glob", "Grep"},
diff --git a/internal/api/webhook_test.go b/internal/api/webhook_test.go
index 8b0599a..1bc4aaa 100644
--- a/internal/api/webhook_test.go
+++ b/internal/api/webhook_test.go
@@ -237,6 +237,104 @@ func TestGitHubWebhook_NoSecretConfigured_SkipsHMACCheck(t *testing.T) {
}
}
+func TestGitHubWebhook_CreatesTask_WithDefaultModel(t *testing.T) {
+ srv, store := testServer(t)
+ srv.projects = []config.Project{{Name: "myrepo", Dir: "/workspace/myrepo"}}
+
+ w := webhookPost(t, srv, "check_run", checkRunFailurePayload, "")
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d", w.Code)
+ }
+ var resp map[string]string
+ json.NewDecoder(w.Body).Decode(&resp)
+ tk, err := store.GetTask(resp["task_id"])
+ if err != nil {
+ t.Fatalf("task not found: %v", err)
+ }
+ if tk.Agent.Model == "" {
+ t.Error("expected model to be set, got empty string")
+ }
+}
+
+const checkRunNullBranchPayload = `{
+ "action": "completed",
+ "check_run": {
+ "name": "CI Build",
+ "conclusion": "failure",
+ "html_url": "",
+ "details_url": "https://github.com/owner/myrepo/actions/runs/999/jobs/123",
+ "head_sha": "abc123def",
+ "check_suite": {
+ "head_branch": null
+ },
+ "pull_requests": [{"head": {"ref": "feature/my-branch"}}]
+ },
+ "repository": {
+ "name": "myrepo",
+ "full_name": "owner/myrepo"
+ }
+}`
+
+func TestGitHubWebhook_CheckRunNullBranch_UsesPRRefAndDetailsURL(t *testing.T) {
+ srv, store := testServer(t)
+ srv.projects = []config.Project{{Name: "myrepo", Dir: "/workspace/myrepo"}}
+
+ w := webhookPost(t, srv, "check_run", checkRunNullBranchPayload, "")
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d; body: %s", w.Code, w.Body.String())
+ }
+ var resp map[string]string
+ json.NewDecoder(w.Body).Decode(&resp)
+ tk, err := store.GetTask(resp["task_id"])
+ if err != nil {
+ t.Fatalf("task not found: %v", err)
+ }
+ if !strings.Contains(tk.Name, "feature/my-branch") {
+ t.Errorf("task name %q should contain PR branch", tk.Name)
+ }
+ if !strings.Contains(tk.Agent.Instructions, "actions/runs/999") {
+ t.Errorf("instructions should contain details_url fallback, got: %s", tk.Agent.Instructions)
+ }
+}
+
+const workflowRunNullBranchPayload = `{
+ "action": "completed",
+ "workflow_run": {
+ "name": "CI Pipeline",
+ "conclusion": "failure",
+ "html_url": "",
+ "head_sha": "def456abc",
+ "head_branch": null,
+ "pull_requests": [{"head": {"ref": "fix/something"}}]
+ },
+ "repository": {
+ "name": "myrepo",
+ "full_name": "owner/myrepo"
+ }
+}`
+
+func TestGitHubWebhook_WorkflowRunNullBranch_UsesPRRef(t *testing.T) {
+ srv, store := testServer(t)
+ srv.projects = []config.Project{{Name: "myrepo", Dir: "/workspace/myrepo"}}
+
+ w := webhookPost(t, srv, "workflow_run", workflowRunNullBranchPayload, "")
+
+ if w.Code != http.StatusOK {
+ t.Fatalf("want 200, got %d; body: %s", w.Code, w.Body.String())
+ }
+ var resp map[string]string
+ json.NewDecoder(w.Body).Decode(&resp)
+ tk, err := store.GetTask(resp["task_id"])
+ if err != nil {
+ t.Fatalf("task not found: %v", err)
+ }
+ if !strings.Contains(tk.Name, "fix/something") {
+ t.Errorf("task name %q should contain PR branch", tk.Name)
+ }
+}
+
func TestGitHubWebhook_UnknownEvent_Returns204(t *testing.T) {
srv, _ := testServer(t)