old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions

This commit is contained in:
master
2026-01-15 18:37:59 +02:00
parent c631bacee2
commit 88a85cdd92
208 changed files with 32271 additions and 2287 deletions

View File

@@ -1,26 +1,33 @@
using System.Globalization;
using StellaOps.AdvisoryAI.Remediation.ScmConnector;
namespace StellaOps.AdvisoryAI.Remediation;
/// <summary>
/// GitHub implementation of pull request generator.
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot
/// Task: REMEDY-09
/// Sprint: SPRINT_20251226_016_AI_remedy_autopilot (REMEDY-09)
/// Updated: SPRINT_20260112_007_BE_remediation_pr_generator (REMEDY-BE-002)
/// </summary>
public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
{
private readonly IRemediationPlanStore _planStore;
private readonly IScmConnector? _scmConnector;
private readonly PrTemplateBuilder _templateBuilder;
private readonly TimeProvider _timeProvider;
private readonly Func<Guid> _guidFactory;
private readonly Func<int, int, int> _randomFactory;
public GitHubPullRequestGenerator(
IRemediationPlanStore planStore,
IScmConnector? scmConnector = null,
PrTemplateBuilder? templateBuilder = null,
TimeProvider? timeProvider = null,
Func<Guid>? guidFactory = null,
Func<int, int, int>? randomFactory = null)
{
_planStore = planStore;
_scmConnector = scmConnector;
_templateBuilder = templateBuilder ?? new PrTemplateBuilder();
_timeProvider = timeProvider ?? TimeProvider.System;
_guidFactory = guidFactory ?? Guid.NewGuid;
_randomFactory = randomFactory ?? Random.Shared.Next;
@@ -33,6 +40,7 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
CancellationToken cancellationToken = default)
{
var nowStr = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
// Validate plan is PR-ready
if (!plan.PrReady)
{
@@ -49,89 +57,254 @@ public sealed class GitHubPullRequestGenerator : IPullRequestGenerator
};
}
// Generate branch name
var branchName = GenerateBranchName(plan);
// Generate branch name and PR content using the template builder
var branchName = _templateBuilder.BuildBranchName(plan);
var prTitle = _templateBuilder.BuildPrTitle(plan);
var prBody = _templateBuilder.BuildPrBody(plan);
// In a real implementation, this would:
// 1. Create a new branch
// 2. Apply remediation steps (update files)
// 3. Commit changes
// 4. Create PR via GitHub API
// Extract owner/repo from URL
var (owner, repo) = ExtractOwnerRepo(plan.Request.RepositoryUrl);
var baseBranch = plan.Request.TargetBranch;
var prId = $"gh-pr-{_guidFactory():N}";
return new PullRequestResult
// If no SCM connector configured, return placeholder result
if (_scmConnector is null)
{
PrId = prId,
PrNumber = _randomFactory(1000, 9999), // Placeholder
Url = $"https://github.com/{ExtractOwnerRepo(plan.Request.RepositoryUrl)}/pull/{prId}",
BranchName = branchName,
Status = PullRequestStatus.Creating,
StatusMessage = "Pull request is being created",
CreatedAt = nowStr,
UpdatedAt = nowStr
};
var prId = $"gh-pr-{_guidFactory():N}";
return new PullRequestResult
{
PrId = prId,
PrNumber = 0,
Url = string.Empty,
BranchName = branchName,
Status = PullRequestStatus.Failed,
StatusMessage = "SCM connector not configured",
PrBody = prBody,
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
try
{
// Step 1: Create branch
var branchResult = await _scmConnector.CreateBranchAsync(
owner, repo, branchName, baseBranch, cancellationToken);
if (!branchResult.Success)
{
return new PullRequestResult
{
PrId = $"pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = branchName,
Status = PullRequestStatus.Failed,
StatusMessage = branchResult.ErrorMessage ?? "Failed to create branch",
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
// Step 2: Apply remediation steps (update files)
foreach (var step in plan.Steps.OrderBy(s => s.Order))
{
if (string.IsNullOrEmpty(step.FilePath) || string.IsNullOrEmpty(step.NewValue))
continue;
var commitMessage = $"fix({plan.Request.VulnerabilityId}): {step.Description}";
var fileResult = await _scmConnector.UpdateFileAsync(
owner, repo, branchName, step.FilePath, step.NewValue, commitMessage, cancellationToken);
if (!fileResult.Success)
{
return new PullRequestResult
{
PrId = $"pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = branchName,
Status = PullRequestStatus.Failed,
StatusMessage = $"Failed to update file {step.FilePath}: {fileResult.ErrorMessage}",
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
}
// Step 3: Create pull request
var prResult = await _scmConnector.CreatePullRequestAsync(
owner, repo, branchName, baseBranch, prTitle, prBody, cancellationToken);
if (!prResult.Success)
{
return new PullRequestResult
{
PrId = $"pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = branchName,
Status = PullRequestStatus.Failed,
StatusMessage = prResult.ErrorMessage ?? "Failed to create PR",
PrBody = prBody,
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
// Success
var prId = $"gh-pr-{prResult.PrNumber}";
return new PullRequestResult
{
PrId = prId,
PrNumber = prResult.PrNumber,
Url = prResult.PrUrl ?? $"https://github.com/{owner}/{repo}/pull/{prResult.PrNumber}",
BranchName = branchName,
Status = PullRequestStatus.Open,
StatusMessage = "Pull request created successfully",
PrBody = prBody,
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
catch (Exception ex)
{
return new PullRequestResult
{
PrId = $"pr-{_guidFactory():N}",
PrNumber = 0,
Url = string.Empty,
BranchName = branchName,
Status = PullRequestStatus.Failed,
StatusMessage = $"Unexpected error: {ex.Message}",
CreatedAt = nowStr,
UpdatedAt = nowStr
};
}
}
public Task<PullRequestResult> GetStatusAsync(
public async Task<PullRequestResult> GetStatusAsync(
string prId,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would query GitHub API
var now = _timeProvider.GetUtcNow().ToString("O", CultureInfo.InvariantCulture);
return Task.FromResult(new PullRequestResult
// Extract PR number from prId (format: gh-pr-1234)
if (!int.TryParse(prId.Replace("gh-pr-", ""), out var prNumber))
{
return new PullRequestResult
{
PrId = prId,
PrNumber = 0,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Failed,
StatusMessage = $"Invalid PR ID format: {prId}",
CreatedAt = now,
UpdatedAt = now
};
}
if (_scmConnector is null)
{
return new PullRequestResult
{
PrId = prId,
PrNumber = prNumber,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Open,
StatusMessage = "Status check not available (no SCM connector)",
CreatedAt = now,
UpdatedAt = now
};
}
// Note: We would need the owner/repo from context to make the actual API call
// For now, return a placeholder
return new PullRequestResult
{
PrId = prId,
PrNumber = 0,
PrNumber = prNumber,
Url = string.Empty,
BranchName = string.Empty,
Status = PullRequestStatus.Open,
StatusMessage = "Waiting for CI",
CreatedAt = now,
UpdatedAt = now
});
};
}
public Task UpdateWithDeltaVerdictAsync(
public async Task UpdateWithDeltaVerdictAsync(
string prId,
DeltaVerdictResult deltaVerdict,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would update PR description via GitHub API
return Task.CompletedTask;
if (_scmConnector is null)
return;
// Extract PR number from prId
if (!int.TryParse(prId.Replace("gh-pr-", ""), out var prNumber))
return;
// Build a comment with the delta verdict
var comment = BuildDeltaVerdictComment(deltaVerdict);
// Note: We would need owner/repo from context. Storing for later enhancement.
// For now, this is a placeholder for when context is available.
await Task.CompletedTask;
}
public Task ClosePullRequestAsync(
public async Task ClosePullRequestAsync(
string prId,
string reason,
CancellationToken cancellationToken = default)
{
// In a real implementation, this would close PR via GitHub API
return Task.CompletedTask;
if (_scmConnector is null)
return;
// Extract PR number from prId
if (!int.TryParse(prId.Replace("gh-pr-", ""), out var prNumber))
return;
// Note: We would need owner/repo from context to close the PR
await Task.CompletedTask;
}
private string GenerateBranchName(RemediationPlan plan)
private static string BuildDeltaVerdictComment(DeltaVerdictResult verdict)
{
var vulnId = plan.Request.VulnerabilityId.Replace(":", "-").ToLowerInvariant();
var timestamp = _timeProvider.GetUtcNow().ToString("yyyyMMdd", CultureInfo.InvariantCulture);
return $"stellaops/fix-{vulnId}-{timestamp}";
var lines = new System.Text.StringBuilder();
lines.AppendLine("## StellaOps Delta Verdict");
lines.AppendLine();
lines.AppendLine($"**Improved:** {verdict.Improved}");
lines.AppendLine($"**Vulnerabilities Fixed:** {verdict.VulnerabilitiesFixed}");
lines.AppendLine($"**Vulnerabilities Introduced:** {verdict.VulnerabilitiesIntroduced}");
lines.AppendLine($"**Verdict ID:** {verdict.VerdictId}");
lines.AppendLine($"**Computed At:** {verdict.ComputedAt}");
return lines.ToString();
}
private static string ExtractOwnerRepo(string? repositoryUrl)
private static (string owner, string repo) ExtractOwnerRepo(string? repositoryUrl)
{
if (string.IsNullOrEmpty(repositoryUrl))
{
return "owner/repo";
return ("owner", "repo");
}
// Extract owner/repo from GitHub URL
var uri = new Uri(repositoryUrl);
var path = uri.AbsolutePath.Trim('/');
if (path.EndsWith(".git"))
if (path.EndsWith(".git", StringComparison.OrdinalIgnoreCase))
{
path = path[..^4];
}
return path;
var parts = path.Split('/');
if (parts.Length >= 2)
{
return (parts[0], parts[1]);
}
return ("owner", "repo");
}
}

View File

@@ -96,6 +96,12 @@ public sealed record PullRequestResult
/// </summary>
public string? StatusMessage { get; init; }
/// <summary>
/// PR body/description content.
/// Sprint: SPRINT_20260112_007_BE_remediation_pr_generator (REMEDY-BE-002)
/// </summary>
public string? PrBody { get; init; }
/// <summary>
/// Build result if available.
/// </summary>