fix(worker): kept worktree in a company repo opens a PR, unexpected ADO/git errors never fail a task start

This commit is contained in:
mika kuns
2026-10-01 12:25:15 +02:00
parent 93fd6740a1
commit 4917533cb4
7 changed files with 54 additions and 13 deletions
@@ -45,8 +45,6 @@ public sealed class WorktreeRepository
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
}
/// <summary>Records a successful auto-rebase: the branch's fork point moves to the rebased-onto
/// commit and its head moves to the new tip <c>git rebase</c> produced.</summary>
/// <summary>Stores the draft PR an approve in a company repo created or reused.</summary>
public async Task SetPullRequestUrlAsync(string taskId, string url, CancellationToken ct = default)
{
@@ -55,6 +53,8 @@ public sealed class WorktreeRepository
.ExecuteUpdateAsync(s => s.SetProperty(w => w.PullRequestUrl, url), ct);
}
/// <summary>Records a successful auto-rebase: the branch's fork point moves to the rebased-onto
/// commit and its head moves to the new tip <c>git rebase</c> produced.</summary>
public async Task SetRebasedAsync(string taskId, string baseCommit, string headCommit, CancellationToken ct = default)
{
await _context.Worktrees
@@ -121,7 +121,7 @@ public sealed class AzureDevOpsClient
private static string StripHeads(string branch) =>
branch.StartsWith("refs/heads/", StringComparison.Ordinal) ? branch["refs/heads/".Length..] : branch;
private async Task<JsonNode> SendAsync(
private async Task<JsonObject> SendAsync(
AzdoRepo repo, HttpMethod method, string path, string? query, string? body, CancellationToken ct)
{
var pat = Pat;
@@ -153,7 +153,11 @@ public sealed class AzureDevOpsClient
throw new AzureDevOpsException("Azure DevOps 203: PAT abgelaufen oder ohne Scope Code (Read & Write).");
if (response.IsSuccessStatusCode)
{
try { return JsonNode.Parse(payload) ?? new JsonObject(); }
try
{
return JsonNode.Parse(payload) as JsonObject
?? throw new AzureDevOpsException($"Unerwartete Antwort von Azure DevOps bei {method} {path}.");
}
catch (JsonException ex) { throw new AzureDevOpsException($"Unerwartete Antwort von Azure DevOps: {ex.Message}"); }
}
@@ -174,9 +178,10 @@ public sealed class AzureDevOpsClient
{
try
{
if (JsonNode.Parse(payload)?["message"]?.GetValue<string>() is { Length: > 0 } msg) return msg;
if (JsonNode.Parse(payload) is JsonObject o && o["message"]?.GetValue<string>() is { Length: > 0 } msg) return msg;
}
catch (JsonException) { }
// InvalidOperationException: "message" exists but is not a string.
catch (Exception ex) when (ex is JsonException or InvalidOperationException) { }
return payload.Length <= 500 ? payload : payload[..500];
}
}
@@ -83,7 +83,8 @@ public sealed class PullRequestPublisher
var description = BuildDescription(task, ticketId, await ResolveProjectIdAsync(ticketId, ct), verifyCommand);
(url, created) = await _azdo.EnsureDraftPullRequestAsync(repo, wt.BranchName, target, title, description, ct);
}
catch (AzureDevOpsException ex)
// InvalidOperationException: a field of the ADO answer has an unexpected JSON type.
catch (Exception ex) when (ex is AzureDevOpsException or InvalidOperationException)
{
return Blocked($"gepusht, PR fehlgeschlagen: {ex.Message}");
}
@@ -1054,7 +1054,8 @@ public sealed class TaskMergeService
}
}
if (wt is { State: WorktreeState.Active } && _pullRequests is not null
// Kept (parked or reverted) goes the PR way too: a local merge has no place in a company repo.
if (wt is { State: WorktreeState.Active or WorktreeState.Kept } && _pullRequests is not null
&& !string.IsNullOrWhiteSpace(list.WorkingDir)
&& await _pullRequests.IsCompanyRepoAsync(list.WorkingDir, ct))
return await ApproveCompanyAsync(task, list, wt, verifyCommand, ct, progress);
@@ -1164,9 +1165,18 @@ public sealed class TaskMergeService
if (failed is not null) return failed;
}
return await ProgressReporter.RunAsync(
var result = await ProgressReporter.RunAsync(
_pullRequests.PublishAsync(task, list, wt, ranVerify ? verifyCommand : null, ct),
ProgressReportInterval, progress, "push + draft PR running");
// A branch with an open PR is under review again, whatever parked it before.
if (wt.State == WorktreeState.Kept && result.PullRequestUrl is not null)
{
using (var ctx = _dbFactory.CreateDbContext())
await new WorktreeRepository(ctx).SetStateAsync(task.Id, WorktreeState.Active, ct);
await _broadcaster.WorktreeUpdated(task.Id);
}
return result;
}
/// <summary>
+12 -3
View File
@@ -282,8 +282,17 @@ public sealed class WorktreeManager
.Select(c => c.BaseBranch)
.FirstOrDefaultAsync(ct)).NullIfBlank();
if (isCompany && await FetchCompanyBaseAsync(workingDir, baseBranch, ct) is { } fetched)
return fetched;
if (isCompany)
{
try
{
if (await FetchCompanyBaseAsync(workingDir, baseBranch, ct) is { } fetched) return fetched;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Fetching the company base in {working_dir} failed, continuing with the local branch", workingDir);
}
}
return baseBranch is null
? await _git.RevParseHeadAsync(workingDir, ct)
@@ -304,7 +313,7 @@ public sealed class WorktreeManager
if (basis is null && _azdo is not null && pat is not null && AzureDevOpsClient.ParseRemote(remote) is { } repo)
{
try { basis = await _azdo.GetDefaultBranchAsync(repo, ct); }
catch (AzureDevOpsException ex) { _logger.LogWarning("Default branch from Azure DevOps unavailable: {reason}", ex.Message); }
catch (Exception ex) when (ex is AzureDevOpsException or InvalidOperationException) { _logger.LogWarning("Default branch from Azure DevOps unavailable: {reason}", ex.Message); }
}
basis ??= await _git.GetRemoteHeadBranchAsync(workingDir, ct);
if (basis is null)