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:
@@ -29,7 +29,7 @@ There is no separate "Merge all" entry.
|
|||||||
the unit branch (`UnitBranchService.AbsorbChildAsync`, ff-only) instead of merging to the target.
|
the unit branch (`UnitBranchService.AbsorbChildAsync`, ff-only) instead of merging to the target.
|
||||||
- **No active worktree** (sandbox run) → straight to `Done`.
|
- **No active worktree** (sandbox run) → straight to `Done`.
|
||||||
- **Company repo** (`PullRequestPublisher.IsCompanyRepoAsync`, origin matches
|
- **Company repo** (`PullRequestPublisher.IsCompanyRepoAsync`, origin matches
|
||||||
`company_remote_pattern`, checked live) with an `Active` worktree → `ApproveCompanyAsync`
|
`company_remote_pattern`, checked live) with an `Active` or `Kept` worktree → `ApproveCompanyAsync`
|
||||||
instead of a merge, after the planning-parent gate and the child absorb: missing PAT / non-ADO
|
instead of a merge, after the planning-parent gate and the child absorb: missing PAT / non-ADO
|
||||||
remote is `blocked` before anything runs, then the verify command in the **worktree**
|
remote is `blocked` before anything runs, then the verify command in the **worktree**
|
||||||
(`skipVerify` ignored — no local merge to defer it to; red = `verify_failed`, nothing pushed),
|
(`skipVerify` ignored — no local merge to defer it to; red = `verify_failed`, nothing pushed),
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ public sealed class WorktreeRepository
|
|||||||
.SetProperty(w => w.MergeCommit, mergeCommit), ct);
|
.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>
|
/// <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)
|
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);
|
.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)
|
public async Task SetRebasedAsync(string taskId, string baseCommit, string headCommit, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _context.Worktrees
|
await _context.Worktrees
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ public sealed class AzureDevOpsClient
|
|||||||
private static string StripHeads(string branch) =>
|
private static string StripHeads(string branch) =>
|
||||||
branch.StartsWith("refs/heads/", StringComparison.Ordinal) ? branch["refs/heads/".Length..] : 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)
|
AzdoRepo repo, HttpMethod method, string path, string? query, string? body, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var pat = Pat;
|
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).");
|
throw new AzureDevOpsException("Azure DevOps 203: PAT abgelaufen oder ohne Scope Code (Read & Write).");
|
||||||
if (response.IsSuccessStatusCode)
|
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}"); }
|
catch (JsonException ex) { throw new AzureDevOpsException($"Unerwartete Antwort von Azure DevOps: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,9 +178,10 @@ public sealed class AzureDevOpsClient
|
|||||||
{
|
{
|
||||||
try
|
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];
|
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);
|
var description = BuildDescription(task, ticketId, await ResolveProjectIdAsync(ticketId, ct), verifyCommand);
|
||||||
(url, created) = await _azdo.EnsureDraftPullRequestAsync(repo, wt.BranchName, target, title, description, ct);
|
(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}");
|
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)
|
&& !string.IsNullOrWhiteSpace(list.WorkingDir)
|
||||||
&& await _pullRequests.IsCompanyRepoAsync(list.WorkingDir, ct))
|
&& await _pullRequests.IsCompanyRepoAsync(list.WorkingDir, ct))
|
||||||
return await ApproveCompanyAsync(task, list, wt, verifyCommand, ct, progress);
|
return await ApproveCompanyAsync(task, list, wt, verifyCommand, ct, progress);
|
||||||
@@ -1164,9 +1165,18 @@ public sealed class TaskMergeService
|
|||||||
if (failed is not null) return failed;
|
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),
|
_pullRequests.PublishAsync(task, list, wt, ranVerify ? verifyCommand : null, ct),
|
||||||
ProgressReportInterval, progress, "push + draft PR running");
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -282,8 +282,17 @@ public sealed class WorktreeManager
|
|||||||
.Select(c => c.BaseBranch)
|
.Select(c => c.BaseBranch)
|
||||||
.FirstOrDefaultAsync(ct)).NullIfBlank();
|
.FirstOrDefaultAsync(ct)).NullIfBlank();
|
||||||
|
|
||||||
if (isCompany && await FetchCompanyBaseAsync(workingDir, baseBranch, ct) is { } fetched)
|
if (isCompany)
|
||||||
return fetched;
|
{
|
||||||
|
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
|
return baseBranch is null
|
||||||
? await _git.RevParseHeadAsync(workingDir, ct)
|
? 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)
|
if (basis is null && _azdo is not null && pat is not null && AzureDevOpsClient.ParseRemote(remote) is { } repo)
|
||||||
{
|
{
|
||||||
try { basis = await _azdo.GetDefaultBranchAsync(repo, ct); }
|
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);
|
basis ??= await _git.GetRemoteHeadBranchAsync(workingDir, ct);
|
||||||
if (basis is null)
|
if (basis is null)
|
||||||
|
|||||||
@@ -158,6 +158,22 @@ public sealed class CompanyApproveTests : IDisposable
|
|||||||
Assert.StartsWith("Resolves [#412](https://ticket.fb-tuning.local/board/project/393/ticket/412)", description);
|
Assert.StartsWith("Resolves [#412](https://ticket.fb-tuning.local/board/project/393/ticket/412)", description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Approve_KeptWorktree_OpensPrInsteadOfMergingLocally()
|
||||||
|
{
|
||||||
|
if (!GitRepoFixture.IsGitAvailable()) return;
|
||||||
|
await SetUpWorktree();
|
||||||
|
using (var ctx = _db.CreateContext())
|
||||||
|
await new WorktreeRepository(ctx).SetStateAsync("t1", WorktreeState.Kept);
|
||||||
|
var mainBefore = GitRepoFixture.RunGit(_repo.RepoDir, "rev-parse", "main").Trim();
|
||||||
|
|
||||||
|
var r = await Merge(Ado()).ApproveAndMergeAsync("t1", "", default);
|
||||||
|
|
||||||
|
Assert.Equal(TaskMergeService.StatusPrCreated, r.Status);
|
||||||
|
Assert.Equal(mainBefore, GitRepoFixture.RunGit(_repo.RepoDir, "rev-parse", "main").Trim());
|
||||||
|
Assert.Equal(WorktreeState.Active, (await Rows()).Wt.State);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Approve_ReusesActivePr_AsUpdate()
|
public async Task Approve_ReusesActivePr_AsUpdate()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user