summaryrefslogtreecommitdiff
path: root/internal/api/webhook.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-06-03 01:23:08 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-06-03 01:23:08 +0000
commit476a3136a9f55e151ae689cd098795ec865e7850 (patch)
treee89d08604a30e7eb9ee5251aa2c3b12d03df89b7 /internal/api/webhook.go
parent69208e618b0084ec18932120197f94bab98b713d (diff)
fix: clone from local mirror and sync from GitHub push events
- ContainerRunner now resolves local project path for non-story (CI) tasks, cloning from the local bare repo instead of the HTTPS GitHub URL to avoid auth failures on the host. - CI failure tasks now use SSH URLs (git@github.com:...) instead of HTTPS to avoid credential prompts even when no local path is found. - GitHub push webhook events now trigger an async git fetch into the local bare repo, keeping the local mirror in sync when work happens directly on GitHub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/api/webhook.go')
-rw-r--r--internal/api/webhook.go56
1 files changed, 55 insertions, 1 deletions
diff --git a/internal/api/webhook.go b/internal/api/webhook.go
index 3af4cc8..182a85e 100644
--- a/internal/api/webhook.go
+++ b/internal/api/webhook.go
@@ -10,6 +10,7 @@ import (
"io"
"log/slog"
"net/http"
+ "os/exec"
"path/filepath"
"strings"
"time"
@@ -115,6 +116,8 @@ func (s *Server) handleGitHubWebhook(w http.ResponseWriter, r *http.Request) {
s.handleCheckRunEvent(w, body)
case "workflow_run":
s.handleWorkflowRunEvent(w, body)
+ case "push":
+ s.handlePushEvent(w, body)
default:
w.WriteHeader(http.StatusNoContent)
}
@@ -230,7 +233,7 @@ func (s *Server) createCIFailureTask(w http.ResponseWriter, repoName, fullName,
State: task.StatePending,
CreatedAt: now,
UpdatedAt: now,
- RepositoryURL: fmt.Sprintf("https://github.com/%s.git", fullName),
+ RepositoryURL: fmt.Sprintf("git@github.com:%s.git", fullName),
}
if project != nil {
t.Project = project.Name
@@ -243,3 +246,54 @@ func (s *Server) createCIFailureTask(w http.ResponseWriter, repoName, fullName,
writeJSON(w, http.StatusOK, map[string]string{"task_id": t.ID})
}
+
+// pushPayload is a minimal GitHub push webhook payload.
+type pushPayload struct {
+ Ref string `json:"ref"`
+ Repository struct {
+ Name string `json:"name"`
+ FullName string `json:"full_name"`
+ } `json:"repository"`
+}
+
+// handlePushEvent syncs the local bare repo with GitHub when a push arrives.
+// The fetch runs asynchronously so the webhook returns immediately.
+func (s *Server) handlePushEvent(w http.ResponseWriter, body []byte) {
+ var p pushPayload
+ if err := json.Unmarshal(body, &p); err != nil || p.Repository.FullName == "" {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ project := matchProject(s.projects, p.Repository.Name)
+ if project == nil {
+ slog.Info("push event: no matching project, skipping sync", "repo", p.Repository.FullName)
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ dbProj, err := s.store.GetProject(project.Name)
+ if err != nil || dbProj == nil || dbProj.RemoteURL == "" {
+ slog.Info("push event: project has no local remote, skipping sync", "project", project.Name)
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+
+ githubURL := fmt.Sprintf("git@github.com:%s.git", p.Repository.FullName)
+ if s.githubURLForTest != nil {
+ githubURL = s.githubURLForTest(p.Repository.FullName)
+ }
+ bareRepo := dbProj.RemoteURL
+
+ go func() {
+ out, err := exec.Command("git", "-C", bareRepo, "fetch", githubURL,
+ "+refs/heads/*:refs/heads/*", "--prune").CombinedOutput()
+ if err != nil {
+ slog.Error("github push sync failed", "repo", p.Repository.FullName, "bare", bareRepo, "error", err, "output", string(out))
+ } else {
+ slog.Info("github push sync succeeded", "repo", p.Repository.FullName, "bare", bareRepo)
+ }
+ }()
+
+ w.WriteHeader(http.StatusNoContent)
+}