diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-06-03 01:23:08 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-06-03 01:23:08 +0000 |
| commit | 476a3136a9f55e151ae689cd098795ec865e7850 (patch) | |
| tree | e89d08604a30e7eb9ee5251aa2c3b12d03df89b7 /internal/api/webhook.go | |
| parent | 69208e618b0084ec18932120197f94bab98b713d (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.go | 56 |
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) +} |
