Refactor code structure and optimize performance across multiple modules
This commit is contained in:
448
docs/modules/advisory-ai/guides/scm-connector-plugins.md
Normal file
448
docs/modules/advisory-ai/guides/scm-connector-plugins.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 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<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
|
||||
|
||||
```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<IScmConnectorPlugin> 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<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
|
||||
|
||||
```csharp
|
||||
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:
|
||||
```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)
|
||||
Reference in New Issue
Block a user