13 KiB
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
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
public interface IScmConnector
{
string ScmType { get; }
// Branch operations
Task<BranchResult> CreateBranchAsync(
string owner, string repo, string branchName, string baseBranch, ...);
// File operations
Task<FileUpdateResult> UpdateFileAsync(
string owner, string repo, string branch, string filePath,
string content, string commitMessage, ...);
// Pull request operations
Task<PrCreateResult> CreatePullRequestAsync(
string owner, string repo, string headBranch, string baseBranch,
string title, string body, ...);
Task<PrStatusResult> GetPullRequestStatusAsync(...);
Task<bool> UpdatePullRequestAsync(...);
Task<bool> AddCommentAsync(...);
Task<bool> ClosePullRequestAsync(...);
// CI status
Task<CiStatusResult> GetCiStatusAsync(
string owner, string repo, string commitSha, ...);
}
Catalog and Factory
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<IScmConnectorPlugin> Plugins { get; }
}
Configuration
Sample Configuration
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
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 SHAPOST /repos/{owner}/{repo}/git/refs- Create branchPUT /repos/{owner}/{repo}/contents/{path}- Update filePOST /repos/{owner}/{repo}/pulls- Create PRGET /repos/{owner}/{repo}/commits/{sha}/check-runs- CI status
GitLab Connector
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 branchPOST /projects/{id}/repository/commits- Commit file changesPOST /projects/{id}/merge_requests- Create MRGET /projects/{id}/pipelines?sha={sha}- CI statusGET /projects/{id}/pipelines/{id}/jobs- Job details
Azure DevOps Connector
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 refsPOST /{org}/{project}/_apis/git/refs- Create branchPOST /{org}/{project}/_apis/git/pushes- Commit changesPOST /{org}/{project}/_apis/git/pullrequests- Create PRGET /{org}/{project}/_apis/build/builds- Build status
Gitea Connector
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 branchPOST /api/v1/repos/{owner}/{repo}/branches- Create branchPUT /api/v1/repos/{owner}/{repo}/contents/{path}- Update filePOST /api/v1/repos/{owner}/{repo}/pulls- Create PRGET /api/v1/repos/{owner}/{repo}/commits/{sha}/status- StatusGET /api/v1/repos/{owner}/{repo}/actions/runs- Workflow runs
Usage
Dependency Injection
// 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
public class RemediationService
{
private readonly ScmConnectorCatalog _catalog;
public async Task<PrCreateResult> 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
public async Task<CiState> 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:
// 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:
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:
services.AddScmConnectors(config =>
{
config.AddPlugin(new BitbucketScmConnectorPlugin());
});
Error Handling
All connector methods return result objects with Success and ErrorMessage:
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
-
Token Storage: Never store tokens in configuration files. Use environment variables or secret management.
-
Minimum Permissions: Request only required scopes for each platform.
-
TLS Verification: Always verify TLS certificates in production (
verifySsl: true). -
Audit Logging: All SCM operations are logged for compliance.
-
Repository Access: Connectors only access repositories explicitly provided. No enumeration of accessible repos.
Telemetry
SCM operations emit structured logs:
{
"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"
}