mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
7ce106ed1b
File a GitHub issue from a failing test case, and let the case follow the
issue afterwards.
POST api/testcasemanagement/v1/testcase/{id}/issue file + link
DELETE api/testcasemanagement/v1/testcase/{id}/issue unlink
POST api/testcasemanagement/v1/issues/sync reconcile now
plus TestCaseIssueReconciler, a background sweep on the same code path as the
manual endpoint so the two cannot drift. Admin-gated: these are not part of the
2023 client's surface, unlike the existing test-case routes which are open
because the watch gates that tab client-side.
The link lives in TestCaseEntity.JiraBugUrl — the field Rec Room's own tooling
used for the bug filed against a failing case. That avoids a migration, which
matters more here than the name: the Postgres path is EnsureCreated-only and
never replays migrations, so a new column has to be added twice or it is missing
in production. A leftover genuine Jira link is ignored rather than misparsed,
since the issue number is only read from a GitHub-shaped URL.
Filing is idempotent — a case already carrying a live issue returns it instead
of filing again, so sweeping a failing pass repeatedly does not spam duplicates.
Unlinking leaves the issue alone; closing someone's issue as a side effect of
tidying a QA link would be the wrong call.
Reconcile moves a case only between the two states it owns: closed+Failed goes
to NotYetTested, open+NotYetTested goes back to Failed. A closed issue is NOT
marked Passed — closing is a developer's claim that the fix landed, not a
tester's observation that the case passes, so the case goes back in the queue.
The first draft flipped any non-Failed case to Failed on an open issue, which
contradicted its own doc comment and yanked a case out from under a tester
mid-claim; the test for that caught it.
Everything degrades rather than fails when unconfigured: IsConfigured is false
without a token, endpoints answer 503 with the reason, and the reconciler logs
once and idles. GitHub is faked in the tests, which cover idempotency, both
state transitions, the mid-claim case, and the unconfigured path.
90 lines
3.7 KiB
C#
90 lines
3.7 KiB
C#
using Microsoft.Extensions.Options;
|
|
using DorkNet.Server.Controllers.API.TestCaseManagement;
|
|
using DorkNet.Server.Data;
|
|
|
|
namespace DorkNet.Server.Services;
|
|
|
|
/// <summary>
|
|
/// Keeps linked test cases in step with their GitHub issues without anyone
|
|
/// having to press anything.
|
|
///
|
|
/// <para>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
|
|
/// <see cref="TestCaseIssuesController.ReconcileAsync"/> and is shared with the
|
|
/// manual endpoint, so the automatic and manual paths cannot drift.</para>
|
|
///
|
|
/// <para>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.</para>
|
|
/// </summary>
|
|
public sealed class TestCaseIssueReconciler(
|
|
IServiceScopeFactory scopeFactory,
|
|
IGitHubIssues github,
|
|
IConfiguration config,
|
|
ILogger<TestCaseIssueReconciler> log) : BackgroundService
|
|
{
|
|
/// <summary>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
|
|
/// <c>GitHub:ReconcileIntervalMinutes</c>; 0 or less disables the sweep
|
|
/// while leaving the manual endpoint working.</summary>
|
|
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<int?>("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<DorkNetDbContext>();
|
|
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<bool> SafeWaitAsync(PeriodicTimer timer, CancellationToken ct)
|
|
{
|
|
try { return await timer.WaitForNextTickAsync(ct); }
|
|
catch (OperationCanceledException) { return false; }
|
|
}
|
|
}
|