From e744ebab1fc4863276ed5c141da71fd4b3c421e0 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Sat, 4 Apr 2026 11:53:50 +0100 Subject: [PATCH] Time out stalled GitHub clones after ten minutes Progress streams now stay open until a job ends, so a hung clone would otherwise hold its job and stream forever. --- github.go | 5 +++-- github_test.go | 2 +- main.go | 9 ++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/github.go b/github.go index 4ede6b6..c06f820 100644 --- a/github.go +++ b/github.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/rand" "crypto/subtle" "encoding/base64" @@ -178,8 +179,8 @@ func (s *Server) githubAuthFor(sid string) githubAuth { // gitClone builds the clone command. The token travels in environment-provided git config, // never in the URL or arguments, so it cannot leak through process lists or error output. -func gitClone(repoURL, dest, token string) *exec.Cmd { - cmd := exec.Command("git", "clone", "--depth=1", "--single-branch", "--quiet", "--", repoURL, dest) +func gitClone(ctx context.Context, repoURL, dest, token string) *exec.Cmd { + cmd := exec.CommandContext(ctx, "git", "clone", "--depth=1", "--single-branch", "--quiet", "--", repoURL, dest) // Ignore the host's git config and credential helpers: only the visitor's own token may // ever authenticate a clone. cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", diff --git a/github_test.go b/github_test.go index ca9142b..1fbf1d8 100644 --- a/github_test.go +++ b/github_test.go @@ -135,7 +135,7 @@ func TestGitCloneKeepsTokenOutOfArgsAndIgnoresHostCredentials(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git not installed") } - cmd := gitClone("https://github.com/o/r", t.TempDir(), "ghu_secret") + cmd := gitClone(t.Context(), "https://github.com/o/r", t.TempDir(), "ghu_secret") if strings.Contains(strings.Join(cmd.Args, " "), "ghu_secret") { t.Fatal("token appears in git arguments") } diff --git a/main.go b/main.go index c6cb7e2..f7e95d8 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "crypto/rand" "embed" "encoding/json" @@ -49,6 +50,7 @@ const ( idleExpiry = time.Hour // snapshots unused and jobs untouched for this long are dropped maxSnapshots = 8 // ponytail: fixed caps sized for the 2GB container; make them configurable if memory changes maxIndexing = 2 // concurrent index runs; each already uses every CPU + cloneTimeout = 10 * time.Minute ) var sseKeepAlive = 15 * time.Second // variable so tests can shorten it @@ -186,9 +188,14 @@ func (s *Server) createJob(w http.ResponseWriter, r *http.Request) { token := s.githubAuthFor(owner).token go func() { checkout := filepath.Join(root, "repo") - output, err := gitClone(url, checkout, token).CombinedOutput() + ctx, cancel := context.WithTimeout(context.Background(), cloneTimeout) + output, err := gitClone(ctx, url, checkout, token).CombinedOutput() + cancel() if err != nil { text := string(output) + if ctx.Err() != nil { + text = "timed out after " + cloneTimeout.String() + } if token != "" { text = strings.ReplaceAll(text, token, "***") }