From 0b741bd25dd855c906b7ba51dff6b36d480d3f3b Mon Sep 17 00:00:00 2001 From: yg Date: Tue, 7 Jul 2026 08:16:07 +0900 Subject: [PATCH 1/2] fix(server): initialize store container before worker pool The server paniced on startup with 'could not find service ReviewJobStore' for every store driver. provideStores registers the individual store interfaces (ReviewJobStore, ReplyJobStore, ...) as a side effect and is lazy; cmd/server invokes worker.Pool first, which needs those interfaces before anything invokes *store.Stores, so the registrations never ran. Eagerly invoke *store.Stores after config validation, mirroring the CLI. --- cmd/server/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/server/main.go b/cmd/server/main.go index a78aa57..ac670e8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -22,6 +22,7 @@ import ( "github.com/antlss/gitlab-review-agent/internal/handler/webhook" "github.com/antlss/gitlab-review-agent/internal/handler/worker" "github.com/antlss/gitlab-review-agent/internal/pkg/queue" + "github.com/antlss/gitlab-review-agent/internal/pkg/store" ) func main() { @@ -41,6 +42,13 @@ func main() { } slog.Info("starting ai-review-agent server", "store_driver", cfg.Store.Driver) + // Eagerly initialize the store container. provideStores registers the + // individual store interfaces (ReviewJobStore, ReplyJobStore, ...) as a + // side effect, and it is lazy — nothing else invokes *store.Stores before + // the worker pool needs those interfaces, so without this the server panics + // with "could not find service ReviewJobStore". The CLI already does this. + _ = do.MustInvoke[*store.Stores](injector) + ctx, cancel := context.WithCancel(context.Background()) defer cancel() From a126f2d57ce80b5ee2d8f2951e78eb99662d9de9 Mon Sep 17 00:00:00 2001 From: yg Date: Tue, 7 Jul 2026 08:16:16 +0900 Subject: [PATCH 2/2] fix(git): authenticate clone/fetch with oauth2 URL, not PRIVATE-TOKEN header GitLab's git-over-HTTP smart protocol does not honor the 'PRIVATE-TOKEN' http.extraHeader (nor Authorization: Bearer) used for API calls, so the CLI clone got 401 and fell back to interactive username prompting, failing with 'could not read Username' in a non-interactive context. Build the clone URL with oauth2: basic-auth embedded (the method GitLab accepts for git transport) and set GIT_TERMINAL_PROMPT=0 so auth failures fail fast instead of hanging. The slow go-git fallback is no longer routinely hit. --- internal/pkg/git/manager.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/internal/pkg/git/manager.go b/internal/pkg/git/manager.go index 5fc5684..706d724 100644 --- a/internal/pkg/git/manager.go +++ b/internal/pkg/git/manager.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "net/url" "os" "os/exec" "path/filepath" @@ -95,7 +96,7 @@ func (m *Manager) ReleaseGitLock(_ context.Context, projectID int64) { // without forcing full working tree materialization for large repositories. func (m *Manager) FetchAndCheckout(ctx context.Context, projectID int64, projectPath string, mrIID int64, targetBranch, headSHA string) error { repoPath := m.RepoPath(projectID) - cloneURL := fmt.Sprintf("%s/%s.git", m.gitlabURL, projectPath) + cloneURL := m.authCloneURL(projectPath) cloned, err := m.ensureFullClone(ctx, repoPath, cloneURL) if err != nil { @@ -562,11 +563,34 @@ func (m *Manager) goGitAuth() *githttp.BasicAuth { } } +// authCloneURL builds the HTTPS clone URL with oauth2 basic-auth credentials +// embedded (https://oauth2:@host/path.git). GitLab's git-over-HTTP smart +// protocol does not honor the "PRIVATE-TOKEN" http.extraHeader used for API +// calls, so a bare URL makes the git CLI prompt for a username and fail in a +// non-interactive context. Embedding oauth2: is the auth method GitLab +// accepts for git transport. Falls back to the plain URL if the base can't be +// parsed or no token is set. +func (m *Manager) authCloneURL(projectPath string) string { + plain := fmt.Sprintf("%s/%s.git", m.gitlabURL, projectPath) + if m.gitlabToken == "" { + return plain + } + u, err := url.Parse(m.gitlabURL) + if err != nil || u.Host == "" { + return plain + } + u.User = url.UserPassword("oauth2", m.gitlabToken) + return fmt.Sprintf("%s/%s.git", u.String(), strings.TrimPrefix(projectPath, "/")) +} + // GitEnv returns environment variables for git commands that inject the GitLab // token, http buffer, and HTTP/1.1 settings via GIT_CONFIG environment variables. func (m *Manager) GitEnv() []string { + // GIT_TERMINAL_PROMPT=0: never prompt for credentials in this non-interactive + // context — fail fast instead of hanging when auth is missing/rejected. if m.gitlabToken == "" { return append(os.Environ(), + "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_COUNT=4", "GIT_CONFIG_KEY_0=http.postBuffer", "GIT_CONFIG_VALUE_0=524288000", @@ -579,6 +603,7 @@ func (m *Manager) GitEnv() []string { ) } return append(os.Environ(), + "GIT_TERMINAL_PROMPT=0", "GIT_CONFIG_COUNT=5", "GIT_CONFIG_KEY_0=http.extraHeader", fmt.Sprintf("GIT_CONFIG_VALUE_0=PRIVATE-TOKEN: %s", m.gitlabToken),