# SCM Connector Plugins > **Sprint:** SPRINT_20251226_016_AI_remedy_autopilot > **Tasks:** REMEDY-08 through REMEDY-14 This guide documents the SCM (Source Control Management) connector plugin architecture for automated remediation PR generation. ## Overview StellaOps supports automated Pull Request generation for remediation plans across multiple SCM platforms. The plugin architecture enables customer-premise integrations with: - **GitHub** (github.com and GitHub Enterprise Server) - **GitLab** (gitlab.com and self-hosted) - **Azure DevOps** (Services and Server) - **Gitea** (including Forgejo and Codeberg) ## Architecture ### Plugin Interface ```csharp public interface IScmConnectorPlugin { string ScmType { get; } // "github", "gitlab", "azuredevops", "gitea" string DisplayName { get; } // Human-readable name bool IsAvailable(ScmConnectorOptions options); // Check if configured bool CanHandle(string repositoryUrl); // Auto-detect from URL IScmConnector Create(ScmConnectorOptions options, HttpClient httpClient); } ``` ### Connector Interface ```csharp public interface IScmConnector { string ScmType { get; } // Branch operations Task CreateBranchAsync( string owner, string repo, string branchName, string baseBranch, ...); // File operations Task UpdateFileAsync( string owner, string repo, string branch, string filePath, string content, string commitMessage, ...); // Pull request operations Task CreatePullRequestAsync( string owner, string repo, string headBranch, string baseBranch, string title, string body, ...); Task GetPullRequestStatusAsync(...); Task UpdatePullRequestAsync(...); Task AddCommentAsync(...); Task ClosePullRequestAsync(...); // CI status Task GetCiStatusAsync( string owner, string repo, string commitSha, ...); } ``` ### Catalog and Factory ```csharp public sealed class ScmConnectorCatalog { // Get connector by explicit type IScmConnector? GetConnector(string scmType, ScmConnectorOptions options); // Auto-detect SCM type from repository URL IScmConnector? GetConnectorForRepository(string repositoryUrl, ScmConnectorOptions options); // List all available plugins IReadOnlyList Plugins { get; } } ``` ## Configuration ### Sample Configuration ```yaml scmConnectors: timeoutSeconds: 30 userAgent: "StellaOps.AdvisoryAI.Remediation/1.0" github: enabled: true baseUrl: "" # Default: https://api.github.com apiToken: "${GITHUB_PAT}" gitlab: enabled: true baseUrl: "" # Default: https://gitlab.com/api/v4 apiToken: "${GITLAB_PAT}" azuredevops: enabled: true baseUrl: "" # Default: https://dev.azure.com apiToken: "${AZURE_DEVOPS_PAT}" gitea: enabled: true baseUrl: "https://git.example.com" # Required apiToken: "${GITEA_TOKEN}" ``` ### Environment Variables | Variable | Description | |----------|-------------| | `STELLAOPS_SCM_GITHUB_TOKEN` | GitHub PAT or App token | | `STELLAOPS_SCM_GITLAB_TOKEN` | GitLab Personal/Project token | | `STELLAOPS_SCM_AZUREDEVOPS_TOKEN` | Azure DevOps PAT | | `STELLAOPS_SCM_GITEA_TOKEN` | Gitea application token | ### Required Token Scopes | Platform | Required Scopes | |----------|-----------------| | **GitHub** | `repo`, `workflow` (PAT) or `contents:write`, `pull_requests:write`, `checks:read` (App) | | **GitLab** | `api`, `read_repository`, `write_repository` | | **Azure DevOps** | Code (Read & Write), Pull Request Contribute, Build (Read) | | **Gitea** | `repo` (full repository access) | ## Connector Details ### GitHub Connector ```yaml github: enabled: true baseUrl: "" # Leave empty for github.com apiToken: "${GITHUB_PAT}" ``` **Features:** - Bearer token authentication - Check-runs API for CI status (GitHub Actions) - Combined commit status support - Enterprise Server support via `baseUrl` **API Endpoints Used:** - `GET /repos/{owner}/{repo}/git/refs/heads/{branch}` - Get branch SHA - `POST /repos/{owner}/{repo}/git/refs` - Create branch - `PUT /repos/{owner}/{repo}/contents/{path}` - Update file - `POST /repos/{owner}/{repo}/pulls` - Create PR - `GET /repos/{owner}/{repo}/commits/{sha}/check-runs` - CI status ### GitLab Connector ```yaml gitlab: enabled: true baseUrl: "" # Leave empty for gitlab.com apiToken: "${GITLAB_PAT}" ``` **Features:** - PRIVATE-TOKEN header authentication - Merge Request creation (GitLab terminology) - Pipeline and Jobs API for CI status - Self-hosted instance support **API Endpoints Used:** - `POST /projects/{id}/repository/branches` - Create branch - `POST /projects/{id}/repository/commits` - Commit file changes - `POST /projects/{id}/merge_requests` - Create MR - `GET /projects/{id}/pipelines?sha={sha}` - CI status - `GET /projects/{id}/pipelines/{id}/jobs` - Job details ### Azure DevOps Connector ```yaml azuredevops: enabled: true baseUrl: "" # Leave empty for Azure DevOps Services apiToken: "${AZURE_DEVOPS_PAT}" apiVersion: "7.1" ``` **Features:** - Basic authentication with PAT (empty username, token as password) - Push API for atomic commits - Azure Pipelines build status - Azure DevOps Server support **API Endpoints Used:** - `GET /{org}/{project}/_apis/git/refs` - Get branch refs - `POST /{org}/{project}/_apis/git/refs` - Create branch - `POST /{org}/{project}/_apis/git/pushes` - Commit changes - `POST /{org}/{project}/_apis/git/pullrequests` - Create PR - `GET /{org}/{project}/_apis/build/builds` - Build status ### Gitea Connector ```yaml gitea: enabled: true baseUrl: "https://git.example.com" # Required apiToken: "${GITEA_TOKEN}" ``` **Features:** - Token header authentication - Gitea Actions support (workflow runs) - Compatible with Forgejo and Codeberg - Combined commit status API **API Endpoints Used:** - `GET /api/v1/repos/{owner}/{repo}/branches/{branch}` - Get branch - `POST /api/v1/repos/{owner}/{repo}/branches` - Create branch - `PUT /api/v1/repos/{owner}/{repo}/contents/{path}` - Update file - `POST /api/v1/repos/{owner}/{repo}/pulls` - Create PR - `GET /api/v1/repos/{owner}/{repo}/commits/{sha}/status` - Status - `GET /api/v1/repos/{owner}/{repo}/actions/runs` - Workflow runs ## Usage ### Dependency Injection ```csharp // In Startup.cs or Program.cs services.AddScmConnectors(config => { // Optionally add custom plugins config.AddPlugin(new CustomScmConnectorPlugin()); // Or remove built-in plugins config.RemovePlugin("github"); }); ``` ### Creating a Connector ```csharp public class RemediationService { private readonly ScmConnectorCatalog _catalog; public async Task CreateRemediationPrAsync( string repositoryUrl, RemediationPlan plan, CancellationToken cancellationToken) { var options = new ScmConnectorOptions { ApiToken = _configuration["ScmToken"], BaseUrl = _configuration["ScmBaseUrl"] }; // Auto-detect connector from URL var connector = _catalog.GetConnectorForRepository(repositoryUrl, options); if (connector is null) throw new InvalidOperationException($"No connector available for {repositoryUrl}"); // Create branch var branchResult = await connector.CreateBranchAsync( owner: "myorg", repo: "myrepo", branchName: $"stellaops/remediation/{plan.Id}", baseBranch: "main", cancellationToken); // Update files foreach (var change in plan.FileChanges) { await connector.UpdateFileAsync( owner: "myorg", repo: "myrepo", branch: branchResult.BranchName, filePath: change.Path, content: change.NewContent, commitMessage: $"chore: apply remediation for {plan.FindingId}", cancellationToken); } // Create PR return await connector.CreatePullRequestAsync( owner: "myorg", repo: "myrepo", headBranch: branchResult.BranchName, baseBranch: "main", title: $"[StellaOps] Remediation for {plan.FindingId}", body: GeneratePrBody(plan), cancellationToken); } } ``` ### Polling CI Status ```csharp public async Task WaitForCiAsync( IScmConnector connector, string owner, string repo, string commitSha, TimeSpan timeout, CancellationToken cancellationToken) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { var status = await connector.GetCiStatusAsync( owner, repo, commitSha, cancellationToken); switch (status.OverallState) { case CiState.Success: case CiState.Failure: case CiState.Error: return status.OverallState; case CiState.Pending: case CiState.Running: await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); break; } } return CiState.Unknown; } ``` ## CI State Mapping Different SCM platforms use different status values. The connector normalizes them: | Platform | Pending | Running | Success | Failure | Error | |----------|---------|---------|---------|---------|-------| | **GitHub** | `pending`, `queued` | `in_progress` | `success` | `failure` | `error`, `cancelled` | | **GitLab** | `pending`, `waiting` | `running` | `success` | `failed` | `canceled`, `skipped` | | **Azure DevOps** | `notStarted`, `postponed` | `inProgress` | `succeeded` | `failed` | `canceled` | | **Gitea** | `pending`, `queued` | `running` | `success` | `failure` | `cancelled`, `timed_out` | ## URL Auto-Detection The `CanHandle` method on each plugin detects repository URLs: | Plugin | URL Patterns | |--------|--------------| | **GitHub** | `github.com`, `github.` | | **GitLab** | `gitlab.com`, `gitlab.` | | **Azure DevOps** | `dev.azure.com`, `visualstudio.com`, `azure.com` | | **Gitea** | `gitea.`, `forgejo.`, `codeberg.org` | Example: ```csharp // Auto-detects GitHub var connector = catalog.GetConnectorForRepository( "https://github.com/myorg/myrepo", options); // Auto-detects GitLab var connector = catalog.GetConnectorForRepository( "https://gitlab.com/mygroup/myproject", options); ``` ## Custom Plugins To add support for a new SCM platform: ```csharp public sealed class BitbucketScmConnectorPlugin : IScmConnectorPlugin { public string ScmType => "bitbucket"; public string DisplayName => "Bitbucket"; public bool IsAvailable(ScmConnectorOptions options) => !string.IsNullOrEmpty(options.ApiToken); public bool CanHandle(string repositoryUrl) => repositoryUrl.Contains("bitbucket.org", StringComparison.OrdinalIgnoreCase); public IScmConnector Create(ScmConnectorOptions options, HttpClient httpClient) => new BitbucketScmConnector(httpClient, options); } public sealed class BitbucketScmConnector : ScmConnectorBase { // Implement abstract methods... } ``` Register the custom plugin: ```csharp services.AddScmConnectors(config => { config.AddPlugin(new BitbucketScmConnectorPlugin()); }); ``` ## Error Handling All connector methods return result objects with `Success` and `ErrorMessage`: ```csharp var result = await connector.CreateBranchAsync(...); if (!result.Success) { _logger.LogError("Failed to create branch: {Error}", result.ErrorMessage); return; } // Continue with successful result var branchSha = result.CommitSha; ``` ## Security Considerations 1. **Token Storage**: Never store tokens in configuration files. Use environment variables or secret management. 2. **Minimum Permissions**: Request only required scopes for each platform. 3. **TLS Verification**: Always verify TLS certificates in production (`verifySsl: true`). 4. **Audit Logging**: All SCM operations are logged for compliance. 5. **Repository Access**: Connectors only access repositories explicitly provided. No enumeration of accessible repos. ## Telemetry SCM operations emit structured logs: ```json { "timestamp": "2025-12-26T10:30:00Z", "operation": "scm_create_pr", "scmType": "github", "owner": "myorg", "repo": "myrepo", "branch": "stellaops/remediation/plan-123", "duration_ms": 1234, "success": true, "pr_number": 456, "pr_url": "https://github.com/myorg/myrepo/pull/456" } ``` ## Related Documentation - [Remediation API](../remediation-api.md) - [AI Attestations](./ai-attestations.md) - [Offline Model Bundles](./offline-model-bundles.md) - [Configuration Reference](../../../../etc/scm-connectors.yaml.sample)