using Microsoft.Extensions.Options; using DorkNet.Server.Controllers.API.TestCaseManagement; using DorkNet.Server.Data; namespace DorkNet.Server.Services; /// /// Keeps linked test cases in step with their GitHub issues without anyone /// having to press anything. /// /// A closed issue means the bug is fixed, so its test case is due for /// another run; a reopened one means it came back. Waiting for a human to /// notice that defeats the point of linking them, so this sweeps on an /// interval. The reconcile logic itself lives in /// and is shared with the /// manual endpoint, so the automatic and manual paths cannot drift. /// /// Does nothing at all when GitHub is unconfigured — a server without a /// token is a normal deployment, not a broken one, and it should not log a /// failure every interval forever. /// public sealed class TestCaseIssueReconciler( IServiceScopeFactory scopeFactory, IGitHubIssues github, IConfiguration config, ILogger log) : BackgroundService { /// How often to sweep. Issue state changes are not urgent — a QA /// pass is a human-scale activity — and each sweep costs one API call per /// linked case against a rate limit shared with everything else using the /// token, so this is deliberately slow. Override with /// GitHub:ReconcileIntervalMinutes; 0 or less disables the sweep /// while leaving the manual endpoint working. private const int DefaultIntervalMinutes = 15; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { if (!github.IsConfigured) { log.LogInformation( "[testcase-issue] GitHub not configured; the reconciler is idle"); return; } var minutes = config.GetValue("GitHub:ReconcileIntervalMinutes") ?? DefaultIntervalMinutes; if (minutes <= 0) { log.LogInformation( "[testcase-issue] reconcile interval is {Minutes}; sweeps disabled", minutes); return; } log.LogInformation( "[testcase-issue] reconciling against {Repo} every {Minutes} minute(s)", github.Repository, minutes); using var timer = new PeriodicTimer(TimeSpan.FromMinutes(minutes)); while (await SafeWaitAsync(timer, stoppingToken)) { try { await using var scope = scopeFactory.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var changes = await TestCaseIssuesController.ReconcileAsync( db, github, log, stoppingToken); if (changes.Count > 0) log.LogInformation("[testcase-issue] sweep updated {Count} case(s)", changes.Count); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { return; } catch (Exception ex) { // One bad sweep must not take the loop down — GitHub being // unreachable for a minute is not a reason to stop // reconciling for the lifetime of the process. log.LogWarning(ex, "[testcase-issue] reconcile sweep failed; retrying next interval"); } } } private static async Task SafeWaitAsync(PeriodicTimer timer, CancellationToken ct) { try { return await timer.WaitForNextTickAsync(ct); } catch (OperationCanceledException) { return false; } } }