Source Control Provider Abstraction Architecture in Background Agents
The source control provider abstraction in Background Agents is a pluggable interface layer that isolates all SCM operations behind a unified factory pattern, supporting GitHub, GitLab, and Bitbucket through a single type-safe contract.
The ColeMurray/background-agents repository implements a robust control-plane package that decouples source-control operations from specific provider implementations. This architecture enables the system to interact with multiple Git hosting platforms through a consistent API surface while maintaining type safety and centralized error handling.
Core Architecture Components
The abstraction follows a classic "interface + factory + concrete implementations" pattern located in packages/control-plane/src/source-control/. This design ensures that consumer code remains provider-agnostic while supporting runtime selection via environment configuration.
Type Definitions and Contracts
All shared types and interfaces reside in packages/control-plane/src/source-control/types.ts. This file defines the uniform contract that every provider must satisfy, including:
RepositoryInfo: Metadata structure for repository dataSourceControlAuthContext: Authentication context interfaceGitPushSpec: Specification for git push operationsCreatePullRequestConfig: Configuration type for PR creationSourceControlProviderName: Union type defining supported providers ("github" | "bitbucket" | "gitlab")
These TypeScript definitions ensure consistent typing across the entire source control provider abstraction, preventing provider-specific data structures from leaking into consumer code.
Provider Interface
While the interface definition sits alongside the types file, concrete implementations must satisfy methods such as:
getRepository(): Fetch repository metadatacreatePullRequest(): Generate new pull requestsbuildGitPushSpec(): Construct git push specificationsgetCredentialHelperAuth(): Retrieve authentication credentialslistRepositories(): Enumerate accessible repositories
This interface acts as the runtime boundary that all concrete providers must implement.
Factory Pattern Implementation
The factory located in packages/control-plane/src/source-control/providers/index.ts handles provider instantiation. It reads the NEXT_PUBLIC_SCM_PROVIDER environment variable at runtime and returns the appropriate concrete class instance:
// Factory logic from providers/index.ts
switch (providerName) {
case "github":
return new GitHubProvider(config);
case "gitlab":
return new GitLabProvider(config);
case "bitbucket":
return new BitbucketProvider(config);
default:
throw new ProviderNotSupportedError(providerName);
}
The factory throws Unsupported source control provider for unknown provider names, ensuring fail-fast behavior on misconfiguration.
Concrete Provider Implementations
Each supported platform lives in its own module within the providers directory:
- GitHub:
packages/control-plane/src/source-control/providers/github-provider.tsimplements the interface using GitHub's REST and GraphQL APIs - GitLab:
packages/control-plane/src/source-control/providers/gitlab-provider.tsprovides the same contract against GitLab's API - Bitbucket:
packages/control-plane/src/source-control/providers/bitbucket-provider.tsfollows the identical pattern for Bitbucket Cloud
These classes encapsulate platform-specific API logic while exposing the standard interface to the rest of the system.
Error Handling Strategy
Centralized error types in packages/control-plane/src/source-control/errors.ts provide consistent exception handling across providers. Key error classes include:
SourceControlError: Base error for all SCM operationsProviderNotSupportedError: Thrown when the factory encounters an unregistered provider name
This centralization ensures that consumers can catch and handle source-control failures uniformly without importing provider-specific error types.
Consumer Usage Pattern
The control-plane accesses the abstraction lazily through session-based helpers. In packages/control-plane/src/session/durable-object.ts, the getSourceControlProvider() function creates providers on-demand and caches instances for the session duration.
Routes such as src/routes/repos.ts and src/routes/scm-credentials.ts demonstrate the consumer pattern:
import { getSourceControlProvider } from "./session/durable-object";
async function listRepos(sessionId: string) {
const provider = await getSourceControlProvider(sessionId);
const repos = await provider.listRepositories();
return repos;
}
This approach ensures that routes work against the generic interface without branching on provider names, maintaining clean separation of concerns.
Extensibility: Adding a New Provider
Extending the source control provider abstraction requires three steps:
-
Extend the union type in
types.ts:export type SourceControlProviderName = "github" | "gitlab" | "bitbucket" | "mynewprovider"; -
Create the implementation in a new file (e.g.,
mynewprovider.ts) satisfying the provider interface -
Register in the factory in
providers/index.ts:case "mynewprovider": return new MyNewProvider(config);
This architecture guarantees that adding a new SCM platform only requires changes to the provider layer, leaving the rest of the codebase untouched.
Summary
- Pluggable Architecture: The source control provider abstraction uses a factory pattern to instantiate GitHub, GitLab, or Bitbucket providers based on the
NEXT_PUBLIC_SCM_PROVIDERenvironment variable - Type Safety: Centralized type definitions in
types.tsensure consistent contracts across all providers - Lazy Initialization:
getSourceControlProvider()indurable-object.tscaches provider instances per session - Centralized Errors:
errors.tsdefines uniform exception handling for all SCM operations - Easy Extension: New providers require only interface implementation and factory registration without modifying consumer code
Frequently Asked Questions
How does the factory determine which provider to instantiate?
The factory reads the NEXT_PUBLIC_SCM_PROVIDER environment variable at build time or the runtime runtimeProvider variable, then matches this against the SourceControlProviderName union type. It returns an instance of GitHubProvider, GitLabProvider, or BitbucketProvider based on the configured value, throwing ProviderNotSupportedError for unknown names.
What methods must a new source control provider implement?
Any concrete provider must implement the interface defined alongside types.ts, including getRepository(), createPullRequest(), buildGitPushSpec(), getCredentialHelperAuth(), and listRepositories(). These methods ensure that consumers can perform all necessary SCM operations without knowing the underlying platform.
Where does error handling for SCM operations occur?
Error handling is centralized in packages/control-plane/src/source-control/errors.ts, which defines SourceControlError and ProviderNotSupportedError. This allows routes like repos.ts and scm-credentials.ts to catch and handle failures uniformly, regardless of which concrete provider generated the error.
Can consumer code access provider-specific details directly?
No. Consumer code in routes such as src/routes/repos.ts and src/routes/scm-credentials.ts accesses the abstraction through getSourceControlProvider(), which returns the generic interface. This design prevents provider-specific logic from leaking into the application layer and maintains strict separation between the control-plane and external SCM APIs.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →