old sprints work, new sprints for exposing functionality via cli, improve code_of_conduct and other agents instructions
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user