summaryrefslogtreecommitdiff
path: root/internal/api/webhook_test.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_test.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_test.go')
-rw-r--r--internal/api/webhook_test.go78
1 files changed, 74 insertions, 4 deletions
diff --git a/internal/api/webhook_test.go b/internal/api/webhook_test.go
index 967b62b..05fe82c 100644
--- a/internal/api/webhook_test.go
+++ b/internal/api/webhook_test.go
@@ -8,10 +8,14 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
+ "os/exec"
+ "path/filepath"
"strings"
"testing"
+ "time"
"github.com/thepeterstone/claudomator/internal/config"
+ "github.com/thepeterstone/claudomator/internal/task"
)
// signBody computes the HMAC-SHA256 signature for a webhook payload.
@@ -124,8 +128,8 @@ func TestGitHubWebhook_CheckRunFailure_CreatesTask(t *testing.T) {
if !strings.Contains(tk.Name, "main") {
t.Errorf("task name %q does not contain branch", tk.Name)
}
- if tk.RepositoryURL != "https://github.com/owner/myrepo.git" {
- t.Errorf("task repository url = %q, want https://github.com/owner/myrepo.git", tk.RepositoryURL)
+ if tk.RepositoryURL != "git@github.com:owner/myrepo.git" {
+ t.Errorf("task repository url = %q, want git@github.com:owner/myrepo.git", tk.RepositoryURL)
}
if !contains(tk.Tags, "ci") || !contains(tk.Tags, "auto") {
t.Errorf("task tags %v missing expected ci/auto tags", tk.Tags)
@@ -375,8 +379,8 @@ func TestGitHubWebhook_FallbackToSingleProject(t *testing.T) {
if err != nil {
t.Fatalf("task not found: %v", err)
}
- if tk.RepositoryURL != "https://github.com/owner/myrepo.git" {
- t.Errorf("expected fallback repository url, got %q", tk.RepositoryURL)
+ if tk.RepositoryURL != "git@github.com:owner/myrepo.git" {
+ t.Errorf("expected fallback repository url (ssh), got %q", tk.RepositoryURL)
}
}
@@ -400,6 +404,72 @@ func TestGitHubWebhook_NoProjectsConfigured_CreatesTaskWithGitHubURL(t *testing.
}
}
+func TestGitHubWebhook_PushEvent_Returns204(t *testing.T) {
+ srv, _ := testServer(t)
+ srv.projects = []config.Project{{Name: "myrepo", Dir: "/workspace/myrepo"}}
+
+ payload := `{"ref":"refs/heads/main","repository":{"name":"myrepo","full_name":"owner/myrepo"}}`
+ w := webhookPost(t, srv, "push", payload, "")
+
+ if w.Code != http.StatusNoContent {
+ t.Fatalf("want 204, got %d; body: %s", w.Code, w.Body.String())
+ }
+}
+
+func TestGitHubWebhook_PushEvent_SyncsLocalBareRepo(t *testing.T) {
+ dir := t.TempDir()
+
+ // Create a "github" source bare repo with a commit.
+ githubBare := filepath.Join(dir, "github.git")
+ localBare := filepath.Join(dir, "local.git")
+ for _, bare := range []string{githubBare, localBare} {
+ if out, err := exec.Command("git", "init", "--bare", bare).CombinedOutput(); err != nil {
+ t.Fatalf("git init bare %s: %v\n%s", bare, err, out)
+ }
+ }
+ // Seed githubBare with a commit via a temp clone.
+ clone := filepath.Join(dir, "clone")
+ run := func(args ...string) {
+ t.Helper()
+ if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil {
+ t.Fatalf("%v: %v\n%s", args, err, out)
+ }
+ }
+ run("git", "clone", githubBare, clone)
+ run("git", "-C", clone, "config", "user.email", "test@test.com")
+ run("git", "-C", clone, "config", "user.name", "Test")
+ run("git", "-C", clone, "commit", "--allow-empty", "-m", "initial")
+ run("git", "-C", clone, "push", "origin", "main")
+
+ // Register DB project with localBare as the remote_url.
+ srv, store := testServer(t)
+ proj := &task.Project{ID: "myrepo", Name: "myrepo", RemoteURL: localBare, LocalPath: clone}
+ if err := store.CreateProject(proj); err != nil {
+ t.Fatalf("CreateProject: %v", err)
+ }
+ srv.projects = []config.Project{{Name: "myrepo", Dir: clone}}
+
+ // Override the github SSH URL to point to our local githubBare for testing.
+ srv.githubURLForTest = func(fullName string) string { return githubBare }
+
+ payload := `{"ref":"refs/heads/main","repository":{"name":"myrepo","full_name":"owner/myrepo"}}`
+ w := webhookPost(t, srv, "push", payload, "")
+ if w.Code != http.StatusNoContent {
+ t.Fatalf("want 204, got %d", w.Code)
+ }
+
+ // Wait for the async sync goroutine.
+ deadline := time.Now().Add(5 * time.Second)
+ for time.Now().Before(deadline) {
+ out, err := exec.Command("git", "-C", localBare, "log", "--oneline", "-1").CombinedOutput()
+ if err == nil && strings.TrimSpace(string(out)) != "" {
+ return // success — local bare has the commit
+ }
+ time.Sleep(50 * time.Millisecond)
+ }
+ t.Error("local bare repo was not synced from github within timeout")
+}
+
// contains checks if a string slice contains a value.
func contains(slice []string, val string) bool {
for _, s := range slice {