using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Collections.Immutable; namespace StellaOps.Cli.GitOps; /// /// Controller for GitOps-based release automation. /// Monitors Git repositories and triggers releases based on Git events. /// public sealed class GitOpsController : BackgroundService { private readonly IGitEventSource _eventSource; private readonly IReleaseService _releaseService; private readonly IPromotionService _promotionService; private readonly TimeProvider _timeProvider; private readonly GitOpsConfig _config; private readonly ILogger _logger; private readonly ConcurrentDictionary _repoStates = new(); public event EventHandler? ReleaseTriggered; public event EventHandler? PromotionTriggered; public event EventHandler? ValidationFailed; public GitOpsController( IGitEventSource eventSource, IReleaseService releaseService, IPromotionService promotionService, TimeProvider timeProvider, GitOpsConfig config, ILogger logger) { _eventSource = eventSource; _releaseService = releaseService; _promotionService = promotionService; _timeProvider = timeProvider; _config = config; _logger = logger; _eventSource.EventReceived += OnGitEventReceived; } /// /// Registers a repository for GitOps monitoring. /// public async Task RegisterRepositoryAsync( GitOpsRepositoryConfig repoConfig, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(repoConfig); _logger.LogInformation( "Registering repository {RepoUrl} for GitOps", repoConfig.RepositoryUrl); var state = new GitOpsState { RepositoryUrl = repoConfig.RepositoryUrl, Config = repoConfig, Status = GitOpsStatus.Active, RegisteredAt = _timeProvider.GetUtcNow() }; _repoStates[repoConfig.RepositoryUrl] = state; // Start monitoring await _eventSource.SubscribeAsync(repoConfig.RepositoryUrl, repoConfig.Branches, ct); return new RegistrationResult { Success = true, RepositoryUrl = repoConfig.RepositoryUrl, MonitoredBranches = repoConfig.Branches }; } /// /// Unregisters a repository from GitOps monitoring. /// public async Task UnregisterRepositoryAsync( string repositoryUrl, CancellationToken ct = default) { if (!_repoStates.TryRemove(repositoryUrl, out _)) { return false; } await _eventSource.UnsubscribeAsync(repositoryUrl, ct); _logger.LogInformation( "Unregistered repository {RepoUrl} from GitOps", repositoryUrl); return true; } /// /// Manually triggers a release for a commit. /// public async Task TriggerReleaseAsync( ManualTriggerRequest request, CancellationToken ct = default) { _logger.LogInformation( "Manually triggering release for {RepoUrl} at {CommitSha}", request.RepositoryUrl, request.CommitSha); var gitEvent = new GitEvent { Type = GitEventType.Push, RepositoryUrl = request.RepositoryUrl, Branch = request.Branch, CommitSha = request.CommitSha, CommitMessage = request.CommitMessage ?? "Manual trigger", Author = request.Author ?? "system", Timestamp = _timeProvider.GetUtcNow() }; return await ProcessGitEventAsync(gitEvent, ct); } /// /// Gets the status of all monitored repositories. /// public IReadOnlyList GetRepositoryStatuses() { return _repoStates.Values.ToList(); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("GitOps controller starting"); await _eventSource.StartAsync(stoppingToken); try { // Keep running until stopped await Task.Delay(Timeout.Infinite, stoppingToken); } catch (OperationCanceledException) { // Expected on shutdown } await _eventSource.StopAsync(CancellationToken.None); _logger.LogInformation("GitOps controller stopped"); } private async void OnGitEventReceived(object? sender, GitEvent e) { try { await ProcessGitEventAsync(e, CancellationToken.None); } catch (Exception ex) { _logger.LogError(ex, "Error processing Git event for {RepoUrl}", e.RepositoryUrl); } } private async Task ProcessGitEventAsync( GitEvent gitEvent, CancellationToken ct) { if (!_repoStates.TryGetValue(gitEvent.RepositoryUrl, out var state)) { return new TriggerResult { Success = false, Error = "Repository not registered" }; } _logger.LogDebug( "Processing {EventType} event for {RepoUrl} on {Branch}", gitEvent.Type, gitEvent.RepositoryUrl, gitEvent.Branch); // Check if branch matches triggers var trigger = FindMatchingTrigger(state.Config, gitEvent); if (trigger is null) { _logger.LogDebug( "No matching trigger for branch {Branch}", gitEvent.Branch); return new TriggerResult { Success = true, Skipped = true, Reason = "No matching trigger" }; } // Validate commit message patterns if configured if (!ValidateCommitMessage(gitEvent.CommitMessage, trigger)) { ValidationFailed?.Invoke(this, new GitOpsEventArgs { Event = gitEvent, Reason = "Commit message validation failed" }); return new TriggerResult { Success = false, Error = "Commit message validation failed" }; } // Execute trigger action return trigger.Action switch { TriggerAction.CreateRelease => await CreateReleaseAsync(gitEvent, trigger, ct), TriggerAction.Promote => await PromoteAsync(gitEvent, trigger, ct), TriggerAction.ValidateOnly => await ValidateAsync(gitEvent, trigger, ct), _ => new TriggerResult { Success = false, Error = "Unknown action" } }; } private GitOpsTrigger? FindMatchingTrigger(GitOpsRepositoryConfig config, GitEvent gitEvent) { return config.Triggers.FirstOrDefault(t => MatchesBranch(t.BranchPattern, gitEvent.Branch) && (t.EventTypes.Length == 0 || t.EventTypes.Contains(gitEvent.Type))); } private static bool MatchesBranch(string pattern, string branch) { if (pattern == "*") { return true; } if (pattern.EndsWith("/*")) { var prefix = pattern[..^2]; return branch.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } return pattern.Equals(branch, StringComparison.OrdinalIgnoreCase); } private static bool ValidateCommitMessage(string? message, GitOpsTrigger trigger) { if (string.IsNullOrEmpty(trigger.CommitMessagePattern)) { return true; } if (string.IsNullOrEmpty(message)) { return false; } var regex = new System.Text.RegularExpressions.Regex(trigger.CommitMessagePattern); return regex.IsMatch(message); } private async Task CreateReleaseAsync( GitEvent gitEvent, GitOpsTrigger trigger, CancellationToken ct) { _logger.LogInformation( "Creating release from {CommitSha} on {Branch}", gitEvent.CommitSha, gitEvent.Branch); try { var releaseId = await _releaseService.CreateReleaseAsync(new CreateReleaseRequest { RepositoryUrl = gitEvent.RepositoryUrl, CommitSha = gitEvent.CommitSha, Branch = gitEvent.Branch, Environment = trigger.TargetEnvironment ?? "development", Version = ExtractVersion(gitEvent, trigger), AutoPromote = trigger.AutoPromote }, ct); ReleaseTriggered?.Invoke(this, new GitOpsEventArgs { Event = gitEvent, ReleaseId = releaseId }); return new TriggerResult { Success = true, ReleaseId = releaseId }; } catch (Exception ex) { _logger.LogError(ex, "Failed to create release for {CommitSha}", gitEvent.CommitSha); return new TriggerResult { Success = false, Error = ex.Message }; } } private async Task PromoteAsync( GitEvent gitEvent, GitOpsTrigger trigger, CancellationToken ct) { _logger.LogInformation( "Promoting from {SourceEnv} to {TargetEnv}", trigger.SourceEnvironment, trigger.TargetEnvironment); try { var promotionId = await _promotionService.PromoteAsync(new PromoteRequest { SourceEnvironment = trigger.SourceEnvironment!, TargetEnvironment = trigger.TargetEnvironment!, CommitSha = gitEvent.CommitSha, AutoApprove = trigger.AutoApprove }, ct); PromotionTriggered?.Invoke(this, new GitOpsEventArgs { Event = gitEvent, PromotionId = promotionId }); return new TriggerResult { Success = true, PromotionId = promotionId }; } catch (Exception ex) { _logger.LogError(ex, "Failed to promote"); return new TriggerResult { Success = false, Error = ex.Message }; } } private Task ValidateAsync( GitEvent gitEvent, GitOpsTrigger trigger, CancellationToken ct) { _logger.LogInformation( "Validating commit {CommitSha}", gitEvent.CommitSha); // Validation-only mode - no actual release creation return Task.FromResult(new TriggerResult { Success = true, ValidationOnly = true }); } private static string ExtractVersion(GitEvent gitEvent, GitOpsTrigger trigger) { // Try to extract version from tag or branch if (gitEvent.Type == GitEventType.Tag && gitEvent.Tag is not null) { var tag = gitEvent.Tag; if (tag.StartsWith("v", StringComparison.OrdinalIgnoreCase)) { tag = tag[1..]; } return tag; } // Use commit SHA prefix as version return gitEvent.CommitSha[..8]; } } /// /// Configuration for GitOps controller. /// public sealed record GitOpsConfig { public TimeSpan PollInterval { get; init; } = TimeSpan.FromSeconds(30); public bool EnableWebhooks { get; init; } = true; public int MaxConcurrentEvents { get; init; } = 5; } /// /// Configuration for a GitOps-monitored repository. /// public sealed record GitOpsRepositoryConfig { public required string RepositoryUrl { get; init; } public ImmutableArray Branches { get; init; } = ["main", "release/*"]; public ImmutableArray Triggers { get; init; } = []; } /// /// A GitOps trigger definition. /// public sealed record GitOpsTrigger { public required string BranchPattern { get; init; } public ImmutableArray EventTypes { get; init; } = []; public required TriggerAction Action { get; init; } public string? TargetEnvironment { get; init; } public string? SourceEnvironment { get; init; } public string? CommitMessagePattern { get; init; } public bool AutoPromote { get; init; } public bool AutoApprove { get; init; } } /// /// Trigger action types. /// public enum TriggerAction { CreateRelease, Promote, ValidateOnly } /// /// State of a monitored repository. /// public sealed record GitOpsState { public required string RepositoryUrl { get; init; } public required GitOpsRepositoryConfig Config { get; init; } public required GitOpsStatus Status { get; init; } public required DateTimeOffset RegisteredAt { get; init; } public DateTimeOffset? LastEventAt { get; init; } public string? LastCommitSha { get; init; } } /// /// GitOps status. /// public enum GitOpsStatus { Active, Paused, Error } /// /// A Git event. /// public sealed record GitEvent { public required GitEventType Type { get; init; } public required string RepositoryUrl { get; init; } public required string Branch { get; init; } public required string CommitSha { get; init; } public string? CommitMessage { get; init; } public string? Tag { get; init; } public required string Author { get; init; } public required DateTimeOffset Timestamp { get; init; } } /// /// Git event types. /// public enum GitEventType { Push, Tag, PullRequest, Merge } /// /// Result of repository registration. /// public sealed record RegistrationResult { public required bool Success { get; init; } public string? RepositoryUrl { get; init; } public ImmutableArray MonitoredBranches { get; init; } = []; public string? Error { get; init; } } /// /// Request to manually trigger. /// public sealed record ManualTriggerRequest { public required string RepositoryUrl { get; init; } public required string Branch { get; init; } public required string CommitSha { get; init; } public string? CommitMessage { get; init; } public string? Author { get; init; } } /// /// Result of a trigger. /// public sealed record TriggerResult { public required bool Success { get; init; } public bool Skipped { get; init; } public bool ValidationOnly { get; init; } public Guid? ReleaseId { get; init; } public Guid? PromotionId { get; init; } public string? Reason { get; init; } public string? Error { get; init; } } /// /// Event args for GitOps events. /// public sealed class GitOpsEventArgs : EventArgs { public required GitEvent Event { get; init; } public Guid? ReleaseId { get; init; } public Guid? PromotionId { get; init; } public string? Reason { get; init; } } /// /// Request to create a release. /// public sealed record CreateReleaseRequest { public required string RepositoryUrl { get; init; } public required string CommitSha { get; init; } public required string Branch { get; init; } public required string Environment { get; init; } public required string Version { get; init; } public bool AutoPromote { get; init; } } /// /// Request to promote. /// public sealed record PromoteRequest { public required string SourceEnvironment { get; init; } public required string TargetEnvironment { get; init; } public required string CommitSha { get; init; } public bool AutoApprove { get; init; } } /// /// Interface for Git event source. /// public interface IGitEventSource { event EventHandler? EventReceived; Task StartAsync(CancellationToken ct = default); Task StopAsync(CancellationToken ct = default); Task SubscribeAsync(string repositoryUrl, ImmutableArray branches, CancellationToken ct = default); Task UnsubscribeAsync(string repositoryUrl, CancellationToken ct = default); } /// /// Interface for release service. /// public interface IReleaseService { Task CreateReleaseAsync(CreateReleaseRequest request, CancellationToken ct = default); } /// /// Interface for promotion service. /// public interface IPromotionService { Task PromoteAsync(PromoteRequest request, CancellationToken ct = default); }