SCM Connector Plugins

When Advisory AI produces a remediation plan, it can open the fix as a pull (or merge) request directly in your source control system. SCM connector plugins are the adapters that make this work across hosting platforms while normalizing their differing APIs and CI status models behind one interface.

Audience: operators configuring remediation PR delivery for a tenant, and developers adding a connector for a new SCM platform.

Overview

Stella Ops supports automated pull-request generation for remediation plans across multiple SCM platforms. The plugin architecture enables customer-premise integrations with:

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

VariableDescription
STELLAOPS_SCM_GITHUB_TOKENGitHub PAT or App token
STELLAOPS_SCM_GITLAB_TOKENGitLab Personal/Project token
STELLAOPS_SCM_AZUREDEVOPS_TOKENAzure DevOps PAT
STELLAOPS_SCM_GITEA_TOKENGitea application token

Required Token Scopes

PlatformRequired Scopes
GitHubrepo, workflow (PAT) or contents:write, pull_requests:write, checks:read (App)
GitLabapi, read_repository, write_repository
Azure DevOpsCode (Read & Write), Pull Request Contribute, Build (Read)
Gitearepo (full repository access)

Connector Details

GitHub Connector

github:
  enabled: true
  baseUrl: ""  # Leave empty for github.com
  apiToken: "${GITHUB_PAT}"

Features:

API Endpoints Used:

GitLab Connector

gitlab:
  enabled: true
  baseUrl: ""  # Leave empty for gitlab.com
  apiToken: "${GITLAB_PAT}"

Features:

API Endpoints Used:

Azure DevOps Connector

azuredevops:
  enabled: true
  baseUrl: ""  # Leave empty for Azure DevOps Services
  apiToken: "${AZURE_DEVOPS_PAT}"
  apiVersion: "7.1"

Features:

API Endpoints Used:

Gitea Connector

gitea:
  enabled: true
  baseUrl: "https://git.example.com"  # Required
  apiToken: "${GITEA_TOKEN}"

Features:

API Endpoints Used:

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:

PlatformPendingRunningSuccessFailureError
GitHubpending, queuedin_progresssuccessfailureerror, cancelled
GitLabpending, waitingrunningsuccessfailedcanceled, skipped
Azure DevOpsnotStarted, postponedinProgresssucceededfailedcanceled
Giteapending, queuedrunningsuccessfailurecancelled, timed_out

URL Auto-Detection

The CanHandle method on each plugin detects repository URLs:

PluginURL Patterns
GitHubgithub.com, github.
GitLabgitlab.com, gitlab.
Azure DevOpsdev.azure.com, visualstudio.com, azure.com
Giteagitea., 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

  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:

{
  "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"
}

The sample connector configuration ships as etc/scm-connectors.yaml.sample in the source tree.