feat: add GitLab support to coderd/externalauth/gitprovider

Fixes CODAGT-146

Add GitLab support to the gitprovider package for gitsync/chatd PR
diff flows. This is a squashed stack of 3 PRs:

#25651 - refactor(coderd/externalauth): prepare gitprovider for multi-provider support
- Change gitprovider.New to return (Provider, error)
- Extract shared helpers (parseRetryAfter, checkRateLimitError,
  countDiffLines, escapePathPreserveSlashes) from github.go
- Update all callers (db2sdk, exp_chats, gitsync) for new signature
- Add error logging for provider construction failures
- Thread context through provider resolution

#25652 - feat(coderd/externalauth/gitprovider): add GitLab provider
- Implement full Provider interface: FetchPullRequestStatus,
  FetchPullRequestDiff, FetchBranchDiff, ResolveBranchPullRequest
- Handle nested groups, forks, and self-hosted instances
- Rate limit detection on both library and raw HTTP paths
- URL parsing/building with NormalizePullRequestURL support
- Unit tests covering error paths, URL parsing, state mapping
- Document GitLab configuration and known limitations

#25653 - test(coderd/externalauth/gitprovider): add GitLab VCR integration tests
- FetchPullRequestStatus: 4 fixtures (open, conflicts, merged, closed)
- FetchPullRequestDiff: 4 fixtures
- FetchBranchDiff: 3 fixtures (open, deleted, fork)
- ResolveBranchPullRequest: 3 fixtures
- go-vcr cassettes with sanitized GitLab API responses
This commit is contained in:
Cian Johnston
2026-05-25 17:41:02 +01:00
committed by GitHub
parent 2ad2f7869d
commit 579daaff70
34 changed files with 4146 additions and 179 deletions
+2 -2
View File
@@ -30,7 +30,7 @@ const (
// ProviderResolver maps a git remote origin to the gitprovider
// that handles it. Returns nil if no provider matches.
type ProviderResolver func(origin string) gitprovider.Provider
type ProviderResolver func(ctx context.Context, origin string) gitprovider.Provider
var ErrNoTokenAvailable error = errors.New("no token available")
@@ -159,7 +159,7 @@ func (r *Refresher) Refresh(
// duplicate resolution for rows in the same group.
var resolved []resolvedGroup
for key, indices := range groups {
provider := r.providers(key.origin)
provider := r.providers(ctx, key.origin)
if provider == nil {
err := xerrors.Errorf("no provider for origin %q", key.origin)
for _, i := range indices {
+13 -13
View File
@@ -128,7 +128,7 @@ func TestRefresher_WithPRURL(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -184,7 +184,7 @@ func TestRefresher_BranchResolvesToPR(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -226,7 +226,7 @@ func TestRefresher_BranchNoPRYet(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -255,7 +255,7 @@ func TestRefresher_BranchNoPRYet(t *testing.T) {
func TestRefresher_NoProviderForOrigin(t *testing.T) {
t.Parallel()
providers := func(_ string) gitprovider.Provider { return nil }
providers := func(_ context.Context, _ string) gitprovider.Provider { return nil }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -296,7 +296,7 @@ func TestRefresher_TokenResolutionFails(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return nil, errors.New("token lookup failed")
}
@@ -328,7 +328,7 @@ func TestRefresher_EmptyToken(t *testing.T) {
mp := &mockProvider{}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref(""), nil
}
@@ -366,7 +366,7 @@ func TestRefresher_ProviderFetchFails(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -402,7 +402,7 @@ func TestRefresher_PRURLParseFailure(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -440,7 +440,7 @@ func TestRefresher_BatchGroupsByOwnerAndOrigin(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
var tokenCalls atomic.Int32
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
@@ -522,7 +522,7 @@ func TestRefresher_UsesInjectedClock(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -574,7 +574,7 @@ func TestRefresher_RateLimitSkipsRemainingInGroup(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
@@ -695,7 +695,7 @@ func TestRefresher_CorrectTokenPerOrigin(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
r := gitsync.NewRefresher(providers, tokens, slogtest.Make(t, nil), quartz.NewReal())
@@ -780,7 +780,7 @@ func TestRefresher_ConcurrentProcessing(t *testing.T) {
},
}
providers := func(_ string) gitprovider.Provider { return mp }
providers := func(_ context.Context, _ string) gitprovider.Provider { return mp }
tokens := func(_ context.Context, _ uuid.UUID, _ string) (*string, error) {
return ptr.Ref("test-token"), nil
}
+3 -3
View File
@@ -82,7 +82,7 @@ func newTestRefresher(t *testing.T, clk quartz.Clock, opts ...testRefresherOpt)
},
}
providers := func(string) gitprovider.Provider { return prov }
providers := func(context.Context, string) gitprovider.Provider { return prov }
tokens := func(context.Context, uuid.UUID, string) (*string, error) {
return ptr.Ref("tok"), nil
}
@@ -1120,7 +1120,7 @@ func TestRefreshChat_RefreshError(t *testing.T) {
// UpsertChatDiffStatus should NOT be called.
// Provider resolver returns nil → "no provider" error.
providers := func(string) gitprovider.Provider { return nil }
providers := func(context.Context, string) gitprovider.Provider { return nil }
tokens := func(context.Context, uuid.UUID, string) (*string, error) {
return ptr.Ref("tok"), nil
}
@@ -1205,7 +1205,7 @@ func TestWorker_NoTokenBackoff(t *testing.T) {
// Token resolver returns empty token → ErrNoTokenAvailable.
// Provider methods should never be called.
prov := &mockProvider{}
providers := func(string) gitprovider.Provider { return prov }
providers := func(context.Context, string) gitprovider.Provider { return prov }
tokens := func(context.Context, uuid.UUID, string) (*string, error) {
return ptr.Ref(""), nil
}